diff --git a/.github/workflows/build-manager.yml b/.github/workflows/build-manager.yml index 3949a7bd6..1ef40e771 100644 --- a/.github/workflows/build-manager.yml +++ b/.github/workflows/build-manager.yml @@ -184,9 +184,6 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 - - name: Setup Android SDK - uses: android-actions/setup-android@v4 - - name: Build APK run: | if [ "${{ github.event_name }}" == "pull_request" ]; then diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 37800c82c..a33a6a6ba 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -75,9 +75,6 @@ jobs: build-scan-publish: false cache-read-only: true - - name: Setup Android SDK - uses: android-actions/setup-android@v4 - - name: Grant execute permission for gradlew run: chmod +x ./gradlew diff --git a/.github/workflows/lints-check.yml b/.github/workflows/lints-check.yml index e8bbfa688..3e9926fbe 100644 --- a/.github/workflows/lints-check.yml +++ b/.github/workflows/lints-check.yml @@ -123,9 +123,6 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 - - - name: Setup Android SDK - uses: android-actions/setup-android@v4 - name: Check Manager Lint run: | diff --git a/kernel/compat/kernel_compat.c b/kernel/compat/kernel_compat.c index 8be372694..e7936b184 100644 --- a/kernel/compat/kernel_compat.c +++ b/kernel/compat/kernel_compat.c @@ -9,6 +9,7 @@ #include #include #include +#include #include "klog.h" // IWYU pragma: keep #include "kernel_compat.h" @@ -196,26 +197,11 @@ void ksu_run_in_init_if_possible(void (*callback)(void *), void *data) } #ifdef KSU_COMPAT_REQUIRE_SESSION_KEYRING -#include -#include -#include #include "ksu.h" -static inline struct key *ksu_get_session_keyring(const struct cred *cred) -{ -// https://github.com/torvalds/linux/commit/3a50597de8635cd05133bd12c95681c82fe7b878 -#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 8, 0) - return rcu_dereference(cred->session_keyring); -#else - return rcu_dereference(current->cred->tgcred->session_keyring); -#endif -} - +struct key *init_session_keyring = NULL; extern int install_session_keyring_to_cred(struct cred *, struct key *); -// WARNING! Make sure caller in init!!! -// https://github.com/torvalds/linux/commit/5c7e372caa35d303e414caeb64ee2243fd3cac3d -// in our target kernel version, it are protected by rcu, so let's rcu_dereference here void setup_ksu_cred_session_keyring(void) { if (ksu_get_session_keyring(ksu_cred)) { @@ -223,12 +209,12 @@ void setup_ksu_cred_session_keyring(void) return; } - if (strcmp(current->comm, "init")) { - // we are only interested in `init` process + if (init_session_keyring == NULL) { + // if init_session_keyring is null, skip return; } - install_session_keyring_to_cred(ksu_cred, ksu_get_session_keyring(current_cred())); + install_session_keyring_to_cred(ksu_cred, init_session_keyring); pr_info("kernel_compat: %s: install init_session_keyring to ksu_cred\n", __func__); } diff --git a/kernel/compat/kernel_compat.h b/kernel/compat/kernel_compat.h index 50141dc27..14e952927 100644 --- a/kernel/compat/kernel_compat.h +++ b/kernel/compat/kernel_compat.h @@ -279,7 +279,20 @@ extern void ksu_run_in_init_if_possible(void (*callback)(void *), void *data); #if defined(CONFIG_KEYS) && (LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) || defined(KSU_COMPAT_IS_HISI_LEGACY) || \ defined(KSU_COMPAT_IS_HISI_LEGACY_HM2)) #define KSU_COMPAT_REQUIRE_SESSION_KEYRING +#include + +extern struct key *init_session_keyring; extern void setup_ksu_cred_session_keyring(void); + +static inline struct key *ksu_get_session_keyring(const struct cred *cred) +{ +// https://github.com/torvalds/linux/commit/3a50597de8635cd05133bd12c95681c82fe7b878 +#if LINUX_VERSION_CODE >= KERNEL_VERSION(3, 8, 0) + return rcu_dereference(cred->session_keyring); +#else + return rcu_dereference(current->cred->tgcred->session_keyring); +#endif +} #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 3, 0) || defined(KSU_HAS_MODERN_STATIC_KEY_INTERFACE) diff --git a/kernel/core/init.c b/kernel/core/init.c index 9be1c15b5..78d9e6ae9 100644 --- a/kernel/core/init.c +++ b/kernel/core/init.c @@ -142,6 +142,10 @@ void setup_ksu_cred(void) { setup_ksu_cred_selinux(); #ifdef KSU_COMPAT_REQUIRE_SESSION_KEYRING + if (init_session_keyring == NULL) { + init_session_keyring = ksu_get_session_keyring(current_cred()); + } + setup_ksu_cred_session_keyring(); #endif } @@ -155,6 +159,11 @@ bool allow_shell = false; bool ksu_no_custom_rc = false; module_param_named(norc, ksu_no_custom_rc, bool, 0); +#ifdef MODULE +bool ksu_bundled = false; +module_param_named(bundled, ksu_bundled, bool, 0); +#endif + char ksu_block_modules[256]; module_param_string(block_modules, ksu_block_modules, sizeof(ksu_block_modules), 0); MODULE_PARM_DESC(block_modules, "Comma-separated preset module names to acknowledge without loading"); diff --git a/kernel/feature/adb_root.c b/kernel/feature/adb_root.c index b795a6012..f79030db3 100644 --- a/kernel/feature/adb_root.c +++ b/kernel/feature/adb_root.c @@ -8,6 +8,12 @@ #include #include #include +#include + +// https://github.com/torvalds/linux/commit/68db0cf10678630d286f4bbbbdfa102951a35faa +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0) +#include +#endif #include "adb_root.h" #include "arch.h" diff --git a/kernel/feature/kernel_umount.c b/kernel/feature/kernel_umount.c index c7bcfd767..78ed9522d 100644 --- a/kernel/feature/kernel_umount.c +++ b/kernel/feature/kernel_umount.c @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -31,7 +30,6 @@ #include "feature/sucompat.h" static bool ksu_kernel_umount_enabled = true; -bool ksu_webview_zygote_umount_enabled = false; static int kernel_umount_feature_get(u64 *value) { @@ -54,27 +52,6 @@ static const struct ksu_feature_handler kernel_umount_handler = { .set_handler = kernel_umount_feature_set, }; -static int webview_zygote_umount_feature_get(u64 *value) -{ - *value = ksu_webview_zygote_umount_enabled ? 1 : 0; - return 0; -} - -static int webview_zygote_umount_feature_set(u64 value) -{ - bool enable = value != 0; - ksu_webview_zygote_umount_enabled = enable; - pr_info("webview_zygote_umount: set to %d\n", enable); - return 0; -} - -static const struct ksu_feature_handler webview_zygote_umount_handler = { - .feature_id = KSU_FEATURE_WEBVIEW_ZYGOTE_UMOUNT, - .name = "webview_zygote_umount", - .get_handler = webview_zygote_umount_feature_get, - .set_handler = webview_zygote_umount_feature_set, -}; - #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0) || defined(KSU_HAS_PATH_UMOUNT) extern int path_umount(struct path *path, int flags); static void ksu_umount_mnt(const char *mnt, struct path *path, int flags) @@ -152,7 +129,7 @@ int ksu_handle_umount(uid_t old_uid, uid_t new_uid) // 1. Normal app: zygote -> appuid // 2. Isolated process forked from zygote: zygote -> isolated_process // 3. App zygote forked from zygote: zygote -> appuid - // 4. Webview zygote forked from zygote: zygote -> webview_zygote (controlled by feature policy) + // 4. Webview zygote forked from zygote: zygote -> webview_zygote // 5. Isolated process forked from app zygote: appuid -> isolated_process (already handled by 3) // 6. Isolated process forked from webview zygote (already handled by 4) if (!is_appuid(new_uid) && new_uid != WEBVIEW_ZYGOTE_UID && !is_isolated_process(new_uid)) { @@ -205,13 +182,9 @@ void __init ksu_kernel_umount_init(void) if (ksu_register_feature_handler(&kernel_umount_handler)) { pr_err("Failed to register kernel_umount feature handler\n"); } - if (ksu_register_feature_handler(&webview_zygote_umount_handler)) { - pr_err("Failed to register webview_zygote_umount feature handler\n"); - } } void __exit ksu_kernel_umount_exit(void) { - ksu_unregister_feature_handler(KSU_FEATURE_WEBVIEW_ZYGOTE_UMOUNT); ksu_unregister_feature_handler(KSU_FEATURE_KERNEL_UMOUNT); } diff --git a/kernel/feature/kernel_umount.h b/kernel/feature/kernel_umount.h index 355f8ca5c..d76044a04 100644 --- a/kernel/feature/kernel_umount.h +++ b/kernel/feature/kernel_umount.h @@ -7,7 +7,6 @@ void ksu_kernel_umount_init(void); void ksu_kernel_umount_exit(void); -extern bool ksu_webview_zygote_umount_enabled; // Handler function to be called from setresuid hook int ksu_handle_umount(uid_t old_uid, uid_t new_uid); diff --git a/kernel/feature/sucompat.c b/kernel/feature/sucompat.c index 4fd64343d..aa4a13ebf 100644 --- a/kernel/feature/sucompat.c +++ b/kernel/feature/sucompat.c @@ -33,6 +33,7 @@ #include "runtime/ksud.h" #include "feature/sucompat.h" #include "policy/app_profile.h" +#include "supercall/supercall.h" #ifdef CONFIG_KSU_TRACEPOINT_HOOK #include "hook/syscall_hook.h" #else @@ -235,6 +236,7 @@ static long ksu_handle_execve_sucompat_common_internal(const char __user **filen char path[sizeof(su_path) + 1]; long ret, orig_regs[5]; unsigned long addr; + int su_fd = -1; int tmp_fd; struct file *ksud_file; const struct cred *old_cred; @@ -309,6 +311,13 @@ static long ksu_handle_execve_sucompat_common_internal(const char __user **filen regs->__PT_PARM3_REG = orig_regs[2]; regs->__PT_SYSCALL_PARM4_REG = orig_regs[3]; regs->__PT_PARM5_REG = orig_regs[4]; + } else { + // Only grant the scoped driver capability after the selected root + // profile has been applied successfully. + su_fd = ksu_install_su_fd(); + if (su_fd < 0) { + pr_warn("install su session fd failed: %d\n", su_fd); + } } return ret; @@ -343,19 +352,19 @@ static inline int do_ksu_handle_execveat_sucompat(int *fd, const char *filename, // Yep, maybe someusers love turn off sucompat <- idk how they managed to keep using it // But for mostly users, sucompat is enabled, so unlikely here if (!static_branch_unlikely(&ksu_su_compat_enabled)) { - return 0; + return -EINVAL; } #else if (!ksu_su_compat_enabled) { - return 0; + return -EINVAL; } #endif if (!is_allowed) - return 0; + return -EINVAL; if (likely(memcmp(filename, su_path, sizeof(su_path)))) - return 0; + return -EINVAL; pr_info("do_execveat_common su found\n"); @@ -375,6 +384,24 @@ static inline int do_ksu_handle_execveat_sucompat(int *fd, const char *filename, memcpy((void *)filename, ksud_path, sizeof(ksud_path)); out: ksu_sulog_emit_pending(pending_sucompat, 0, GFP_KERNEL); + // always flag that, to avoid old version of susfs hang in boot + set_thread_flag(TIF_PROC_IN_KSU_EXECVE); + return 0; +} + +// fd, filename, argv, envp, flags and retval were NOT provided in bprm_committed_creds (KSU_COMPAT_NO_POST_EXECVE_HOOK)! +int ksu_handle_post_execve(int *fd, const char *filename, void *argv, void *envp, int *flags, int *retval) +{ + if (likely(!test_thread_flag(TIF_PROC_IN_KSU_EXECVE))) { + return -EINVAL; + } +#ifndef KSU_COMPAT_HAS_SUSFS_INSTALL_SU_FD_DIRECT_CALL + ksu_install_su_fd(); +#endif + // #ifdef KSU_COMPAT_NO_POST_EXECVE_HOOK + // return 0; + // #endif + // TODO Implement tmpfd of ksud when KSU_COMPAT_NO_POST_EXECVE_HOOK is not defined return 0; } @@ -412,7 +439,7 @@ int ksu_handle_execve(int *fd, const char *filename, void *argv, void *envp, int #ifndef CONFIG_KSU_TRACEPOINT_HOOK if (ksu_is_current_proc_unprivillege()) { - return 0; + return -EINVAL; } #endif @@ -424,7 +451,7 @@ int ksu_handle_execve(int *fd, const char *filename, void *argv, void *envp, int } if (*fd != AT_FDCWD || *flags != 0) { - return 0; + return -EINVAL; } skip_check: @@ -453,21 +480,29 @@ int ksu_handle_execve(int *fd, const char *filename, void *argv, void *envp, int return ret; } -// old hook, link to ksu_handle_execve int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags) { struct filename *filename; filename = *filename_ptr; if (IS_ERR(filename)) { - return 0; + return -EINVAL; } return ksu_handle_execve(fd, filename->name, argv, envp, flags); } -// because simonpunk, he do check in hook side -// and call ksu_handle_execveat_sucompat -// we need unpack filename* in here, and pass it to ksu_handle_execveat +int ksu_handle_post_execveat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags, int *retval) +{ + struct filename *filename; + filename = *filename_ptr; + if (IS_ERR(filename)) { + return -EINVAL; + } + + return ksu_handle_post_execve(fd, filename->name, argv, envp, flags, retval); +} + +// compat for check in hook #ifdef CONFIG_KSU_SUSFS int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags) { @@ -479,6 +514,12 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, void * return ksu_handle_execveat(fd, filename_ptr, argv, envp, flags); } + +int ksu_handle_post_execveat_sucompat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags, + int *retval) +{ + return ksu_handle_post_execveat(fd, filename_ptr, argv, envp, flags, retval); +} #endif #endif diff --git a/kernel/feature/sucompat.h b/kernel/feature/sucompat.h index 0c6365c0b..efdc23d14 100644 --- a/kernel/feature/sucompat.h +++ b/kernel/feature/sucompat.h @@ -20,6 +20,7 @@ int ksu_handle_stat(int *dfd, struct filename **filename, int *flags); #else 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_post_execve(int *fd, const char *filename, void *argv, void *envp, int *flags, int *retval); #endif // #ifdef CONFIG_KSU_SUSFS #ifdef CONFIG_KSU_TRACEPOINT_HOOK @@ -42,17 +43,32 @@ long ksu_handle_execveat_sucompat_internal(const char __user **filename_user, in #elif defined(CONFIG_KSU_SUSFS) // susfs #include +// sync with manual hook +#define TIF_PROC_IN_KSU_EXECVE 61 + #define ksu_is_current_proc_unprivillege susfs_is_current_proc_no_su #define ksu_set_current_proc_unprivillege susfs_set_current_proc_no_su #define ksu_clear_current_proc_unprivillege susfs_clear_current_proc_no_su #else // manual hook +// we have a huge number spare TIFs can use +// https://elixir.bootlin.com/linux/v7.2.2/source/arch/arm64/include/asm/thread_info.h#L90 +// https://elixir.bootlin.com/linux/v7.2.2/source/arch/arm/include/asm/thread_info.h#L154 +// https://elixir.bootlin.com/linux/v7.2.2/source/arch/x86/include/asm/thread_info.h#L103 +// 23 - 31 is spare in arm32 (9 tifs) +// 32 - 63 is spare in arm64 (32 tifs) +// 28 - 31 is spare in x86 (4 tifs) +// 28 - 63 is spare in x86-64 (36 tifs) + // 63 already used as TIF_KSU_DISABLE_ESCAPE_WITH_ROOT (64bit) // 31 already used as TIF_KSU_DISABLE_ESCAPE_WITH_ROOT (32bit) +// TIF_PROC_IN_KSU_EXECVE may reuse in future? because it only useful when current->in_execve=1 #ifdef CONFIG_64BIT #define TIF_PROC_NON_PRIVILEGE 62 +#define TIF_PROC_IN_KSU_EXECVE 61 #else #define TIF_PROC_NON_PRIVILEGE 30 +#define TIF_PROC_IN_KSU_EXECVE 29 #endif static inline bool ksu_is_current_proc_unprivillege(void) diff --git a/kernel/hook/lsm_hook_magic.h b/kernel/hook/lsm_hook_magic.h index c98aec693..905f5df31 100644 --- a/kernel/hook/lsm_hook_magic.h +++ b/kernel/hook/lsm_hook_magic.h @@ -31,12 +31,17 @@ struct ksu_lsm_hook { int offset; }; +// clang-format off #define KSU_LSM_HOOK_INIT(member, target_symbol, replacement_fn, off) \ { \ - .head_name = #member, .target_name = target_symbol, .head_offset = offsetof(KSU_LSM_HOOK_HEADS_TYPE, member), \ - .hook_offset = offsetof(struct security_hook_list, hook.member), .replacement = (void *)(replacement_fn), \ + .head_name = #member, \ + .target_name = target_symbol, \ + .head_offset = offsetof(KSU_LSM_HOOK_HEADS_TYPE, member), \ + .hook_offset = offsetof(struct security_hook_list, hook.member), \ + .replacement = (void *)(replacement_fn), \ .offset = off, \ } +// clang-format on // This API implements runtime patching of existing LSM hook slots. It is a // workaround for out-of-tree modules, not the normal LSM registration path via diff --git a/kernel/hook/lsm_hooks.c b/kernel/hook/lsm_hooks.c index e07ed438c..45e60f1fd 100644 --- a/kernel/hook/lsm_hooks.c +++ b/kernel/hook/lsm_hooks.c @@ -68,6 +68,38 @@ static int ksu_inode_rename(struct inode *old_inode, struct dentry *old_dentry, return 0; } +#ifdef KSU_COMPAT_NO_POST_EXECVE_HOOK +#include +#include "feature/sucompat.h" + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 7, 0) || \ + defined(KSU_COMPAT_CONSTIFY_BPRM_PARAMETER_IN_SECURITY_BPRM_COMMITTED_CREDS) +static void ksu_handle_bprm_committed_creds(const struct linux_binprm *bprm) +#else +static void ksu_handle_bprm_committed_creds(struct linux_binprm *bprm) +#endif +{ + ksu_handle_post_execve(NULL, NULL, NULL, NULL, NULL, NULL); +} +#endif + +#ifdef KSU_COMPAT_REQUIRE_SESSION_KEYRING +static int ksu_handle_key_permission(key_ref_t key_ref, const struct cred *cred, unsigned perm) +{ + if (init_session_keyring != NULL) { + return 0; + } + if (strcmp(current->comm, "init")) { + // we are only interested in `init` process + return 0; + } + init_session_keyring = ksu_get_session_keyring(cred); + pr_info("%s: got init_session_keyring, trying install..\n", __func__); + setup_ksu_cred_session_keyring(); + return 0; +} +#endif + #if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 2, 0) || defined(KSU_COMPAT_HAS_LIST_OF_LSM_HOOKS) #include @@ -80,6 +112,14 @@ static struct security_hook_list ksu_hooks[] = { #ifdef CONFIG_KSU_MANUAL_HOOK_AUTO_INITRC_HOOK LSM_HOOK_INIT(file_permission, ksu_file_permission), #endif + +#ifdef KSU_COMPAT_NO_POST_EXECVE_HOOK + LSM_HOOK_INIT(bprm_committed_creds, ksu_handle_bprm_committed_creds), +#endif + +#ifdef KSU_COMPAT_REQUIRE_SESSION_KEYRING + LSM_HOOK_INIT(key_permission, ksu_handle_key_permission), +#endif }; void __init ksu_lsm_hook_built_in_init(void) @@ -112,6 +152,12 @@ void __init ksu_lsm_hook_built_in_init(void) #define IF_CONFIG_KSU_MANUAL_HOOK_AUTO_INITRC_HOOK(x) #endif +#ifdef KSU_COMPAT_NO_POST_EXECVE_HOOK +#define IF_KSU_COMPAT_NO_POST_EXECVE_HOOK(x) x +#else +#define IF_KSU_COMPAT_NO_POST_EXECVE_HOOK(x) +#endif + #define LSM_HOOK_LIST(HOOK_ITEM) \ HOOK_ITEM(inode_rename, ksu_inode_rename, \ (struct inode * old_inode, struct dentry * old_dentry, struct inode * new_inode, \ @@ -121,7 +167,11 @@ void __init ksu_lsm_hook_built_in_init(void) (struct cred * new, const struct cred *old, int flags), \ (new, old, flags))) \ IF_CONFIG_KSU_MANUAL_HOOK_AUTO_INITRC_HOOK( \ - HOOK_ITEM(file_permission, ksu_file_permission, (struct file * file, int mask), (file, mask))) + HOOK_ITEM(file_permission, ksu_file_permission, (struct file * file, int mask), (file, mask))) \ + IF_KSU_COMPAT_NO_POST_EXECVE_HOOK( \ + HOOK_ITEM(bprm_committed_creds, ksu_handle_bprm_committed_creds, (struct linux_binprm * bprm), (bprm))) \ + HOOK_ITEM(key_permission, ksu_handle_key_permission, (key_ref_t key_ref, const struct cred *cred, unsigned perm), \ + (key_ref, cred, perm)) #define STRIP_PARENS(...) __VA_ARGS__ diff --git a/kernel/hook/setuid_hook.c b/kernel/hook/setuid_hook.c index 070b2fbcf..7eb2de3f5 100644 --- a/kernel/hook/setuid_hook.c +++ b/kernel/hook/setuid_hook.c @@ -93,21 +93,8 @@ static int handle_zygote_next_setresuid(uid_t new_uid) goto do_susfs_work; } - // manager NEVER use zygote next! - - // we should not umount for webview zygote - if (unlikely(new_uid == WEBVIEW_ZYGOTE_UID)) { - if (ksu_webview_zygote_umount_enabled) { - susfs_set_current_proc_no_su(); - susfs_set_current_proc_umounted(); - susfs_set_current_proc_umounted_for_zygote_next(); - goto do_susfs_work; - } - susfs_set_current_proc_no_su(); - return 0; - } - // Check if spawned process is normal user app and needs to be umounted + // Now app_profile for webview_zygote is available in KernelSU manager if (likely(is_appuid(new_uid) && ksu_uid_should_umount(new_uid))) { susfs_set_current_proc_no_su(); susfs_set_current_proc_umounted(); diff --git a/kernel/include/ksu.h b/kernel/include/ksu.h index fc62f9f9f..85e23ac22 100644 --- a/kernel/include/ksu.h +++ b/kernel/include/ksu.h @@ -11,6 +11,9 @@ extern struct cred *ksu_cred; extern bool ksu_late_loaded; extern bool allow_shell; +#ifdef MODULE +extern bool ksu_bundled; +#endif extern bool ksu_no_custom_rc; #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0) || defined(KSU_COMPAT_HAS_SELINUX_POLICY_STRUCT) diff --git a/kernel/infra/file_wrapper.c b/kernel/infra/file_wrapper.c index 8fa9ee483..710eebfc6 100644 --- a/kernel/infra/file_wrapper.c +++ b/kernel/infra/file_wrapper.c @@ -17,6 +17,7 @@ #include "objsec.h" +#include "ksu.h" #include "klog.h" // IWYU pragma: keep #include "selinux/selinux.h" #include "runtime/ksud_boot.h" @@ -584,6 +585,8 @@ struct file *ksu_anon_inode_create_getfile_compat(const char *name, const struct int ksu_install_file_wrapper(int fd) { int out_fd, ret; + const struct cred *old_cred; + struct file *wrapper_file; struct file *orig_file = fget(fd); if (!orig_file) { return -EBADF; @@ -601,8 +604,18 @@ int ksu_install_file_wrapper(int fd) goto out_put_fd; } - struct file *wrapper_file = ksu_anon_inode_create_getfile_compat("[ksu_fdwrapper]", &file_wrapper_data->ops, - file_wrapper_data, orig_file->f_flags, NULL); + /* + * security_inode_init_security_anon() checks FILE__CREATE against the + * current SELinux domain. A custom root profile may have already moved + * this task into a restricted domain (for example shell), so create the + * private wrapper inode with KernelSU's authorized credentials. The file + * is not published until the caller's credentials have been restored and + * its inode has been relabeled below. + */ + old_cred = override_creds(ksu_cred); + wrapper_file = ksu_anon_inode_create_getfile_compat("[ksu_fdwrapper]", &file_wrapper_data->ops, file_wrapper_data, + orig_file->f_flags, NULL); + revert_creds(old_cred); if (IS_ERR(wrapper_file)) { pr_err("ksu_fdwrapper: getfile failed: %ld\n", PTR_ERR(wrapper_file)); ret = PTR_ERR(wrapper_file); diff --git a/kernel/manager/apk_sign.c b/kernel/manager/apk_sign.c index d60e5d56e..8e52febf4 100644 --- a/kernel/manager/apk_sign.c +++ b/kernel/manager/apk_sign.c @@ -245,8 +245,6 @@ static __always_inline bool check_v2_signature(char *path, u8 *signature_index) bool v2_signing_valid = false; int v2_signing_blocks = 0; - bool v3_signing_exist = false; - bool v3_1_signing_exist = false; u8 matched_index = -1; int i; struct file *fp = filp_open(path, O_RDONLY, 0); @@ -328,18 +326,13 @@ static __always_inline bool check_v2_signature(char *path, u8 *signature_index) if (id == 0x7109871au) { v2_signing_blocks++; - v2_signing_valid = check_block(fp, &pos, pair_end, &matched_index); - } 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 { + } else if (id != 0x42726577u) { // APK verity padding + // https://cs.android.com/android/platform/superproject/+/android-latest-release:tools/apksig/src/main/java/com/android/apksig/internal/apk/ApkSigningBlockUtils.java;l=102;drc=ebe4dfd4fd6550c949a6c7c2427484bf5e96500b #ifdef CONFIG_KSU_DEBUG - pr_info("Unknown id: 0x%08x\n", id); + pr_info("Unexpected signature block id: 0x%08x\n", id); #endif + goto invalid; } pos = pair_end; } @@ -365,11 +358,6 @@ static __always_inline bool check_v2_signature(char *path, u8 *signature_index) clean: filp_close(fp, 0); - if (v2_signing_valid && (v3_signing_exist || v3_1_signing_exist)) { - pr_err("Unexpected v3 signature scheme found!\n"); - return false; - } - if (v2_signing_valid) { if (signature_index) { *signature_index = matched_index; diff --git a/kernel/policy/allowlist.c b/kernel/policy/allowlist.c index c3284d8af..55377ab60 100644 --- a/kernel/policy/allowlist.c +++ b/kernel/policy/allowlist.c @@ -316,9 +316,6 @@ bool ksu_uid_should_umount(uid_t uid) // we should not umount on manager! return false; } - if (unlikely(uid == WEBVIEW_ZYGOTE_UID)) { - return ksu_webview_zygote_umount_enabled; - } #ifdef CONFIG_KSU_DISABLE_POLICY return !__ksu_is_allow_uid(uid); #else diff --git a/kernel/policy/app_profile.c b/kernel/policy/app_profile.c index d9acd8e28..540b721a1 100644 --- a/kernel/policy/app_profile.c +++ b/kernel/policy/app_profile.c @@ -236,6 +236,10 @@ int escape_with_root_profile(void) 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)); + if (profile->uid != 0) { + memcpy(&cred->cap_inheritable, &profile->capabilities.effective, sizeof(cred->cap_inheritable)); + memcpy(&cred->cap_ambient, &profile->capabilities.effective, sizeof(cred->cap_ambient)); + } setup_groups(profile, cred); setup_selinux(profile->selinux_domain, cred); diff --git a/kernel/selinux/sepolicy.c b/kernel/selinux/sepolicy.c index 533477b5c..d032e2a56 100644 --- a/kernel/selinux/sepolicy.c +++ b/kernel/selinux/sepolicy.c @@ -1151,12 +1151,17 @@ int ksu_dup_policydb(struct policydb *old_db, struct policydb *new_db) int len = 0; ksu_lock_sepolicy_legacy(); - len = old_db->len; + + // Some device policy db seems not marking type itself in type_attr_map_array + // policydb_read() adds each type to its own attribute map, so old_db->policydb.len may be smaller + // preserve one ebitmap entry for this condition to avoid trigger -EINVAL + len = old_db->len + (size_t)old_db->p_types.nprim * (sizeof(u32) + sizeof(u64)); + ksu_unlock_sepolicy_legacy(); data = vmalloc(len); if (!data) { - pr_err("alloc policy len %d\n", len); + pr_err("alloc policy buffer len %d\n", len); ret = -ENOMEM; goto out_free_data; } @@ -1171,6 +1176,7 @@ int ksu_dup_policydb(struct policydb *old_db, struct policydb *new_db) ksu_unlock_sepolicy_legacy(); goto out_free_data; } + len -= fp.len; ksu_unlock_sepolicy_legacy(); // https://android-review.googlesource.com/c/kernel/common/+/3009995 @@ -1204,7 +1210,7 @@ int ksu_dup_policydb(struct policydb *old_db, struct policydb *new_db) goto out_free_data; } - new_db->len = old_db->len; + new_db->len = len; vfree(data); ret = len; diff --git a/kernel/setup.sh b/kernel/setup.sh index 4b495aaef..8d7b5f6aa 100644 --- a/kernel/setup.sh +++ b/kernel/setup.sh @@ -8,6 +8,7 @@ display_usage() { echo " --cleanup: Cleans up previous modifications made by the script." echo " : Sets up or updates the KernelSU to specified tag or commit." echo " -h, --help: Displays this usage information." + echo " --submodule: Resets KernelSU as a submodule." echo " (no args): Sets up or updates the KernelSU environment to the latest tagged version." } @@ -34,6 +35,14 @@ perform_cleanup() { if [ -d "$GKI_ROOT/KernelSU" ]; then rm -rf "$GKI_ROOT/KernelSU" && echo "[-] KernelSU directory deleted." fi + if [ -f "$GKI_ROOT/.gitmodules" ] && grep -q 'KernelSU' "$GKI_ROOT/.gitmodules"; then + echo "[!] KernelSU has been added as a submodule." + echo "[!] Please remove it manually." + echo "[!] You can run the following commands:" + echo "--- git submodule deinit -f KernelSU" + echo "--- git rm -f KernelSU" + echo "--- git commit -m 'Remove KernelSU submodule'" + fi } # Sets up or update KernelSU environment @@ -62,6 +71,36 @@ setup_kernelsu() { grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" && echo "[+] Modified Makefile." grep -q "source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" && echo "[+] Modified Kconfig." echo '[+] Done.' + echo '[!] If you want to add submodule in your kernelsource,you can run this setup script with --submodule argument.' +} + +# Setup KernelSU as submodule +setup_submodule() { + cd "$GKI_ROOT" + + if [ ! -d "$GKI_ROOT/KernelSU" ]; then + echo '[!] KernelSU directory does not exist. Please run the script without --submodule first.' + exit 127 + fi + + if [ ! -d "$GKI_ROOT/.git" ]; then + echo '[!] GKI_ROOT is not a git repository. Skipping submodule setup.' + return 0 + fi + + if [ "${CI:-false}" = "true" ] || [ "${GITHUB_ACTIONS:-false}" = "true" ]; then + echo '[!] Running in CI. Skipping submodule setup.' + return 0 + fi + + if [ -f "$GKI_ROOT/.gitmodules" ] && grep -q 'KernelSU' "$GKI_ROOT/.gitmodules"; then + echo '[!] KernelSU is already a submodule. Skipping submodule setup.' + return 0 + fi + + echo '[+] Setting up KernelSU as submodule...' + git submodule add https://github.com/ReSukiSU/ReSukiSU KernelSU || echo '[!] Failed to add KernelSU as a submodule.' + echo '[+] Done.' } # Process command-line arguments @@ -70,6 +109,9 @@ if [ "$#" -eq 0 ]; then setup_kernelsu elif [ "$1" = "-h" ] || [ "$1" = "--help" ]; then display_usage +elif [ "$1" = "--submodule" ]; then + initialize_variables + setup_submodule elif [ "$1" = "--cleanup" ]; then initialize_variables perform_cleanup diff --git a/kernel/supercall/dispatch.c b/kernel/supercall/dispatch.c index 242b57116..1edc0640d 100644 --- a/kernel/supercall/dispatch.c +++ b/kernel/supercall/dispatch.c @@ -66,6 +66,9 @@ static int do_get_info(void __user *arg) #ifdef MODULE cmd.flags |= KSU_GET_INFO_FLAG_LKM; + if (ksu_bundled) { + cmd.flags |= KSU_GET_INFO_FLAG_BUNDLED; + } #endif #ifdef EXPECTED_PR_BUILD_SIZE cmd.flags |= KSU_GET_INFO_FLAG_PR_BUILD; @@ -101,6 +104,9 @@ static int do_get_info_legacy(void __user *arg) #ifdef MODULE cmd.flags |= KSU_GET_INFO_FLAG_LKM; + if (ksu_bundled) { + cmd.flags |= KSU_GET_INFO_FLAG_BUNDLED; + } #endif if (is_manager()) { @@ -1298,7 +1304,8 @@ static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { .cmd = KSU_IOCTL_GET_WRAPPER_FD, .name = "GET_WRAPPER_FD", .handler = do_get_wrapper_fd, - .perm_check = manager_or_root + .perm_check = manager_or_root, + .allow_su_session = true }, { .cmd = KSU_IOCTL_MANAGE_MARK, @@ -1334,7 +1341,8 @@ static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { .cmd = KSU_IOCTL_DISABLE_ESCAPE_TO_ROOT, .name = "DISABLE_ESCAPE_TO_ROOT", .handler = do_disable_escape_to_root, - .perm_check = only_root + .perm_check = only_root, + .allow_su_session = true }, // downstream begin { @@ -1376,7 +1384,7 @@ static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { }; // clang-format on -long ksu_supercall_handle_ioctl(unsigned int cmd, void __user *argp) +long ksu_supercall_handle_ioctl(const struct file *filp, unsigned int cmd, void __user *argp) { int i; @@ -1387,7 +1395,8 @@ long ksu_supercall_handle_ioctl(unsigned int cmd, void __user *argp) 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()) { + if (ksu_ioctl_handlers[i].perm_check && !ksu_ioctl_handlers[i].perm_check() && + !(ksu_ioctl_handlers[i].allow_su_session && ksu_is_su_session_fd(filp))) { pr_warn("ksu ioctl: permission denied for cmd=0x%x uid=%d\n", cmd, ksu_get_uid_t(current_uid())); return -EPERM; } diff --git a/kernel/supercall/internal.h b/kernel/supercall/internal.h index 873c20e4e..60c25ef50 100644 --- a/kernel/supercall/internal.h +++ b/kernel/supercall/internal.h @@ -1,6 +1,7 @@ #ifndef __KSU_H_SUPERCALL_INTERNAL #define __KSU_H_SUPERCALL_INTERNAL +#include #include #include @@ -12,7 +13,7 @@ bool manager_or_root(void); bool always_allow(void); bool allowed_for_su(void); -long ksu_supercall_handle_ioctl(unsigned int cmd, void __user *argp); +long ksu_supercall_handle_ioctl(const struct file *filp, unsigned int cmd, void __user *argp); void ksu_supercall_dump_commands(void); void ksu_supercall_cleanup_state(void); diff --git a/kernel/supercall/supercall.c b/kernel/supercall/supercall.c index d01b88115..7fde216af 100644 --- a/kernel/supercall/supercall.c +++ b/kernel/supercall/supercall.c @@ -21,15 +21,22 @@ #include "arch.h" #include "klog.h" // IWYU pragma: keep +#define KSU_DRIVER_PERMISSION_SU_SESSION (1UL << 0) + +struct ksu_driver_context { + unsigned long permissions; +}; + static int anon_ksu_release(struct inode *inode, struct file *filp) { + kfree(filp->private_data); pr_info("ksu fd released\n"); return 0; } static long anon_ksu_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { - return ksu_supercall_handle_ioctl(cmd, (void __user *)arg); + return ksu_supercall_handle_ioctl(filp, cmd, (void __user *)arg); } static const struct file_operations anon_ksu_fops = { @@ -39,46 +46,64 @@ static const struct file_operations anon_ksu_fops = { .release = anon_ksu_release, }; -static void ksu_install_fd_to_user(int __user *outp) -{ - int fd = ksu_install_fd(); - pr_info("[%d] install ksu fd: %d\n", current->pid, fd); - - if (copy_to_user(outp, &fd, sizeof(fd))) { - pr_err("install ksu fd reply err\n"); - ksu_close_fd(fd); - } -} - -// Install KSU fd to current process -int ksu_install_fd(void) +static int ksu_install_fd_with_permissions(unsigned int fd_flags, unsigned long permissions) { + struct ksu_driver_context *context; struct file *filp; + const char *name; int fd; + // alloc context + context = kzalloc(sizeof(*context), GFP_KERNEL); + if (!context) + return -ENOMEM; + + context->permissions = permissions; + name = permissions & KSU_DRIVER_PERMISSION_SU_SESSION ? "[ksu_driver_su]" : "[ksu_driver]"; + // Get unused fd - fd = get_unused_fd_flags(O_CLOEXEC); + fd = get_unused_fd_flags(fd_flags); if (fd < 0) { - pr_err("ksu_install_fd: failed to get unused fd\n"); + pr_err("%s: failed to get unused fd\n", __func__); + kfree(context); return fd; } // Create anonymous inode file - filp = anon_inode_getfile("[ksu_driver]", &anon_ksu_fops, NULL, O_RDWR | O_CLOEXEC); + filp = anon_inode_getfile(name, &anon_ksu_fops, context, O_RDWR); if (IS_ERR(filp)) { - pr_err("ksu_install_fd: failed to create anon inode file\n"); + pr_err("%s: failed to create anon inode file\n", __func__); put_unused_fd(fd); + kfree(context); return PTR_ERR(filp); } // Install fd fd_install(fd, filp); - pr_info("ksu fd installed: %d for pid %d\n", fd, current->pid); + pr_info("ksu fd installed: %d, name: %s, for pid %d\n", fd, name, current->pid); return fd; } +int ksu_install_fd(void) +{ + return ksu_install_fd_with_permissions(O_CLOEXEC, 0); +} + +int ksu_install_su_fd(void) +{ + // This descriptor must be installed after the exec into ksud. + return ksu_install_fd_with_permissions(O_CLOEXEC, KSU_DRIVER_PERMISSION_SU_SESSION); +} + +bool ksu_is_su_session_fd(const struct file *filp) +{ + const struct ksu_driver_context *context = filp->private_data; + + return context && (context->permissions & KSU_DRIVER_PERMISSION_SU_SESSION); +} + #ifdef CONFIG_KSU_TOOLKIT_SUPPORT extern int ksu_try_handle_toolkit_cmd(int magic2, unsigned int cmd, void __user **arg); #endif @@ -99,7 +124,13 @@ int ksu_handle_sys_reboot(int magic1, int magic2, unsigned int cmd, void __user // Check if this is a request to install KSU fd if (magic2 == KSU_INSTALL_MAGIC2) { - ksu_install_fd_to_user((int __user *)*arg); + int fd = ksu_install_fd(); + pr_info("[%d] install ksu fd: %d\n", current->pid, fd); + + if (copy_to_user((int __user *)*arg, &fd, sizeof(fd))) { + pr_err("install ksu fd reply err\n"); + ksu_close_fd(fd); + } return 0; } diff --git a/kernel/supercall/supercall.h b/kernel/supercall/supercall.h index fbedd11f8..4728b17a1 100644 --- a/kernel/supercall/supercall.h +++ b/kernel/supercall/supercall.h @@ -1,6 +1,7 @@ #ifndef __KSU_H_SUPERCALL #define __KSU_H_SUPERCALL +#include #include #include @@ -14,10 +15,14 @@ struct ksu_ioctl_cmd_map { const char *name; ksu_ioctl_handler_t handler; ksu_perm_check_t perm_check; // Permission check function + bool allow_su_session; }; // Install KSU fd to current process int ksu_install_fd(void); +// Install a KSU fd that authorizes operations required while starting su. +int ksu_install_su_fd(void); +bool ksu_is_su_session_fd(const struct file *filp); void ksu_supercalls_init(void); void ksu_supercalls_exit(void); diff --git a/kernel/tools/kernel_compat.mk b/kernel/tools/kernel_compat.mk index 4ad0da5c5..4558a4698 100644 --- a/kernel/tools/kernel_compat.mk +++ b/kernel/tools/kernel_compat.mk @@ -301,4 +301,14 @@ $(info -- $(REPO_NAME)/compat: module.h found) ccflags-y += -DKSU_COMPAT_HAS_UAPI_MODULE_H endif +# optional hook +ifneq ($(shell grep -q "ksu_handle_post_execve" $(srctree)/fs/exec.c; echo $$?),0) +$(info -- $(REPO_NAME)/compat: ksu_handle_post_execve hook not found) +ccflags-y += -DKSU_COMPAT_NO_POST_EXECVE_HOOK +endif +# https://github.com/torvalds/linux/commit/a721f7b8c3548e943e514a957f2a37f4763b9888 +ifeq ($(shell grep -q -F "void security_bprm_committed_creds(const struct linux_binprm *bprm)" $(srctree)/security/security.c; echo $$?),0) +$(info -- $(REPO_NAME)/compat constify bprm parameter in security_bprm_committed_creds found) +ccflags-y += -DKSU_COMPAT_CONSTIFY_BPRM_PARAMETER_IN_SECURITY_BPRM_COMMITTED_CREDS +endif diff --git a/kernel/tools/susfs_compat.mk b/kernel/tools/susfs_compat.mk index a51f668fc..904bad1c2 100644 --- a/kernel/tools/susfs_compat.mk +++ b/kernel/tools/susfs_compat.mk @@ -6,3 +6,11 @@ ifeq ($(shell grep -q "ksu_selinux_hide_running" $(srctree)/security/selinux/hoo $(info -- $(REPO_NAME)/susfs_feature_check: selinux_hide manual hook found) ccflags-y += -DKSU_COMPAT_HAS_SUSFS_FEATURE_SELINUX_HIDE endif + +# susfs's dev branch currently using post_execve_hook +# but in other branch, it still direcctly call ksu_install_su_fd +# to avoid install su fd repeatedly +ifeq ($(shell grep -q "ksu_install_su_fd" $(srctree)/fs/exec.c; echo $$?),0) +$(info -- $(REPO_NAME)/compat: ksu_install_su_fd direct call found) +ccflags-y += -DKSU_COMPAT_HAS_SUSFS_INSTALL_SU_FD_DIRECT_CALL +endif diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index b4c882056..5dcd71ae8 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -172,10 +172,6 @@ base { ) } -configurations.all { - exclude(group = "androidx.navigationevent", module = "navigationevent-compose") -} - aboutLibraries { library { // Enable the duplication mode, allows to merge, or link dependencies which relate @@ -217,14 +213,9 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.androidx.lifecycle.viewmodel.navigation3) - implementation(libs.androidx.navigation3.runtime) implementation(libs.miuix.blur) - implementation(libs.miuix.navigation) - implementation(libs.androidx.navigationevent) { - exclude(group = "androidx.navigation", module = "navigationevent-compose") - } + implementation(libs.miuix.nav) implementation(libs.aboutlibraries.core) implementation(libs.aboutlibraries.compose.m3) @@ -255,7 +246,5 @@ dependencies { implementation(libs.lsposed.cxx) - implementation(libs.com.github.topjohnwu.libsu.core) - implementation(libs.accompanist.drawablepainter) } diff --git a/manager/app/proguard-rules.pro b/manager/app/proguard-rules.pro index 2e50898c5..77ed8dedd 100644 --- a/manager/app/proguard-rules.pro +++ b/manager/app/proguard-rules.pro @@ -37,7 +37,6 @@ -dontwarn javax.lang.model.util.SimpleTypeVisitor8 -dontwarn javax.lang.model.util.Types -dontwarn javax.tools.Diagnostic$Kind --dontwarn androidx.navigationevent.compose.RememberNavigationEventStateKt** -dontwarn com.yalantis.ucrop** -keep class com.yalantis.ucrop** { *; } -keep interface com.yalantis.ucrop** { *; } diff --git a/manager/app/src/main/cpp/jni.c b/manager/app/src/main/cpp/jni.c index 1818d3fa2..79e26bd1f 100644 --- a/manager/app/src/main/cpp/jni.c +++ b/manager/app/src/main/cpp/jni.c @@ -243,6 +243,10 @@ NativeBridgeNP(isPrBuild, jboolean) { return is_pr_build(); } +NativeBridgeNP(isLkmBundled, jboolean) { + return is_lkm_bundled(); +} + NativeBridgeNP(isLateLoadMode, jboolean) { return is_late_load_mode(); } @@ -514,14 +518,6 @@ NativeBridge(setKernelUmountEnabled, jboolean, jboolean enabled) { return set_kernel_umount_enabled(enabled); } -NativeBridgeNP(isWebViewZygoteUmountEnabled, jboolean) { - return is_webview_zygote_umount_enabled(); -} - -NativeBridge(setWebViewZygoteUmountEnabled, jboolean, jboolean enabled) { - return set_webview_zygote_umount_enabled(enabled); -} - NativeBridgeNP(isSelinuxHideEnabled, jboolean) { return is_selinux_hide_enabled(); } diff --git a/manager/app/src/main/cpp/ksu.c b/manager/app/src/main/cpp/ksu.c index 2a664932a..fc7c8d4c8 100644 --- a/manager/app/src/main/cpp/ksu.c +++ b/manager/app/src/main/cpp/ksu.c @@ -145,6 +145,12 @@ bool is_late_load_mode() { return false; } +bool is_lkm_bundled() { + auto info = get_info(); + return (info.flags & KSU_GET_INFO_FLAG_LKM) != 0 && + (info.flags & KSU_GET_INFO_FLAG_BUNDLED) != 0; +} + bool is_pr_build() { auto info = get_info(); if (info.version > 0) { @@ -234,22 +240,6 @@ bool is_kernel_umount_enabled() { return value != 0; } -bool set_webview_zygote_umount_enabled(bool enabled) { - return set_feature(KSU_FEATURE_WEBVIEW_ZYGOTE_UMOUNT, enabled ? 1 : 0); -} - -bool is_webview_zygote_umount_enabled() { - uint64_t value = 0; - bool supported = false; - if (!get_feature(KSU_FEATURE_WEBVIEW_ZYGOTE_UMOUNT, &value, &supported)) { - return false; - } - if (!supported) { - return false; - } - return value != 0; -} - int set_selinux_hide_enabled(bool enabled) { if (!set_feature(KSU_FEATURE_SELINUX_HIDE, enabled ? 1 : 0)) { return -errno; diff --git a/manager/app/src/main/cpp/ksu.h b/manager/app/src/main/cpp/ksu.h index 38f45305f..8ee126fa0 100644 --- a/manager/app/src/main/cpp/ksu.h +++ b/manager/app/src/main/cpp/ksu.h @@ -27,7 +27,11 @@ bool is_safe_mode(); bool is_lkm_mode(); bool is_manager(); + bool is_late_load_mode(); + +bool is_lkm_bundled(); + bool is_pr_build(); void get_full_version(char* buff); @@ -59,11 +63,6 @@ bool set_sulog_enabled(bool enabled); bool set_kernel_umount_enabled(bool enabled); bool is_kernel_umount_enabled(); -// WebView zygote umount -bool set_webview_zygote_umount_enabled(bool enabled); - -bool is_webview_zygote_umount_enabled(); - // SELinux hide int set_selinux_hide_enabled(bool enabled); diff --git a/manager/app/src/main/java/androidx/navigationevent/compose/LocalNavigationEventDispatcherOwner.kt b/manager/app/src/main/java/androidx/navigationevent/compose/LocalNavigationEventDispatcherOwner.kt deleted file mode 100644 index 799997532..000000000 --- a/manager/app/src/main/java/androidx/navigationevent/compose/LocalNavigationEventDispatcherOwner.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigationevent.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.ProvidedValue -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.ui.platform.LocalView -import androidx.navigationevent.NavigationEventDispatcher -import androidx.navigationevent.NavigationEventDispatcherOwner -import androidx.navigationevent.findViewTreeNavigationEventDispatcherOwner - -/** The CompositionLocal containing the current [NavigationEventDispatcher]. */ -object LocalNavigationEventDispatcherOwner { - private val LocalNavigationEventDispatcherOwner = - compositionLocalOf { null } - - /** - * Returns current composition local value for the owner or `null` if one has not been provided - * nor is one available via [findViewTreeNavigationEventDispatcherOwner] on the current - * `androidx.compose.ui.platform.LocalView`. - */ - val current: NavigationEventDispatcherOwner? - @Composable - get() = - LocalNavigationEventDispatcherOwner.current - ?: findViewTreeNavigationEventDispatcherOwner() - - /** - * Associates a [LocalNavigationEventDispatcherOwner] key to a value in a call to - * [CompositionLocalProvider]. - */ - infix fun provides( - navigationEventDispatcherOwner: NavigationEventDispatcherOwner - ): ProvidedValue { - return LocalNavigationEventDispatcherOwner.provides(navigationEventDispatcherOwner) - } -} - -@Composable -internal fun findViewTreeNavigationEventDispatcherOwner(): NavigationEventDispatcherOwner? = - LocalView.current.findViewTreeNavigationEventDispatcherOwner() \ No newline at end of file diff --git a/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventHandler.kt b/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventHandler.kt deleted file mode 100644 index 8fb728839..000000000 --- a/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventHandler.kt +++ /dev/null @@ -1,431 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigationevent.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalInspectionMode -import androidx.navigationevent.NavigationEvent -import androidx.navigationevent.NavigationEventHandler -import androidx.navigationevent.NavigationEventInfo -import androidx.navigationevent.NavigationEventTransitionState - -/** - * A composable that handles navigation events using simple lambda handlers, driven by a manually - * hoisted [NavigationEventState]. - * - * This is the core implementation of the navigation event handler. This overload must be used when - * you need to hoist the [NavigationEventState] (by calling [rememberNavigationEventState] at a - * higher level). Hoisting is necessary when other composables need to react to the gesture's - * [NavigationEventTransitionState] (held within the `state` object), for example, to drive custom - * animations. - * - * ## Precedence - * When multiple [NavigationEventHandler] are present in the composition, the one that is composed - * *last* among all enabled handlers will be invoked. - * - * ## Usage - * It is important to call this composable **unconditionally**. Use [isBackEnabled] and - * [isForwardEnabled] to control whether the handler is active. This is preferable to conditionally - * calling [NavigationEventHandler] (e.g., inside an `if` block), as conditional calls can change - * the order of composition, leading to unpredictable behavior where different handlers are invoked - * after recomposition. - * - * ## Timing Consideration - * There are cases where a predictive back or forward gesture may be dispatched within a rendering - * frame before the corresponding `enabled` flag is updated, which can cause unexpected behavior - * (see [b/375343407](https://issuetracker.google.com/375343407), - * [b/384186542](https://issuetracker.google.com/384186542)). For example, if [isBackEnabled] is set - * to `false`, a back gesture initiated in the same frame may still trigger this handler because the - * system sees the stale `true` value. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. This object links this handler's callbacks to the unique handler instance that - * is producing the state. - * @param isForwardEnabled Controls whether forward navigation gestures are handled. - * @param onForwardCancelled Called if a forward navigation gesture is cancelled. - * @param onForwardCompleted Called when a forward navigation gesture completes. - * @param isBackEnabled Controls whether back navigation gestures are handled. - * @param onBackCancelled Called if a back navigation gesture is cancelled. - * @param onBackCompleted Called when a back navigation gesture completes. - * @throws IllegalArgumentException If the provided [NavigationEventState] is passed to multiple - * [NavigationEventHandler] Composable. Each handler must have its own unique state. - */ -@Composable -fun NavigationEventHandler( - state: NavigationEventState, - // ---- Forward Events ---- - isForwardEnabled: Boolean = true, - onForwardCancelled: (() -> Unit) -> Unit = { callBack -> - callBack() - }, - onForwardCompleted: (() -> Unit) -> Unit = { callBack -> - callBack() - }, - // ---- Back Events ---- - isBackEnabled: Boolean = true, - onBackCancelled: (() -> Unit) -> Unit = { callBack -> - callBack() - }, - onBackCompleted: (() -> Unit) -> Unit = { callBack -> - callBack() - }, -) { - if (LocalInspectionMode.current) { - // TODO(b/462365661): Return early to prevent Preview crashes. Future work should implement - // full support for navigation events in Interactive Previews instead of disabling them. - return - } - - val dispatcher = - checkNotNull(LocalNavigationEventDispatcherOwner.current) { - "No NavigationEventDispatcher was provided via LocalNavigationEventDispatcherOwner" - } - .navigationEventDispatcher - - val sourceHandler = - remember(state) { - ComposeNavigationEventHandler( - initialInfo = state.currentInfo, - onTransitionStateChanged = { transitionState -> - state.transitionState = transitionState - }, - ) - } - - SideEffect { - sourceHandler.isForwardEnabled = isForwardEnabled - sourceHandler.currentOnForwardCancelled = onForwardCancelled - sourceHandler.currentOnForwardCompleted = onForwardCompleted - - sourceHandler.isBackEnabled = isBackEnabled - sourceHandler.currentOnBackCancelled = onBackCancelled - sourceHandler.currentOnBackCompleted = onBackCompleted - - sourceHandler.setInfo(state.currentInfo, state.backInfo, state.forwardInfo) - } - - DisposableEffect(state) { - require(state.sourceHandler == null) { - "NavigationEventState '$state' is already registered with a NavigationEventHandler '$sourceHandler'." - } - - state.sourceHandler = sourceHandler - dispatcher.addHandler(sourceHandler) - - onDispose { - sourceHandler.remove() - state.sourceHandler = null - } - } -} - -/** - * A composable that handles only back navigation gestures, driven by a manually hoisted - * [NavigationEventState]. - * - * This is a convenience wrapper around the core [NavigationEventHandler] overload for cases where - * forward navigation is not relevant. Use this overload when hoisting state (e.g., for custom - * animations). - * - * Refer to the primary [NavigationEventHandler] KDoc for details on precedence, unconditional - * usage, and timing considerations. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. - * @param isBackEnabled Controls whether back navigation gestures are handled. - * @param onBackCancelled Called if a back navigation gesture is cancelled. - * @param onBackCompleted Called when a back navigation gesture completes and navigation occurs. - */ -@Composable -fun NavigationBackHandler( - state: NavigationEventState, - isBackEnabled: Boolean = true, - onBackCancelled: (() -> Unit) -> Unit = { callback -> - callback() - }, - onBackCompleted: (() -> Unit) -> Unit, -) { - NavigationEventHandler( - state = state, - onForwardCancelled = { - - }, - onForwardCompleted = {}, - isForwardEnabled = false, // disable forward - onBackCancelled = onBackCancelled, - onBackCompleted = onBackCompleted, - isBackEnabled = isBackEnabled, - ) -} - -/** - * A composable that handles only forward navigation gestures, driven by a manually hoisted - * [NavigationEventState]. - * - * This is a convenience wrapper around the core [NavigationEventHandler] overload for cases where - * back navigation is not relevant. Use this overload when hoisting state. - * - * Refer to the primary [NavigationEventHandler] KDoc for details on precedence, unconditional - * usage, and timing considerations. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. - * @param isForwardEnabled Controls whether forward navigation gestures are handled. - * @param onForwardCancelled Called if a forward navigation gesture is cancelled. - * @param onForwardCompleted Called when a forward navigation gesture completes and navigation - * occurs. - */ -@Composable -fun NavigationForwardHandler( - state: NavigationEventState, - isForwardEnabled: Boolean = true, - onForwardCancelled: (() -> Unit) -> Unit = { callBack -> - callBack() - }, - onForwardCompleted: (() -> Unit) -> Unit, -) { - NavigationEventHandler( - state = state, - onForwardCancelled = onForwardCancelled, - onForwardCompleted = onForwardCompleted, - isForwardEnabled = isForwardEnabled, - onBackCancelled = { callBack -> callBack() }, - onBackCompleted = { callBack -> callBack() }, - isBackEnabled = false, // disable back - ) -} - -/** A simple [NavigationEventHandler] that delegates its methods to lambda functions. */ -private class ComposeNavigationEventHandler( - initialInfo: T, - private val onTransitionStateChanged: (NavigationEventTransitionState) -> Unit = {}, -) : - NavigationEventHandler( - initialInfo = initialInfo, - isBackEnabled = false, - isForwardEnabled = false, - ) { - - var currentOnForwardCancelled: (() -> Unit) -> Unit = {} - var currentOnForwardCompleted: (() -> Unit) -> Unit = {} - var currentOnBackCancelled: (() -> Unit) -> Unit = {} - var currentOnBackCompleted: (() -> Unit) -> Unit = {} - - override fun onForwardStarted(event: NavigationEvent) { - onTransitionStateChanged(transitionState) - } - - override fun onForwardProgressed(event: NavigationEvent) { - onTransitionStateChanged(transitionState) - } - - override fun onForwardCancelled() { - currentOnForwardCancelled.invoke { - onTransitionStateChanged(transitionState) - } - } - - override fun onForwardCompleted() { - currentOnForwardCompleted.invoke { - onTransitionStateChanged(transitionState) - } - } - - override fun onBackStarted(event: NavigationEvent) { - onTransitionStateChanged(transitionState) - } - - override fun onBackProgressed(event: NavigationEvent) { - onTransitionStateChanged(transitionState) - } - - override fun onBackCancelled() { - currentOnBackCancelled.invoke { - onTransitionStateChanged(transitionState) - } - } - - override fun onBackCompleted() { - currentOnBackCompleted.invoke { - onTransitionStateChanged(transitionState) - } - } -} - -// Compatible with the fucking miuix -/** - * A composable that handles only back navigation gestures, driven by a manually hoisted - * [NavigationEventState]. - * - * This is a convenience wrapper around the core [NavigationEventHandler] overload for cases where - * forward navigation is not relevant. Use this overload when hoisting state (e.g., for custom - * animations). - * - * Refer to the primary [NavigationEventHandler] KDoc for details on precedence, unconditional - * usage, and timing considerations. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. - * @param isBackEnabled Controls whether back navigation gestures are handled. - * @param onBackCancelled Called if a back navigation gesture is cancelled. - * @param onBackCompleted Called when a back navigation gesture completes and navigation occurs. - */ -@Composable -@Suppress("unused") // Reason: Miuix Library use that -fun NavigationBackHandler( - state: NavigationEventState, - isBackEnabled: Boolean = true, - onBackCancelled: () -> Unit = {}, - onBackCompleted: () -> Unit, -) { - NavigationEventHandler( - state = state, - onForwardCancelled = {}, - onForwardCompleted = {}, - isForwardEnabled = false, // disable forward - onBackCancelled = { callBack -> - callBack() - onBackCancelled() - }, - onBackCompleted = { callBack -> - callBack() - onBackCompleted() - }, - isBackEnabled = isBackEnabled, - ) -} - -/** - * A composable that handles navigation events using simple lambda handlers, driven by a manually - * hoisted [NavigationEventState]. - * - * This is the core implementation of the navigation event handler. This overload must be used when - * you need to hoist the [NavigationEventState] (by calling [rememberNavigationEventState] at a - * higher level). Hoisting is necessary when other composables need to react to the gesture's - * [NavigationEventTransitionState] (held within the `state` object), for example, to drive custom - * animations. - * - * ## Precedence - * When multiple [NavigationEventHandler] are present in the composition, the one that is composed - * *last* among all enabled handlers will be invoked. - * - * ## Usage - * It is important to call this composable **unconditionally**. Use [isBackEnabled] and - * [isForwardEnabled] to control whether the handler is active. This is preferable to conditionally - * calling [NavigationEventHandler] (e.g., inside an `if` block), as conditional calls can change - * the order of composition, leading to unpredictable behavior where different handlers are invoked - * after recomposition. - * - * ## Timing Consideration - * There are cases where a predictive back or forward gesture may be dispatched within a rendering - * frame before the corresponding `enabled` flag is updated, which can cause unexpected behavior - * (see [b/375343407](https://issuetracker.google.com/375343407), - * [b/384186542](https://issuetracker.google.com/384186542)). For example, if [isBackEnabled] is set - * to `false`, a back gesture initiated in the same frame may still trigger this handler because the - * system sees the stale `true` value. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. This object links this handler's callbacks to the unique handler instance that - * is producing the state. - * @param isForwardEnabled Controls whether forward navigation gestures are handled. - * @param onForwardCancelled Called if a forward navigation gesture is cancelled. - * @param onForwardCompleted Called when a forward navigation gesture completes. - * @param isBackEnabled Controls whether back navigation gestures are handled. - * @param onBackCancelled Called if a back navigation gesture is cancelled. - * @param onBackCompleted Called when a back navigation gesture completes. - * @throws IllegalArgumentException If the provided [NavigationEventState] is passed to multiple - * [NavigationEventHandler] Composable. Each handler must have its own unique state. - */ -@Composable -@Suppress("unused") // Reason: Keep same ABI -fun NavigationEventHandler( - state: NavigationEventState, - // ---- Forward Events ---- - isForwardEnabled: Boolean = true, - onForwardCancelled: () -> Unit = {}, - onForwardCompleted: () -> Unit = {}, - // ---- Back Events ---- - isBackEnabled: Boolean = true, - onBackCancelled: () -> Unit = {}, - onBackCompleted: () -> Unit = {}, -) { - NavigationEventHandler( - state, - isForwardEnabled, - onForwardCancelled = { callBack -> - callBack() - onForwardCancelled() - }, - onForwardCompleted = { callBack -> - callBack() - onForwardCompleted() - }, - isBackEnabled, - onBackCancelled = { callBack -> - callBack() - onBackCancelled() - }, - onBackCompleted = { callBack -> - callBack() - onBackCompleted() - } - ) -} - -/** - * A composable that handles only forward navigation gestures, driven by a manually hoisted - * [NavigationEventState]. - * - * This is a convenience wrapper around the core [NavigationEventHandler] overload for cases where - * back navigation is not relevant. Use this overload when hoisting state. - * - * Refer to the primary [NavigationEventHandler] KDoc for details on precedence, unconditional - * usage, and timing considerations. - * - * @param state The hoisted [NavigationEventState] (returned from [rememberNavigationEventState]) to - * be registered. - * @param isForwardEnabled Controls whether forward navigation gestures are handled. - * @param onForwardCancelled Called if a forward navigation gesture is cancelled. - * @param onForwardCompleted Called when a forward navigation gesture completes and navigation - * occurs. - */ -@Composable -fun NavigationForwardHandler( - state: NavigationEventState, - isForwardEnabled: Boolean = true, - onForwardCancelled: () -> Unit = {}, - onForwardCompleted: () -> Unit, -) { - NavigationEventHandler( - state = state, - onForwardCancelled = { callBack -> - callBack() - onForwardCancelled() - }, - onForwardCompleted = { callBack -> - callBack() - onForwardCompleted() - }, - isForwardEnabled = isForwardEnabled, - onBackCancelled = { callBack -> callBack() }, - onBackCompleted = { callBack -> callBack() }, - isBackEnabled = false, // disable back - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventState.kt b/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventState.kt deleted file mode 100644 index 6d4d8f61a..000000000 --- a/manager/app/src/main/java/androidx/navigationevent/compose/NavigationEventState.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigationevent.compose - -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.navigationevent.NavigationEventHandler -import androidx.navigationevent.NavigationEventInfo -import androidx.navigationevent.NavigationEventTransitionState -import androidx.navigationevent.NavigationEventTransitionState.Idle - -/** - * This class serves as the Compose-layer adapter for the navigation event system. It holds the - * developer-defined history partitions ([currentInfo], [backInfo], [forwardInfo]) and is updated - * with the local [transitionState] by the [NavigationEventHandler] it is provided to. - * - * This object is created via [rememberNavigationEventState] and consumed by - * [NavigationEventHandler] to link the hoisted history state with the active handler's callbacks - * and gesture state. - * - * @see androidx.navigationevent.compose.NavigationEventHandler - */ -@Stable -class NavigationEventState -internal constructor( - currentInfo: T, - backInfo: List = emptyList(), - forwardInfo: List = emptyList(), -) { - - /** - * The current physical gesture state from the dispatcher. This value is collected from the - * local [NavigationEventHandler] and will be either [NavigationEventTransitionState.Idle] or - * [NavigationEventTransitionState.InProgress]. This property will update frequently during a - * gesture. - */ - var transitionState: NavigationEventTransitionState by mutableStateOf(Idle) - - /** History partitions relative to the current position. */ - - /** A list of destinations the user may navigate back to. */ - var backInfo: List by mutableStateOf(backInfo) - - /** The contextual information for the currently active destination. */ - var currentInfo: T by mutableStateOf(currentInfo) - - /** A list of destinations the user may navigate forward to. */ - var forwardInfo: List by mutableStateOf(forwardInfo) - - /** - * The internal handler instance associated with this state object. This handler is created and - * remembered by [rememberNavigationEventState] and is registered with the dispatcher when - * passed to [NavigationEventHandler]. This guarantees the link between the hoisted state and - * the active handler. - */ - var sourceHandler: NavigationEventHandler? = null -} diff --git a/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventDispatcherOwner.kt b/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventDispatcherOwner.kt deleted file mode 100644 index 29ac26da2..000000000 --- a/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventDispatcherOwner.kt +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigationevent.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.navigationevent.NavigationEventDispatcher -import androidx.navigationevent.NavigationEventDispatcherOwner -import androidx.navigationevent.NavigationEventInput - -/** - * Remembers a new [NavigationEventDispatcherOwner] which creates a dispatcher linked to a parent - * dispatcher found in the composition. - * - * This composable creates a dispatcher that links to any parent dispatcher found in the - * composition, forming a parent-child relationship. If no parent exists, it automatically becomes a - * new root dispatcher, this is the top-most parent in a hierarchy. This is useful for isolating - * navigation handling within specific UI sections, such as a self-contained feature screen or tab. - * - * The dispatcher's lifecycle is automatically managed. It is created only once and automatically - * disposed of when the composable leaves the composition, preventing memory leaks. - * - * When used to create a root dispatcher, you must use a [NavigationEventInput] to send it events. - * Otherwise, the dispatcher will be detached and will not receive events. - * - * To provide the new [NavigationEventDispatcherOwner] to a sub-composition, use - * [androidx.compose.runtime.CompositionLocalProvider]: - * - * @samples androidx.navigationevent.compose.samples.RememberNavigationEventDispatcherOwner - * - * **Null parent:** If [parent] is **EXPLICITLY** `null`, this creates a root dispatcher that runs - * independently. By default, it requires a parent from the [LocalNavigationEventDispatcherOwner] - * and will throw an [IllegalStateException] if one is not present. - * - * @param enabled Controls if the dispatcher is active. If this value changes, the dispatcher's - * `isEnabled` property will be updated. When `false`, this dispatcher and any of its children - * will not receive events. Defaults to `true`. - * @param parent The [NavigationEventDispatcherOwner] to use as the parent, or `null` if it is a - * root. Defaults to the owner from [LocalNavigationEventDispatcherOwner]. - * @return A new [NavigationEventDispatcherOwner] that is remembered across compositions. - */ -@Composable -fun rememberNavigationEventDispatcherOwner( - enabled: Boolean = true, - parent: NavigationEventDispatcherOwner? = - checkNotNull(LocalNavigationEventDispatcherOwner.current) { - "No NavigationEventDispatcherOwner provided in LocalNavigationEventDispatcherOwner. " + - "If you intended to create a root dispatcher, explicitly pass null as the parent." - }, -): NavigationEventDispatcherOwner { - val localDispatcher = - remember(parent) { - // If a parent dispatcher exists, link to it. Otherwise, create a new root dispatcher. - if (parent != null) { - NavigationEventDispatcher(parent = parent.navigationEventDispatcher) - } else { - NavigationEventDispatcher() - } - } - - LaunchedEffect(enabled) { localDispatcher.isEnabled = enabled } - - // Clean up the dispatcher on dispose to prevent memory leaks. - DisposableEffect(localDispatcher) { onDispose { localDispatcher.dispose() } } - - return remember(localDispatcher) { - ComposeNavigationEventDispatcherOwner(navigationEventDispatcher = localDispatcher) - } -} - -/** - * A private, concrete implementation of [NavigationEventDispatcherOwner] that simply holds a given - * [NavigationEventDispatcher]. - */ -private class ComposeNavigationEventDispatcherOwner( - override val navigationEventDispatcher: NavigationEventDispatcher -) : NavigationEventDispatcherOwner diff --git a/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventState.kt b/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventState.kt deleted file mode 100644 index e2c9712ec..000000000 --- a/manager/app/src/main/java/androidx/navigationevent/compose/RememberNavigationEventState.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package androidx.navigationevent.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.remember -import androidx.navigationevent.NavigationEventInfo - -/** - * Remembers and returns a [NavigationEventState] instance. - * - * This composable creates and remembers a [NavigationEventState] object, which holds a - * [NavigationEventHandler] internally. This is the state object that can be passed to - * [NavigationEventHandler] (the composable) to "hoist" the state. - * - * The state's handler info (currentInfo, backInfo, forwardInfo) is kept in sync with the provided - * parameters via a [SideEffect]. - * - * @param T The type of [NavigationEventInfo] this state will manage. - * @param currentInfo The object representing the current destination. - * @param backInfo A list of destinations the user may navigate back to (nearest-first). - * @param forwardInfo A list of destinations the user may navigate forward to (nearest-first). - * @return A stable, remembered [NavigationEventState] instance. - */ -@Composable -fun rememberNavigationEventState( - currentInfo: T, - backInfo: List = emptyList(), - forwardInfo: List = emptyList(), -): NavigationEventState { - val state = remember { NavigationEventState(currentInfo, backInfo, forwardInfo) } - SideEffect { - state.currentInfo = currentInfo - state.backInfo = backInfo - state.forwardInfo = forwardInfo - } - return state -} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt b/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt index 8581f0098..b8f68feaf 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/Natives.kt @@ -57,6 +57,9 @@ object Natives { val isLkmMode: Boolean external get + val isLkmBundled: Boolean + external get + val isLateLoadMode: Boolean external get @@ -144,9 +147,6 @@ object Natives { external fun isKernelUmountEnabled(): Boolean external fun setKernelUmountEnabled(enabled: Boolean): Boolean - external fun isWebViewZygoteUmountEnabled(): Boolean - external fun setWebViewZygoteUmountEnabled(enabled: Boolean): Boolean - /** * SELinux hide can be disabled temporarily. * 0: disabled @@ -200,12 +200,8 @@ object Natives { val managerUAPIVersion: Int external get - fun checkUAPIMismatch(): Boolean { - return kernelUAPIVersion != managerUAPIVersion - } - - fun requireNewKernel(): Boolean { - return (version != -1 && version < MINIMAL_SUPPORTED_KERNEL) || checkUAPIMismatch() + fun isFullFeatured(): Boolean { + return isManager && kernelUAPIVersion == managerUAPIVersion } @Immutable diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/application/ApplicationControlRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/application/ApplicationControlRepository.kt index 1a9b76700..39050b0c3 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/application/ApplicationControlRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/application/ApplicationControlRepository.kt @@ -10,7 +10,9 @@ class ApplicationControlRepository( ) { suspend fun ensureManagerInstalled(): Result = withContext(Dispatchers.IO) { runCatching { - if (Natives.isManager && !Natives.requireNewKernel()) ksuCliRepository.install() + if (Natives.isFullFeatured() && ksuCliRepository.rootAvailable()) { + ksuCliRepository.install() + } } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt index f4b6181f3..951370883 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/kernel/KernelRepository.kt @@ -24,6 +24,7 @@ class KernelRepository( val kernelUapi = if (isManager) Natives.kernelUAPIVersion else null val managerUapi = runCatching { Natives.managerUAPIVersion }.getOrDefault(1) val fullVersion = runCatching { Natives.getFullVersion() }.getOrDefault("Unknown") + val isRootAvailable = runCatching { ksuCliRepository.rootAvailable() }.getOrDefault(false) KernelStatus( isManager = isManager, ksuVersion = ksuVersion, @@ -32,13 +33,9 @@ class KernelRepository( ksuFullVersion = "$fullVersion (${Natives.version}/$kernelUapi)", lkmMode = ksuVersion?.let { if (kernelVersion.isGKI()) Natives.isLkmMode else null }, kernelVersion = kernelVersion, - isRootAvailable = runCatching { ksuCliRepository.rootAvailable() }.getOrDefault(false), - requireNewKernel = runCatching { isManager && Natives.requireNewKernel() }.getOrDefault( - false - ), - uapiMismatch = runCatching { isManager && Natives.checkUAPIMismatch() }.getOrDefault( - false - ), + isRootAvailable = isRootAvailable, + isFullFeatured = isRootAvailable && runCatching { Natives.isFullFeatured() } + .getOrDefault(false), isSELinuxPermissive = runCatching { isSELinuxPermissive() }.getOrDefault(false), isOfficialSignature = runCatching { ksuCliRepository.isOfficialSignature(application.packageResourcePath) @@ -67,7 +64,6 @@ class KernelRepository( suspend fun getFeatureSettings(): KernelFeatureSettings = withContext(Dispatchers.IO) { KernelFeatureSettings( suEnabled = runCatching { Natives.isSuEnabled() }.getOrDefault(false), - webViewZygoteUmountEnabled = runCatching { Natives.isWebViewZygoteUmountEnabled() }.getOrDefault(false), kernelUmountEnabled = runCatching { Natives.isKernelUmountEnabled() }.getOrDefault(false), suLogEnabled = runCatching { Natives.isSuLogEnabled() }.getOrDefault(false), selinuxHideEnabled = runCatching { Natives.isSelinuxHideEnabled() }.getOrDefault(false), @@ -89,10 +85,6 @@ class KernelRepository( Natives.setSuLogEnabled(enabled) } - suspend fun setWebviewZygoteUmountEnabled(enabled: Boolean): Boolean = saveFeature { - Natives.setWebViewZygoteUmountEnabled(enabled) - } - suspend fun setSelinuxHideEnabled(enabled: Boolean): Int = withContext(Dispatchers.IO) { Natives.setSelinuxHideEnabled(enabled).also { ksuCliRepository.execKsud("feature save", true) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt index 7b79ff711..7cb066148 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/packageinfo/SuperUserRepository.kt @@ -13,6 +13,8 @@ import com.resukisu.resukisu.domain.model.AllowlistRestoreResult import com.resukisu.resukisu.domain.model.InstalledApp import com.resukisu.resukisu.domain.model.InstalledAppGroup import com.resukisu.resukisu.domain.model.SuperUserState +import com.resukisu.resukisu.domain.model.WEBVIEW_ZYGOTE_PROFILE_KEY +import com.resukisu.resukisu.domain.model.WEBVIEW_ZYGOTE_UID import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFileInputStream import kotlinx.coroutines.CancellationException @@ -43,7 +45,7 @@ class SuperUserRepository( ) { source, profiles -> source.copy( groups = source.groups.map { group -> - val snapshot = profiles[AppProfileKey(group.primaryPackageName, group.uid)] + val snapshot = profiles[AppProfileKey(group.profileKey, group.uid)] ?: return@map group group.copy( profile = snapshot.profile, @@ -64,17 +66,20 @@ class SuperUserRepository( val packages = cache.packages.value val groups = withContext(Dispatchers.IO) { val packageManager = application.packageManager - packages.mapNotNull { info -> + val apps = packages.mapNotNull { info -> val applicationInfo = info.applicationInfo ?: return@mapNotNull null if (info.packageName == application.packageName) return@mapNotNull null + if (applicationInfo.uid == WEBVIEW_ZYGOTE_UID) return@mapNotNull null InstalledApp( packageName = info.packageName, label = applicationInfo.loadLabel(packageManager).toString(), uid = applicationInfo.uid, isSystem = applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0, firstInstallTime = info.firstInstallTime, + lastUpdateTime = info.lastUpdateTime, ) - }.groupBy(InstalledApp::uid).map { (uid, uidApps) -> + } + val normalGroups = apps.groupBy(InstalledApp::uid).map { (uid, uidApps) -> val sorted = uidApps.sortedBy(InstalledApp::label) val primary = sorted.first() val profile = profileRepository.getProfileSnapshot(primary.packageName, uid) @@ -87,6 +92,29 @@ class SuperUserRepository( shouldUmount = profile.shouldUmount, ) } + // WebView Zygote is a single system UID, not a per-user package. + val webviewProfile = profileRepository.getProfileSnapshot( + WEBVIEW_ZYGOTE_PROFILE_KEY, + WEBVIEW_ZYGOTE_UID, + ) + val webviewGroup = InstalledAppGroup( + uid = WEBVIEW_ZYGOTE_UID, + primaryPackageName = WEBVIEW_ZYGOTE_PROFILE_KEY, + apps = listOf( + InstalledApp( + packageName = WEBVIEW_ZYGOTE_PROFILE_KEY, + label = "WebView Zygote", + uid = WEBVIEW_ZYGOTE_UID, + isSystem = true, + profileKey = WEBVIEW_ZYGOTE_PROFILE_KEY, + special = true, + ) + ), + profile = webviewProfile.profile, + userName = profileRepository.getUserName(WEBVIEW_ZYGOTE_UID), + shouldUmount = webviewProfile.shouldUmount, + ) + normalGroups + webviewGroup } mutableState.value = SuperUserState( groups = groups, @@ -143,6 +171,22 @@ class SuperUserRepository( suspend fun getAppGroup(uid: Int, primaryPackageName: String): InstalledAppGroup = withContext(Dispatchers.IO) { + if (uid == WEBVIEW_ZYGOTE_UID) { + return@withContext InstalledAppGroup( + uid = WEBVIEW_ZYGOTE_UID, + primaryPackageName = WEBVIEW_ZYGOTE_PROFILE_KEY, + apps = listOf( + InstalledApp( + packageName = WEBVIEW_ZYGOTE_PROFILE_KEY, + label = "WebView Zygote", + uid = WEBVIEW_ZYGOTE_UID, + isSystem = true, + profileKey = WEBVIEW_ZYGOTE_PROFILE_KEY, + special = true, + ) + ), + ) + } val packageManager = application.packageManager val cached = cache.packages.value val packages = (cached.ifEmpty { installedPackages(packageManager) }) @@ -186,6 +230,7 @@ class SuperUserRepository( uid = info?.uid ?: fallbackUid, isSystem = info?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0, firstInstallTime = firstInstallTime, + lastUpdateTime = lastUpdateTime, ) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt index 7736797ca..0e0ca891b 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/settings/SettingsPlatformRepository.kt @@ -217,9 +217,6 @@ class SettingsPlatformRepository( selinuxHideStatus = runCatching { ksuCliRepository.getFeatureStatus("selinux_hide") }.getOrDefault(""), - webViewZygoteUmountStatus = runCatching { - ksuCliRepository.getFeatureStatus("webview_zygote_umount") - }.getOrDefault(""), ) } @@ -258,7 +255,6 @@ class SettingsPlatformRepository( cardConfig.save() themeConfig.preventBackgroundRefresh = false backgroundManager.saveBackgroundDim(0f) - backgroundManager.saveEnableBlur(false) backgroundManager.saveEnableBlurExp(false) backgroundManager.saveUseBackgroundSeedColor(false) backgroundManager.saveEnableHighContrastMode(false) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/shell/KsuCli.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/shell/KsuCli.kt index 2a133dfd7..2aa64e390 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/shell/KsuCli.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/shell/KsuCli.kt @@ -620,15 +620,12 @@ class KsuCliRepository(context: Context) { fun getZygiskImplement(): String { val zygiskModuleIds = listOf( "zygisksu", - "rezygisk", - "shirokozygisk" + "rezygisk" ) for (moduleId in zygiskModuleIds) { - // 忽略禁用/即将删除 if (SuFile.open("/data/adb/modules/$moduleId/disable").isFile || SuFile.open("/data/adb/modules/$moduleId/remove").isFile) continue - // 读取prop val propFile = SuFile.open("/data/adb/modules/$moduleId/module.prop") if (!propFile.isFile) continue diff --git a/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt b/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt index 55c7b4461..c9e8967c6 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/data/system/HomeRuntimeRepository.kt @@ -5,17 +5,17 @@ import android.app.Application import android.os.Build import android.system.Os import com.resukisu.resukisu.BuildConfig -import com.resukisu.resukisu.data.shell.KsuCliRepository import com.resukisu.resukisu.domain.model.HomeBasicInfo -import com.resukisu.resukisu.domain.model.HomeModuleOverview import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext class HomeRuntimeRepository( private val application: Application, - private val ksuCliRepository: KsuCliRepository, ) { - suspend fun getBasicInfo(managerUapiVersion: Int): HomeBasicInfo = + suspend fun getBasicInfo( + managerUapiVersion: Int, + includeSelinuxStatus: Boolean = true, + ): HomeBasicInfo = withContext(Dispatchers.IO) { val uname = runCatching { Os.uname() }.getOrNull() HomeBasicInfo( @@ -27,27 +27,15 @@ class HomeRuntimeRepository( BuildConfig.VERSION_CODE, managerUapiVersion, ), - selinuxStatus = runCatching { getSELinuxStatus(application) }.getOrDefault("Unknown"), + selinuxStatus = if (includeSelinuxStatus) { + runCatching { getSELinuxStatus(application) }.getOrDefault("Unknown") + } else { + "" + }, seccompStatus = runCatching { Os.prctl(21, 0, 0, 0, 0) }.getOrDefault(-1), ) } - suspend fun getModuleOverview(): HomeModuleOverview = withContext(Dispatchers.IO) { - HomeModuleOverview( - count = runCatching { ksuCliRepository.getModuleCount() }.getOrDefault(0), - zygiskImplementation = runCatching { - ksuCliRepository.getZygiskImplement() - }.getOrDefault("None"), - metaModuleImplementation = runCatching { - ksuCliRepository.getMetaModuleImplement() - }.getOrDefault("None"), - ) - } - - suspend fun getSuperuserCount(): Int = withContext(Dispatchers.IO) { - runCatching { ksuCliRepository.getSuperuserCount() }.getOrDefault(0) - } - @SuppressLint("PrivateApi") private fun getDeviceModel(): String = runCatching { val systemProperties = Class.forName("android.os.SystemProperties") diff --git a/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt b/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt index 93dbd276f..aaafed1df 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/di/AppModules.kt @@ -71,8 +71,6 @@ import com.resukisu.resukisu.domain.usecase.GetBooleanPreferenceUseCase import com.resukisu.resukisu.domain.usecase.GetCatalogModuleUseCase import com.resukisu.resukisu.domain.usecase.GetDefaultUmountModulesUseCase import com.resukisu.resukisu.domain.usecase.GetHomeBasicInfoUseCase -import com.resukisu.resukisu.domain.usecase.GetHomeModuleOverviewUseCase -import com.resukisu.resukisu.domain.usecase.GetHomeSuperuserCountUseCase import com.resukisu.resukisu.domain.usecase.GetInstallEnvironmentUseCase import com.resukisu.resukisu.domain.usecase.GetKernelFeatureSettingsUseCase import com.resukisu.resukisu.domain.usecase.GetKernelStatusUseCase @@ -131,7 +129,6 @@ import com.resukisu.resukisu.domain.usecase.SetSelinuxHideEnabledUseCase import com.resukisu.resukisu.domain.usecase.SetStringPreferenceUseCase import com.resukisu.resukisu.domain.usecase.SetStringSetPreferenceUseCase import com.resukisu.resukisu.domain.usecase.SetSuEnabledUseCase -import com.resukisu.resukisu.domain.usecase.SetWebViewZygoteUmountEnabledUseCase import com.resukisu.resukisu.domain.usecase.StartKernelFlashUseCase import com.resukisu.resukisu.domain.usecase.SuSFSConfigUseCase import com.resukisu.resukisu.domain.usecase.TakeModuleUriPermissionUseCase @@ -296,8 +293,6 @@ val repositoryModule = module { val useCaseModule = module { factoryOf(::InitializeApplicationUseCase) factoryOf(::GetHomeBasicInfoUseCase) - factoryOf(::GetHomeModuleOverviewUseCase) - factoryOf(::GetHomeSuperuserCountUseCase) factoryOf(::IsNetworkAvailableUseCase) factoryOf(::LoadSettingsPlatformUseCase) factoryOf(::UpdateAppearanceUseCase) @@ -320,7 +315,6 @@ val useCaseModule = module { factoryOf(::ConfigureSuLogUseCase) factoryOf(::SetSelinuxHideEnabledUseCase) factoryOf(::SetDefaultUmountModulesUseCase) - factoryOf(::SetWebViewZygoteUmountEnabledUseCase) factoryOf(::IsLateLoadModeUseCase) factoryOf(::GetAppProfileUseCase) factoryOf(::SetAppProfileUseCase) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt index beb9c0a74..0a892a177 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/HomeRuntime.kt @@ -43,8 +43,10 @@ data class HomeDashboardState( val betaManagerUpdate: ManagerUpdateInfo? = null, val isBetaManagerUpdateCheckFailed: Boolean = false, val isSimpleMode: Boolean = false, + val showNavigationBarBadge: Boolean = true, + val showHomeCardIcons: Boolean = false, val isInitialDataLoaded: Boolean = false, val isCoreDataLoaded: Boolean = false, val isExtendedDataLoaded: Boolean = false, val isRefreshing: Boolean = false, -) +) \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt index 363100749..d1c866a1b 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/InstalledAppGroup.kt @@ -1,12 +1,24 @@ package com.resukisu.resukisu.domain.model +const val WEBVIEW_ZYGOTE_UID = 1053 +const val WEBVIEW_ZYGOTE_PROFILE_KEY = "webview_zygote" + data class InstalledApp( val packageName: String, val label: String, val uid: Int, val isSystem: Boolean = false, val firstInstallTime: Long = 0L, -) + val lastUpdateTime: Long = 0L, + val profileKey: String = packageName, + val special: Boolean = false, +) { + val displayIdentifier: String + get() = if (special) profileKey else packageName + + val isWebViewZygote: Boolean + get() = special && uid == WEBVIEW_ZYGOTE_UID +} data class InstalledAppGroup( val uid: Int, @@ -19,15 +31,27 @@ data class InstalledAppGroup( val mainApp: InstalledApp get() = apps.first { it.packageName == primaryPackageName } + val profileKey: String + get() = mainApp.profileKey + + val isWebViewZygote: Boolean + get() = mainApp.isWebViewZygote + val packageNames: List get() = apps.map(InstalledApp::packageName) val allowSu: Boolean - get() = profile?.allowSu == true + get() = !isWebViewZygote && profile?.allowSu == true val hasCustomProfile: Boolean get() = profile?.let { - if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault + if (isWebViewZygote) { + !it.nonRootUseDefault + } else if (it.allowSu) { + !it.rootUseDefault + } else { + !it.nonRootUseDefault + } } ?: false val isRecentlyInstalled: Boolean diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt index 8e8730b58..3d20dadfb 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/KernelState.kt @@ -22,8 +22,7 @@ data class KernelStatus( val lkmMode: Boolean? = null, val kernelVersion: KernelVersion, val isRootAvailable: Boolean = false, - val requireNewKernel: Boolean = false, - val uapiMismatch: Boolean = false, + val isFullFeatured: Boolean = false, val isSELinuxPermissive: Boolean = false, val isOfficialSignature: Boolean = true, val kernelPatchImplementation: KernelPatchImplementation = KernelPatchImplementation.NONE, @@ -31,15 +30,11 @@ data class KernelStatus( val isSafeMode: Boolean = false, val isLateLoadMode: Boolean = false, val isPrBuild: Boolean = false, -) { - val isValid: Boolean - get() = isManager && !requireNewKernel && isRootAvailable -} +) data class KernelFeatureSettings( val suEnabled: Boolean, val kernelUmountEnabled: Boolean, - val webViewZygoteUmountEnabled: Boolean, val suLogEnabled: Boolean, val selinuxHideEnabled: Boolean, val defaultUmountModules: Boolean, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/SettingsPlatform.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/SettingsPlatform.kt index 58d167332..037580080 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/model/SettingsPlatform.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/model/SettingsPlatform.kt @@ -33,7 +33,6 @@ data class PlatformFeatureStatus( val adbRootEnabled: Boolean = false, val sulogStatus: String = "", val selinuxHideStatus: String = "", - val webViewZygoteUmountStatus: String = "", ) sealed interface AppearanceSetting { diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt index 5de1a02db..1372c53ad 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/HomeRuntimeUseCases.kt @@ -4,16 +4,10 @@ import com.resukisu.resukisu.data.network.NetworkStatusRepository import com.resukisu.resukisu.data.system.HomeRuntimeRepository class GetHomeBasicInfoUseCase(private val repository: HomeRuntimeRepository) { - suspend operator fun invoke(managerUapiVersion: Int) = - repository.getBasicInfo(managerUapiVersion) -} - -class GetHomeModuleOverviewUseCase(private val repository: HomeRuntimeRepository) { - suspend operator fun invoke() = repository.getModuleOverview() -} - -class GetHomeSuperuserCountUseCase(private val repository: HomeRuntimeRepository) { - suspend operator fun invoke() = repository.getSuperuserCount() + suspend operator fun invoke( + managerUapiVersion: Int, + includeSelinuxStatus: Boolean = true, + ) = repository.getBasicInfo(managerUapiVersion, includeSelinuxStatus) } class IsNetworkAvailableUseCase(private val repository: NetworkStatusRepository) { diff --git a/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/KernelUseCases.kt b/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/KernelUseCases.kt index 4fff334c1..3dd248307 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/KernelUseCases.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/domain/usecase/KernelUseCases.kt @@ -34,10 +34,6 @@ class SetDefaultUmountModulesUseCase(private val repository: KernelRepository) { suspend operator fun invoke(enabled: Boolean) = repository.setDefaultUmountModules(enabled) } -class SetWebViewZygoteUmountEnabledUseCase(private val repository: KernelRepository) { - suspend operator fun invoke(enabled: Boolean) = repository.setWebviewZygoteUmountEnabled(enabled) -} - class IsLateLoadModeUseCase(private val repository: KernelRepository) { operator fun invoke() = repository.isLateLoadMode() } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/MainActivity.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/MainActivity.kt index a139c8cf0..b2429eb1f 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/MainActivity.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/MainActivity.kt @@ -80,7 +80,7 @@ class MainActivity : ComponentActivity() { splashScreen.setKeepOnScreenCondition { shouldKeepStartupSplash( startupState = startupState.value, - homeInitialDataLoaded = homeViewModel.state.value.isInitialDataLoaded, + homeInitialDataLoaded = homeViewModel.homeStateRepository.state.value.isInitialDataLoaded, ) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/NavContainer.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/NavContainer.kt index b2a348bdf..1ecbadbae 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/NavContainer.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/NavContainer.kt @@ -11,10 +11,15 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -23,9 +28,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.paint @@ -42,38 +45,25 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.core.app.ActivityCompat import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator -import androidx.navigation3.runtime.NavEntryDecorator -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberDecoratedNavEntries -import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator -import androidx.navigation3.scene.SceneInfo -import androidx.navigation3.scene.SinglePaneSceneStrategy -import androidx.navigation3.scene.rememberSceneState -import androidx.navigation3.ui.NavDisplay +import androidx.navigationevent.NavigationEventInfo import androidx.navigationevent.compose.NavigationBackHandler -import androidx.navigationevent.compose.NavigationEventState import androidx.navigationevent.compose.rememberNavigationEventState import com.resukisu.resukisu.ui.activity.PermissionRequestInterface -import com.resukisu.resukisu.ui.animation.predictiveback.AOSPCrossActivityAnimation -import com.resukisu.resukisu.ui.animation.predictiveback.KernelSUClassicPredictiveBackAnimation -import com.resukisu.resukisu.ui.animation.predictiveback.MiuixPredictiveBackAnimation -import com.resukisu.resukisu.ui.animation.predictiveback.NoPredictiveBackAnimation -import com.resukisu.resukisu.ui.animation.predictiveback.ScalePredictiveBackAnimation +import com.resukisu.resukisu.ui.animation.predictiveback.installerNavTransition import com.resukisu.resukisu.ui.component.InstallConfirmationDialog import com.resukisu.resukisu.ui.component.ZipFileDetector import com.resukisu.resukisu.ui.component.ZipFileInfo import com.resukisu.resukisu.ui.component.ZipType import com.resukisu.resukisu.ui.navigation.HandleDeepLink import com.resukisu.resukisu.ui.navigation.LocalNavigator +import com.resukisu.resukisu.ui.navigation.Navigator import com.resukisu.resukisu.ui.navigation.Route -import com.resukisu.resukisu.ui.navigation.rememberNavigator import com.resukisu.resukisu.ui.overscroll.StretchOverscrollCompensationState import com.resukisu.resukisu.ui.overscroll.rememberCustomOverscrollFactory import com.resukisu.resukisu.ui.screen.AppProfileScreen @@ -94,13 +84,16 @@ import com.resukisu.resukisu.ui.screen.moduleRepo.ModuleRepoScreen import com.resukisu.resukisu.ui.screen.moduleRepo.OnlineModuleDetailScreen import com.resukisu.resukisu.ui.screen.susfs.SuSFSConfigScreen import com.resukisu.resukisu.ui.screen.themeSettings.ThemeSettingsScreen +import com.resukisu.resukisu.ui.theme.BackgroundRenderState import com.resukisu.resukisu.ui.theme.LocalBackgroundRenderState import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.util.LocalBackgroundBlurAnchor import com.resukisu.resukisu.ui.util.LocalBlurState import com.resukisu.resukisu.ui.util.LocalPermissionRequestInterface +import com.resukisu.resukisu.ui.util.LocalPortraitState import com.resukisu.resukisu.ui.util.LocalSnackbarHost import com.resukisu.resukisu.ui.util.LocalStretchOverscrollCompensationState +import com.resukisu.resukisu.ui.util.rememberDeviceCornerRadius import com.resukisu.resukisu.ui.viewmodel.MainIntentViewModel import com.resukisu.resukisu.ui.viewmodel.PredictiveBackAnimation import com.resukisu.resukisu.ui.viewmodel.SettingsViewModel @@ -116,6 +109,11 @@ import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel import top.yukonga.miuix.kmp.blur.LayerBackdrop import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop +import top.yukonga.miuix.kmp.nav.core.NavCornerClipMode +import top.yukonga.miuix.kmp.nav.core.NavDisplay +import top.yukonga.miuix.kmp.nav.core.NavDisplayEffects +import top.yukonga.miuix.kmp.nav.core.rememberNavBackStack +import top.yukonga.miuix.kmp.nav.transition.NavSwipeDirection import top.yukonga.miuix.kmp.shader.isRenderEffectSupported import kotlin.coroutines.resume @@ -173,7 +171,24 @@ fun NavContainer( } } - val navigator = rememberNavigator(Route.Main) + val backStack = rememberNavBackStack(Route.Main) + val navigator = remember(backStack) { Navigator(backStack) } + val onBack = remember(navigator) { + { + when (val top = navigator.current()) { + is Route.TemplateEditor -> { + if (!top.readOnly) { + navigator.setResult("template_edit", true) + } else { + navigator.pop() + } + } + + else -> navigator.pop() + } + } + } + val useBlur = themeConfig.isEnableBlur lateinit var permissionRequestHandler: ManagedActivityResultLauncher, Map> @@ -334,226 +349,365 @@ fun NavContainer( } ) - val predictiveBackAnimationHandler = remember( + val navCornerRadius = rememberDeviceCornerRadius(defaultRadius = 0.dp) + val roundAllCorners = + settings.predictiveBackAnimation == PredictiveBackAnimation.AOSP || + settings.predictiveBackAnimation == PredictiveBackAnimation.Scale || + settings.predictiveBackAnimation == PredictiveBackAnimation.KernelSUClassic + val backdropColor = MaterialTheme.colorScheme.surfaceContainer + val effects = remember(navCornerRadius, roundAllCorners, backdropColor) { + NavDisplayEffects( + enableCornerClip = true, + cornerClipRadius = if (roundAllCorners && navCornerRadius <= 0.dp) 32.dp else navCornerRadius, + cornerClipMode = if (roundAllCorners) NavCornerClipMode.All else NavCornerClipMode.Leading, + dimAmount = 0.5f, + backdropColor = backdropColor, + blockInputDuringTransition = false, + ) + } + val transition = remember( settings.predictiveBackAnimation, settings.predictiveBackExitDirection ) { - when (settings.predictiveBackAnimation) { - PredictiveBackAnimation.None -> NoPredictiveBackAnimation() - PredictiveBackAnimation.AOSP -> AOSPCrossActivityAnimation(settings.predictiveBackExitDirection) - PredictiveBackAnimation.Scale -> ScalePredictiveBackAnimation( - settings.predictiveBackExitDirection - ) - - PredictiveBackAnimation.KernelSUClassic -> KernelSUClassicPredictiveBackAnimation() - PredictiveBackAnimation.MIUIX -> MiuixPredictiveBackAnimation() - } + installerNavTransition( + animation = settings.predictiveBackAnimation, + exitDirection = settings.predictiveBackExitDirection, + ) } + val swipeBackDirection = when (LocalLayoutDirection.current) { + LayoutDirection.Rtl -> NavSwipeDirection.RightToLeft + LayoutDirection.Ltr -> NavSwipeDirection.LeftToRight + } + val interceptPredictiveBack = + settings.predictiveBackAnimation == PredictiveBackAnimation.None && backStack.size > 1 - var gestureState: NavigationEventState>? = null - val navigationScope = rememberCoroutineScope() - val onBack: (() -> Unit) -> Unit = { callBack -> - navigationScope.launch { - predictiveBackAnimationHandler.onBackPressed( - transitionState = gestureState?.transitionState, - currentPageKey = navigator.current() - ) - - callBack() - - when (val top = navigator.current()) { - is Route.TemplateEditor -> { - if (!top.readOnly) { - navigator.setResult("template_edit", true) - } else { - navigator.pop() - } - } - - else -> navigator.pop() + NavDisplay( + backStack = backStack, + onBack = onBack, + transition = transition, + effects = effects, + ) { + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + AboutScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + OpenSourceLicenseScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + SulogScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + MainScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + AppProfileTemplateScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + TemplateEditorScreen( + templateId = key.templateId, + readOnly = key.readOnly, + isCreation = key.isCreation, + ) + } + } + entry(swipeDismiss = swipeBackDirection) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + AppProfileScreen(key.uid, key.packageName) + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + ModuleRepoScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + OnlineModuleDetailScreen(key.moduleId) + } + } + entry(swipeDismiss = NavSwipeDirection.None) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + InstallScreen(key.preselectedKernelUri) + } + } + entry(swipeDismiss = swipeBackDirection) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + FlashScreen(key.toFlashIt()) + } + } + entry(swipeDismiss = swipeBackDirection) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + ExecuteModuleActionScreen(key.moduleId) + } + } + entry(swipeDismiss = NavSwipeDirection.None) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + MainScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + MainScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + MainScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + MainScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + ThemeSettingsScreen(settingsViewModel = settingsViewModel) + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + SuSFSConfigScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + UmountManagerScreen() + } + } + entry(swipeDismiss = swipeBackDirection) { + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + DynamicManagerScreen() + } + } + entry(swipeDismiss = NavSwipeDirection.None) { key -> + ManagerNavEntry( + interceptPredictiveBack = interceptPredictiveBack, + onBack = onBack, + themeConfig = themeConfig, + backgroundRenderState = backgroundRenderState, + useBlur = useBlur, + ) { + KernelFlashScreen(key.kernelUri, key.selectedSlot) } } } + } +} - val entries = - rememberDecoratedNavEntries( - backStack = navigator.backStack, - entryDecorators = listOf( - rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator(), - NavEntryDecorator( - onPop = { key -> - predictiveBackAnimationHandler.onPagePop( - contentPageKey = key, - animationScope = navigationScope - ) - } - ) { content -> - val snackBarHostState = remember { SnackbarHostState() } - var backgroundBlurAnchorCoordinates by remember { - mutableStateOf(null) - } +@Composable +private fun ManagerNavEntry( + interceptPredictiveBack: Boolean, + onBack: () -> Unit, + themeConfig: ThemeConfig, + backgroundRenderState: BackgroundRenderState, + useBlur: Boolean, + content: @Composable () -> Unit, +) { + val navigationEventState = rememberNavigationEventState(NavigationEventInfo.None) + NavigationBackHandler( + state = navigationEventState, + isBackEnabled = interceptPredictiveBack, + onBackCompleted = onBack, + ) + val snackBarHostState = remember { androidx.compose.material3.SnackbarHostState() } + var backgroundBlurAnchorCoordinates by remember { + mutableStateOf(null) + } - LaunchedEffect(backgroundRenderState.imagePainter) { - if (backgroundRenderState.imagePainter == null) { - backgroundBlurAnchorCoordinates = null - } - } + LaunchedEffect(backgroundRenderState.imagePainter) { + if (backgroundRenderState.imagePainter == null) { + backgroundBlurAnchorCoordinates = null + } + } - with(predictiveBackAnimationHandler) { - Box( - modifier = Modifier - .fillMaxSize() - .predictiveBackAnimationDecorator( - gestureState?.transitionState, - content.contentKey, - navigator.current() - ) - .then( - if (!themeConfig.backgroundImageLoaded) Modifier.background( - MaterialTheme.colorScheme.surfaceContainer - ) else Modifier - ) - ) { - val surfaceContainer = - MaterialTheme.colorScheme.surfaceContainer - - CompositionLocalProvider( - LocalBlurState provides rememberMaterial3BlurBackdrop( - themeConfig.isEnableBlur - ), - LocalSnackbarHost provides snackBarHostState, - LocalBackgroundBlurAnchor provides backgroundBlurAnchorCoordinates, - ) { - backgroundRenderState.imagePainter?.let { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(-1f) - .onGloballyPositioned { newCoordinates -> - backgroundBlurAnchorCoordinates = - newCoordinates.takeIf { coordinates -> - coordinates.isAttached - } - } - .paint( - painter = it, - contentScale = ContentScale.Crop, - ) - .drawWithContent { - drawContent() - drawRect( - color = surfaceContainer.copy( - alpha = themeConfig.backgroundDim - ) - ) - } - ) - } - content.Content() + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .then( + if (!themeConfig.backgroundImageLoaded) Modifier.background( + MaterialTheme.colorScheme.surfaceContainer + ) else Modifier + ) + ) { + val isPortrait = maxWidth < maxHeight || (maxHeight / maxWidth > 1.4f) + val surfaceContainer = + MaterialTheme.colorScheme.surfaceContainer + + CompositionLocalProvider( + LocalPortraitState provides isPortrait, + LocalBlurState provides rememberMaterial3BlurBackdrop( + enableBlur = useBlur + ), + LocalSnackbarHost provides snackBarHostState, + LocalBackgroundBlurAnchor provides backgroundBlurAnchorCoordinates, + ) { + backgroundRenderState.imagePainter?.let { + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(-1f) + .onGloballyPositioned { newCoordinates -> + backgroundBlurAnchorCoordinates = + newCoordinates.takeIf { coordinates -> + coordinates.isAttached } - } } - } - ), - entryProvider = entryProvider { - entry { AboutScreen() } - entry { OpenSourceLicenseScreen() } - entry { SulogScreen() } - entry { MainScreen() } - entry { AppProfileTemplateScreen() } - entry { key -> - TemplateEditorScreen( - templateId = key.templateId, - readOnly = key.readOnly, - isCreation = key.isCreation, - ) - } - entry { key -> AppProfileScreen(key.uid, key.packageName) } - entry { ModuleRepoScreen() } - entry { key -> - OnlineModuleDetailScreen( - key.moduleId + .paint( + painter = it, + contentScale = ContentScale.Crop, ) - } - entry { key -> InstallScreen(key.preselectedKernelUri) } - entry { key -> FlashScreen(key.toFlashIt()) } - entry { key -> - ExecuteModuleActionScreen( - key.moduleId - ) - } - entry { MainScreen() } - entry { MainScreen() } - entry { MainScreen() } - entry { MainScreen() } - entry { - ThemeSettingsScreen(settingsViewModel = settingsViewModel) - } - entry { SuSFSConfigScreen() } - entry { UmountManagerScreen() } - entry { DynamicManagerScreen() } - entry { key -> - KernelFlashScreen( - key.kernelUri, - key.selectedSlot - ) - } - }, - ) - - val sceneState = - rememberSceneState( - entries = entries, - sceneStrategies = listOf(SinglePaneSceneStrategy()), - sceneDecoratorStrategies = emptyList(), - sharedTransitionScope = null, - onBack = { - onBack {} - }, - ) - val scene = sceneState.currentScene - - // Predictive Back Handling - val currentInfo = SceneInfo(scene) - val previousSceneInfos = sceneState.previousScenes.map { SceneInfo(it) } - gestureState = rememberNavigationEventState( - currentInfo = currentInfo, - backInfo = previousSceneInfos - ) - - NavigationBackHandler( - state = gestureState, - isBackEnabled = scene.previousEntries.isNotEmpty(), - onBackCompleted = { callBack -> - onBack(callBack) - }, - onBackCancelled = { callBack -> - callBack() + .drawWithContent { + drawContent() + drawRect( + color = surfaceContainer.copy( + alpha = themeConfig.backgroundDim + ) + ) + } + ) } - ) - - NavDisplay( - sceneState = sceneState, - navigationEventState = gestureState, - contentAlignment = Alignment.TopStart, - sizeTransform = null, - predictivePopTransitionSpec = { swipeEdge -> - with(predictiveBackAnimationHandler) { - onPredictivePopTransitionSpec(swipeEdge = swipeEdge) - } - }, - popTransitionSpec = { - with(predictiveBackAnimationHandler) { - onPopTransitionSpec() - } - }, - transitionSpec = { - with(predictiveBackAnimationHandler) { - onTransitionSpec() - } - }, - ) + Box( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding( + WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal) + ) + ) { + content() + } + } } } @@ -635,7 +789,7 @@ fun rememberMaterial3BlurBackdrop( 0f } val physicalPageOffset = pageOffset * pagerViewportWidth * - if (layoutDirection == LayoutDirection.Ltr) 1f else -1f + if (layoutDirection == LayoutDirection.Ltr) 1f else -1f val backgroundOffset = pagerViewportLeft + physicalPageOffset val backgroundBitmap = backgroundRenderState.imageBitmap @@ -716,7 +870,7 @@ private fun ShortcutIntentHandler( .putExtra("from_webui_shortcut", true) .addFlags( Intent.FLAG_ACTIVITY_NEW_TASK or - Intent.FLAG_ACTIVITY_CLEAR_TASK + Intent.FLAG_ACTIVITY_CLEAR_TASK ) context.startActivity(webIntent) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt index 473067c31..ee8dd3ab8 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/activity/component/NavigationBar.kt @@ -1,14 +1,21 @@ package com.resukisu.resukisu.ui.activity.component import android.annotation.SuppressLint +import android.os.Build import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox @@ -16,6 +23,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FlexibleBottomAppBar import androidx.compose.material3.Icon +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text @@ -25,22 +33,30 @@ import androidx.compose.material3.WideNavigationRailDefaults import androidx.compose.material3.WideNavigationRailItem import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.resukisu.resukisu.ui.component.FloatingBottomBar +import com.resukisu.resukisu.ui.component.FloatingBottomBarItem import com.resukisu.resukisu.ui.screen.BottomBarDestination +import com.resukisu.resukisu.ui.theme.BottomBarStyle import com.resukisu.resukisu.ui.theme.CardConfig import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect +import com.resukisu.resukisu.ui.util.LocalBlurState import com.resukisu.resukisu.ui.util.LocalHandlePageChange +import com.resukisu.resukisu.ui.util.LocalPagerState import com.resukisu.resukisu.ui.util.LocalSelectedPage import com.resukisu.resukisu.ui.viewmodel.HomeViewModel import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel +import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop -// TODO Add FloatingBottomBar as an choice to user @SuppressLint("ContextCastToActivity") @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -56,14 +72,72 @@ fun NavigationBar( val uiState by homeViewModel.uiState.collectAsStateWithLifecycle() val superuserCount = uiState.systemInfo.superuserCount val moduleCount = uiState.systemInfo.moduleCount + val showNavigationBarBadge = uiState.showNavigationBarBadge val page = LocalSelectedPage.current val handlePageChange = LocalHandlePageChange.current + val pagerState = LocalPagerState.current - if (isBottomBar) { + if (isBottomBar && themeConfig.bottomBarStyle == BottomBarStyle.FLOATING && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Box( + modifier = Modifier + .fillMaxWidth() + .windowInsetsPadding( + WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal) + ) + .padding( + bottom = 12.dp + WindowInsets.navigationBars.asPaddingValues() + .calculateBottomPadding() + ), + contentAlignment = Alignment.Center + ) { + FloatingBottomBar( + selectedIndex = pagerState.targetPage, + onSelected = { handlePageChange(it) }, + tabsCount = destinations.size, + isBlurEnabled = LocalBlurState.current != null, + ) { activateTab -> + destinations.forEachIndexed { index, destination -> + FloatingBottomBarItem( + selected = index == pagerState.targetPage, + onClick = { activateTab(index) }, + modifier = Modifier.defaultMinSize(minWidth = 76.dp) + ) { + val contentColor = LocalContentColor.current + val count = when (destination) { + BottomBarDestination.SuperUser -> superuserCount + BottomBarDestination.Module -> moduleCount + else -> 0 + } + val icon: @Composable () -> Unit = { + Icon( + imageVector = destination.iconSelected, + contentDescription = stringResource(destination.label), + tint = contentColor + ) + } + if (count > 0 && showNavigationBarBadge) { + BadgedBox(badge = { Badge { Text(count.toString()) } }) { icon() } + } else { + icon() + } + Text( + text = stringResource(destination.label), + color = contentColor, + fontSize = 11.sp, + lineHeight = 14.sp, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Visible + ) + } + } + } + } + } else if (isBottomBar) { FlexibleBottomAppBar( modifier = modifier .windowInsetsPadding( - WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal) + WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal) ) .blurEffect( compensateHorizontalOverscroll = true, @@ -86,6 +160,7 @@ fun NavigationBar( }, superuserCount = superuserCount, moduleCount = moduleCount, + showNavigationBarBadge = showNavigationBarBadge, ) } } @@ -93,7 +168,7 @@ fun NavigationBar( WideNavigationRail( modifier = modifier .windowInsetsPadding( - WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal) + WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal) ) .blurEffect( compensateHorizontalOverscroll = true, @@ -121,6 +196,7 @@ fun NavigationBar( }, superuserCount = superuserCount, moduleCount = moduleCount, + showNavigationBarBadge = showNavigationBarBadge, ) } } @@ -134,6 +210,7 @@ private fun NavigationRailItem( onClick: () -> Unit, superuserCount: Int, moduleCount: Int, + showNavigationBarBadge: Boolean, ) { WideNavigationRailItem( railExpanded = false, @@ -146,6 +223,7 @@ private fun NavigationRailItem( dest = destination, superUser = superuserCount, module = moduleCount, + show = showNavigationBarBadge, ) } ) { @@ -175,6 +253,7 @@ private fun RowScope.BottomBarNavigationItem( onClick: () -> Unit, superuserCount: Int, moduleCount: Int, + showNavigationBarBadge: Boolean, ) { NavigationBarItem( selected = isSelected, @@ -186,6 +265,7 @@ private fun RowScope.BottomBarNavigationItem( dest = destination, superUser = superuserCount, module = moduleCount, + show = showNavigationBarBadge, ) } ) { @@ -214,6 +294,7 @@ private fun DestinationBadge( dest: BottomBarDestination, superUser: Int, module: Int, + show: Boolean, ) { val count = when (dest) { BottomBarDestination.SuperUser -> superUser @@ -222,7 +303,7 @@ private fun DestinationBadge( } AnimatedVisibility( - visible = count > 0, + visible = count > 0 && show, enter = fadeIn(), exit = fadeOut() ) { diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AOSPCrossActivityAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AOSPCrossActivityAnimation.kt deleted file mode 100644 index a8c84374a..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AOSPCrossActivityAnimation.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.CubicBezierEasing -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalWindowInfo -import androidx.compose.ui.unit.dp -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigationevent.NavigationEvent.Companion.EDGE_LEFT -import androidx.navigationevent.NavigationEventTransitionState -import androidx.navigationevent.NavigationEventTransitionState.InProgress -import com.resukisu.resukisu.ui.util.rememberDeviceCornerRadius -import com.resukisu.resukisu.ui.viewmodel.PredictiveBackExitDirection -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch - -class AOSPCrossActivityAnimation( - private val exitDirection: PredictiveBackExitDirection = PredictiveBackExitDirection.ALWAYS_RIGHT -) : PredictiveBackAnimationHandler { - private var exitingPageKey: String? = null - private val exitAnimatable = Animatable(0f) - - override suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey?, - ) { - exitingPageKey = currentPageKey.toString() - - exitAnimatable.animateTo( - targetValue = 1f, - animationSpec = tween(durationMillis = 150, easing = LinearEasing) - ) - } - - override fun onPagePop(contentPageKey: Any, animationScope: CoroutineScope) { - if (exitingPageKey == contentPageKey) { - exitingPageKey = null - animationScope.launch { - exitAnimatable.snapTo(0f) - } - } - } - - @Composable - override fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier = composed { - val windowInfo = LocalWindowInfo.current - val containerHeightPx = windowInfo.containerSize.height - val pageKey = contentPageKey.toString() - val deviceCornerRadius = rememberDeviceCornerRadius() - - val enteringStartOffsetPx = with(LocalDensity.current) { 96.dp.toPx() } - - val linearProgress = exitAnimatable.value - val emphasizedProgress = CubicBezierEasing(0.2f, 0f, 0f, 1f).transform(linearProgress) - - val progressInProgress = (transitionState as? InProgress) - val edge = progressInProgress?.latestEvent?.swipeEdge ?: 0 - val touchY = progressInProgress?.latestEvent?.touchY - val gestureProgress = progressInProgress?.latestEvent?.progress ?: 0f - - val directionMultiplier = when (exitDirection) { - PredictiveBackExitDirection.FOLLOW_GESTURE -> if (edge == EDGE_LEFT) 1f else -1f - PredictiveBackExitDirection.ALWAYS_RIGHT -> 1f - PredictiveBackExitDirection.ALWAYS_LEFT -> -1f - } - - val isExitingPage = exitingPageKey != null && exitingPageKey == pageKey - val isCurrentNavTarget = exitingPageKey == null && pageKey == currentPageKey.toString() - - val maxScale = 0.85f - val dragScale = 1f - (1f - maxScale) * gestureProgress - - val currentPivotY = if (touchY != null && containerHeightPx > 0) { - (touchY / containerHeightPx).coerceIn(0.1f, 0.9f) - } else 0.5f - val currentPivotX = if (edge == EDGE_LEFT) 0.8f else 0.2f - - this - .graphicsLayer { - if (transitionState is InProgress) - transformOrigin = TransformOrigin(currentPivotX, currentPivotY) - - when { - isExitingPage -> { - // top page when onBackPressed called (back committed) - val computedScaleX = dragScale + (maxScale - dragScale) * emphasizedProgress - val computedTranslationX = - enteringStartOffsetPx * directionMultiplier * emphasizedProgress - val computedAlpha = - if (linearProgress >= 0.2f) 0f else (1f - linearProgress * 5f).coerceAtLeast( - 0f - ) - - scaleX = computedScaleX - scaleY = computedScaleX - translationX = computedTranslationX - alpha = computedAlpha - } - - isCurrentNavTarget -> { - // top page before onBackPressed called - scaleX = dragScale - scaleY = dragScale - translationX = 0f - alpha = 1f - } - - else -> { - // bottom page - val initialTranslationX = -enteringStartOffsetPx * directionMultiplier - - if (exitingPageKey != null) { // after onBackPressed - scaleX = dragScale + (1f - dragScale) * emphasizedProgress - scaleY = dragScale + (1f - dragScale) * emphasizedProgress - translationX = initialTranslationX * (1f - emphasizedProgress) - alpha = 1f - } else if (transitionState is InProgress) { // before onBackPressed - scaleX = dragScale - scaleY = dragScale - translationX = initialTranslationX - alpha = 1f - } - } - } - } - .clip( - if (isExitingPage || isCurrentNavTarget) RoundedCornerShape(deviceCornerRadius) - else RoundedCornerShape(0.dp) - ) - } - - override fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - swipeEdge: Int - ): ContentTransform = ContentTransform( - targetContentEnter = EnterTransition.None, - initialContentExit = ExitTransition.None, - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { -it / 4 }), - initialContentExit = scaleOut(targetScale = 0.9f) + fadeOut(), - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { it }), - initialContentExit = ExitTransition.None, - sizeTransform = null - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AospNavTransition.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AospNavTransition.kt new file mode 100644 index 000000000..d787ad74c --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/AospNavTransition.kt @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.dp +import top.yukonga.miuix.kmp.nav.transition.NavGesture +import top.yukonga.miuix.kmp.nav.transition.NavMotion +import top.yukonga.miuix.kmp.nav.transition.NavRole +import top.yukonga.miuix.kmp.nav.transition.NavSettle +import top.yukonga.miuix.kmp.nav.transition.NavSettlePhase +import top.yukonga.miuix.kmp.nav.transition.NavSettleSpec +import top.yukonga.miuix.kmp.nav.transition.NavSwipeEdge +import top.yukonga.miuix.kmp.nav.transition.NavTransition +import top.yukonga.miuix.kmp.nav.transition.navDirectionalTransition +import top.yukonga.miuix.kmp.nav.transition.navGraphicsTransition +import kotlin.math.abs +import kotlin.math.exp +import kotlin.math.min +import kotlin.math.sin +import kotlin.math.sqrt + +private const val BOUNCE_STIFFNESS = 200f +private const val BOUNCE_DAMPING = 0.75f +private const val BOUNCE_MAX_KICK = 1000f +private const val BOUNCE_MIN_KICK = 120f +private const val OPEN_FADE_START = 0.12f +private const val OPEN_FADE_SPAN = 0.71f +private const val CLOSE_FADE_START = 0.21f +private const val CLOSE_FADE_SPAN = 0.74f +private const val CLASSIC_FADE_DURATION = 83f +private const val OPEN_FADE_OFFSET = 50f +private const val CLOSE_FADE_OFFSET = 35f +private const val CROSS_ACTIVITY_MIN_SCALE = 0.9f + +private val CrossActivityDrift = 96.dp +private val CrossActivityEdgeMargin = 8.dp + +private val ClassicActivityMotion = NavMotion( + programmatic = NavSettleSpec.Tween(durationMillis = 450, easing = FastOutExtraSlowIn), +) + +private val ClassicActivityOpen: NavTransition = navGraphicsTransition( + motion = ClassicActivityMotion, + scrim = { 0f }, +) { scope -> + val depth = scope.relativeDepth + val driftPx = with(scope.density) { CrossActivityDrift.toPx() } + if (depth <= 0f) { + val progress = topProgress(depth) + translationX = (1f - progress) * driftPx + alpha = if (scope.role == NavRole.Incoming) { + val settle = scope.settle + if (settle != null) { + ((settle.elapsedMillis - OPEN_FADE_OFFSET) / CLASSIC_FADE_DURATION) + .coerceIn(0f, 1f) + } else { + ((progress - OPEN_FADE_START) / OPEN_FADE_SPAN).coerceIn(0f, 1f) + } + } else { + 1f + } + } else { + translationX = -coverProgress(depth) * driftPx + } +} + +private val ClassicActivityClose: NavTransition = navGraphicsTransition( + motion = ClassicActivityMotion, + scrim = { 0f }, +) { scope -> + val depth = scope.relativeDepth + val driftPx = with(scope.density) { CrossActivityDrift.toPx() } + if (depth <= 0f) { + val progress = topProgress(depth) + translationX = (1f - progress) * driftPx + alpha = if (scope.role == NavRole.Outgoing) { + val settle = scope.settle + if (settle != null) { + (1f - (settle.elapsedMillis - CLOSE_FADE_OFFSET) / CLASSIC_FADE_DURATION) + .coerceIn(0f, 1f) + } else { + ((progress - CLOSE_FADE_START) / CLOSE_FADE_SPAN).coerceIn(0f, 1f) + } + } else { + 1f + } + } else { + translationX = -coverProgress(depth) * driftPx + } +} + +private val CrossActivityPredictive: NavTransition = navGraphicsTransition( + opaqueDepth = 1f, + motion = NavMotion( + commit = NavSettleSpec.Tween(durationMillis = 450, easing = FastOutExtraSlowIn), + cancel = NavSettleSpec.Spring(stiffness = 1500f), + ), + scrim = { scope -> + val settle = scope.settle + val gesture = scope.gesture + when { + settle?.phase == NavSettlePhase.Commit -> + (1f - settle.elapsedMillis / 450f).coerceIn(0f, 1f) + + gesture != null -> + (scope.relativeDepth.coerceIn(0f, 1f) / + (1f - gesture.progress).coerceAtLeast(0.01f)).coerceIn(0f, 1f) + + else -> scope.relativeDepth.coerceIn(0f, 1f) + } + }, +) { scope -> + val depth = scope.relativeDepth + val gesture = scope.gesture + val settle = scope.settle + val committing = settle?.phase == NavSettlePhase.Commit + val widthPx = scope.layoutSize.width.toFloat() + val heightPx = scope.layoutSize.height.toFloat() + val driftPx = with(scope.density) { CrossActivityDrift.toPx() } + val bounce = bounceScale(settle, gesture) + val hugMax = ( + widthPx * (1f - CROSS_ACTIVITY_MIN_SCALE) / 2f - + with(scope.density) { CrossActivityEdgeMargin.toPx() } + ).coerceAtLeast(0f) + val hugs = gesture?.swipeEdge != NavSwipeEdge.Right + if (depth <= 0f) { + val progress = topProgress(depth) + if (scope.role == NavRole.Outgoing && committing && gesture != null) { + val releaseProgress = (1f - gesture.progress).coerceAtLeast(0.01f) + val post = (1f - progress / releaseProgress).coerceIn(0f, 1f) + val releaseEasedProgress = shapedTopProgress(releaseProgress, gesture) + val committedScale = + CROSS_ACTIVITY_MIN_SCALE + (1f - CROSS_ACTIVITY_MIN_SCALE) * releaseEasedProgress + val grown = committedScale + (1f - committedScale) * post + scaleX = snapScaleToPixelExtent(grown * bounce, widthPx) + scaleY = scaleX + var tx = if (hugs) (1f - releaseEasedProgress) * hugMax else 0f + tx += post * driftPx + alpha = (1f - 5f * (settle.elapsedMillis / 450f)).coerceAtLeast(0f) + translationX = snapTranslationToPixelEdge(tx, scaleX, widthPx) + translationY = snapTranslationToPixelEdge( + translation = crossActivityYShift( + gesture = gesture, + height = heightPx, + scale = scaleX, + density = scope.density, + ), + scale = scaleY, + extent = heightPx, + ) + } else { + val easedProgress = shapedTopProgress(progress, gesture) + scaleX = snapScaleToPixelExtent( + scale = ( + CROSS_ACTIVITY_MIN_SCALE + (1f - CROSS_ACTIVITY_MIN_SCALE) * easedProgress + ) * bounce, + extent = widthPx, + ) + scaleY = scaleX + translationX = snapTranslationToPixelEdge( + translation = if (hugs) (1f - easedProgress) * hugMax else 0f, + scale = scaleX, + extent = widthPx, + ) + alpha = when { + scope.role == NavRole.Outgoing && gesture != null -> { + val releaseProgress = (1f - gesture.progress).coerceAtLeast(0.01f) + (1f - (1f - progress / releaseProgress).coerceIn(0f, 1f) * 3.5f) + .coerceAtLeast(0f) + } + + gesture != null -> 1f + else -> (progress / 0.2f).coerceIn(0f, 1f) + } + translationY = snapTranslationToPixelEdge( + translation = crossActivityYShift( + gesture = gesture, + height = heightPx, + scale = scaleX, + density = scope.density, + ), + scale = scaleX, + extent = heightPx, + ) + } + } else { + val cover = coverProgress(depth) + val post = if (gesture != null) { + val releaseProgress = gesture.progress + if (releaseProgress >= 1f) { + 1f + } else { + (((1f - cover) - releaseProgress) / (1f - releaseProgress)).coerceIn(0f, 1f) + } + } else { + 1f - cover + } + val rawTranslationX = -(1f - post) * driftPx + if (gesture != null) { + val travel = if (committing) gesture.progress else (1f - cover) + val eased = BackGestureEasing.transform(travel.coerceIn(0f, 1f)) + val liveScale = + CROSS_ACTIVITY_MIN_SCALE + (1f - CROSS_ACTIVITY_MIN_SCALE) * (1f - eased) + scaleX = snapScaleToPixelExtent( + (liveScale + (1f - liveScale) * post) * bounce, + widthPx, + ) + scaleY = scaleX + } + translationX = snapTranslationToPixelEdge(rawTranslationX, scaleX, widthPx) + translationY = snapTranslationToPixelEdge( + translation = crossActivityYShift( + gesture = gesture, + height = heightPx, + scale = scaleX, + density = scope.density, + ), + scale = scaleX, + extent = heightPx, + ) + } +} + +internal val AospNavTransition: NavTransition = navDirectionalTransition( + push = ClassicActivityOpen, + pop = ClassicActivityClose, + predictivePop = CrossActivityPredictive, +) + +private fun bounceScale(settle: NavSettle?, gesture: NavGesture?): Float { + if (settle == null || settle.phase != NavSettlePhase.Commit || gesture == null) return 1f + val factor = if (gesture.swipeEdge != NavSwipeEdge.None) 2f else 1f + val floorKick = if (gesture.progress < 0.1f) BOUNCE_MIN_KICK else 0f + val kick = (abs(settle.releaseVelocity) * 100f * (1f - CROSS_ACTIVITY_MIN_SCALE) * factor) + .coerceIn(floorKick, BOUNCE_MAX_KICK) + if (kick <= 0f) return 1f + val omega = sqrt(BOUNCE_STIFFNESS) + val omegaD = omega * sqrt(1f - BOUNCE_DAMPING * BOUNCE_DAMPING) + val t = settle.elapsedMillis / 1000f + val overlay = + -(kick / omegaD) * exp(-BOUNCE_DAMPING * omega * t) * sin(omegaD * t) + return ((100f + overlay) / 100f).coerceAtMost(1f) +} + +private fun shapedTopProgress(progress: Float, gesture: NavGesture?): Float = + if (gesture == null) progress else 1f - BackGestureEasing.transform((1f - progress).coerceIn(0f, 1f)) + +private fun crossActivityYShift( + gesture: NavGesture?, + height: Float, + scale: Float, + density: Density, +): Float { + if (gesture == null || height <= 0f) return 0f + val rawDelta = gesture.touchY - gesture.initialTouchY + val half = height / 2f + val ratio = min(half, abs(rawDelta)) / half + val damped = 1f - (1f - ratio) * (1f - ratio) + val marginPx = with(density) { CrossActivityEdgeMargin.toPx() } + val maxShift = ((height - height * scale) / 2f - marginPx).coerceAtLeast(0f) + return maxShift * damped * (if (rawDelta < 0f) -1f else 1f) +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ClassicNavTransition.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ClassicNavTransition.kt new file mode 100644 index 000000000..d927c7e1a --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ClassicNavTransition.kt @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.zIndex +import top.yukonga.miuix.kmp.nav.transition.NavMotion +import top.yukonga.miuix.kmp.nav.transition.NavSettleSpec +import top.yukonga.miuix.kmp.nav.transition.NavTransition +import top.yukonga.miuix.kmp.nav.transition.NavTransitionScope +import top.yukonga.miuix.kmp.nav.transition.NavTransitions +import top.yukonga.miuix.kmp.nav.transition.navDirectionalTransition + +private val ClassicScaleMotion = NavMotion( + commit = NavSettleSpec.Tween( + durationMillis = 200, + easing = CubicBezierEasing(0.2f, 0f, 0f, 1f), + ), + cancel = NavSettleSpec.Spring(stiffness = 1500f), + programmatic = NavSettleSpec.Tween( + durationMillis = 200, + easing = CubicBezierEasing(0.2f, 0f, 0f, 1f), + ), +) + +private val ClassicScalePop: NavTransition = object : NavTransition { + override val opaqueDepth: Float = 1f + + override val motion: NavMotion = ClassicScaleMotion + + override fun scrimFraction(scope: NavTransitionScope): Float = coverProgress(scope.relativeDepth) + + override fun Modifier.transformEntry(scope: NavTransitionScope): Modifier { + val zIndex = if (scope.relativeDepth > 0f) 1f else 0f + return graphicsLayer { + val depth = scope.relativeDepth + val widthPx = scope.layoutSize.width.toFloat() + val heightPx = scope.layoutSize.height.toFloat() + if (depth <= 0f) { + val progress = topProgress(depth) + scaleX = snapScaleToPixelExtent(0.9f + 0.1f * progress, widthPx) + scaleY = scaleX + translationX = snapTranslationToPixelEdge(0f, scaleX, widthPx) + translationY = snapTranslationToPixelEdge(0f, scaleY, heightPx) + alpha = progress + } else { + translationX = snapTranslationToPixelEdge( + translation = -coverProgress(depth) * widthPx, + scale = 1f, + extent = widthPx, + ) + } + }.zIndex(zIndex) + } +} + +internal val ClassicNavTransition: NavTransition = navDirectionalTransition( + push = NavTransitions.MiuixDefault, + pop = ClassicScalePop, + predictivePop = ClassicScalePop, +) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/InstallerNavTransition.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/InstallerNavTransition.kt new file mode 100644 index 000000000..d2789f512 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/InstallerNavTransition.kt @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import com.resukisu.resukisu.ui.viewmodel.PredictiveBackAnimation +import com.resukisu.resukisu.ui.viewmodel.PredictiveBackExitDirection +import top.yukonga.miuix.kmp.nav.transition.NavTransition +import top.yukonga.miuix.kmp.nav.transition.NavTransitions + +fun installerNavTransition( + animation: PredictiveBackAnimation, + exitDirection: PredictiveBackExitDirection, +): NavTransition = when (animation) { + PredictiveBackAnimation.None -> NoPredictiveBackTransition + PredictiveBackAnimation.MIUIX -> NavTransitions.MiuixDefault + PredictiveBackAnimation.AOSP -> AospNavTransition + PredictiveBackAnimation.Scale -> scaleNavTransition(exitDirection) + PredictiveBackAnimation.KernelSUClassic -> ClassicNavTransition +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/KernelSUClassicPredictiveBackAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/KernelSUClassicPredictiveBackAnimation.kt deleted file mode 100644 index 29145fcff..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/KernelSUClassicPredictiveBackAnimation.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigationevent.NavigationEventTransitionState - -class KernelSUClassicPredictiveBackAnimation : PredictiveBackAnimationHandler { - override suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey? - ) { - // ignore - } - - @Composable - override fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier { - return this - } - - override fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - swipeEdge: Int - ): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { fullWidth -> -fullWidth }), - initialContentExit = scaleOut(targetScale = 0.9f) + fadeOut(), - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { fullWidth -> -fullWidth }), - initialContentExit = scaleOut(targetScale = 0.9f) + fadeOut(), - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { fullWidth -> fullWidth }), - initialContentExit = slideOutHorizontally(targetOffsetX = { fullWidth -> -fullWidth }), - sizeTransform = null - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/MiuixPredictiveBackAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/MiuixPredictiveBackAnimation.kt deleted file mode 100644 index 5db075751..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/MiuixPredictiveBackAnimation.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigation3.ui.defaultPopTransitionSpec -import androidx.navigation3.ui.defaultPredictivePopTransitionSpec -import androidx.navigation3.ui.defaultTransitionSpec -import androidx.navigationevent.NavigationEventTransitionState - -class MiuixPredictiveBackAnimation : PredictiveBackAnimationHandler { - - override suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey? - ) { - // Deliberately empty. Predictive back gesture progress is natively handled - // and synchronized by the Compose transition engine. - } - - @Composable - override fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier { - // NavDisplay automatically handles dimming and corner clipping internally - // through NavDisplayTransitionEffects, so we return unmodified this. - return this - } - - override fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - swipeEdge: Int - ): ContentTransform = defaultPredictivePopTransitionSpec().invoke(this, swipeEdge) - - override fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform = - defaultPopTransitionSpec().invoke(this) - - override fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform = - defaultTransitionSpec().invoke(this) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionEasing.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionEasing.kt new file mode 100644 index 000000000..058c7b95a --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionEasing.kt @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.Easing + +internal val FastOutExtraSlowIn: Easing = run { + val knotX = 0.166666f + val knotY = 0.4f + val first = CubicBezierEasing(0.05f / knotX, 0f, 0.133333f / knotX, 0.06f / knotY) + val second = CubicBezierEasing( + (0.208333f - knotX) / (1f - knotX), + (0.82f - knotY) / (1f - knotY), + (0.25f - knotX) / (1f - knotX), + (1f - knotY) / (1f - knotY), + ) + Easing { fraction -> + if (fraction < knotX) { + knotY * first.transform(fraction / knotX) + } else { + knotY + (1f - knotY) * second.transform((fraction - knotX) / (1f - knotX)) + } + } +} + +internal val BackGestureEasing: Easing = CubicBezierEasing(0.1f, 0.1f, 0f, 1f) + +internal fun topProgress(depth: Float): Float = (1f + depth).coerceIn(0f, 1f) + +internal fun coverProgress(depth: Float): Float = depth.coerceIn(0f, 1f) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionGeometry.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionGeometry.kt new file mode 100644 index 000000000..df3f7a8fc --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NavTransitionGeometry.kt @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import kotlin.math.roundToInt + +internal fun snapScaleToPixelExtent(scale: Float, extent: Float): Float = + if (extent > 0f) (scale * extent).roundToInt() / extent else scale + +internal fun snapTranslationToPixelEdge( + translation: Float, + scale: Float, + extent: Float, + pivotFraction: Float = 0.5f, +): Float { + if (extent <= 0f) return translation + val scaledEdgeOffset = extent * pivotFraction * (1f - scale) + return (translation + scaledEdgeOffset).roundToInt() - scaledEdgeOffset +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackAnimation.kt deleted file mode 100644 index 0a3cb2fa8..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackAnimation.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigation3.ui.defaultPopTransitionSpec -import androidx.navigation3.ui.defaultTransitionSpec -import androidx.navigationevent.NavigationEventTransitionState -import com.resukisu.resukisu.ui.navigation.LocalNavigator - -class NoPredictiveBackAnimation : PredictiveBackAnimationHandler { - override suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey? - ) { - // Ignore predictive back gesture progress completely. - } - - @Composable - override fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier { - val navigator = LocalNavigator.current - - // Determine if there are pages to pop. - val canPop = navigator.backStack.size > 1 - - // Only intercept the back button when we can actually pop. - // If enabled is false, the system handles the back press (e.g., exits the Activity). - // Using BackHandler here completely intercepts the system predictive back dispatch, - // preventing the predictive gesture from starting. - BackHandler(enabled = canPop) { - navigator.pop() - } - - return this - } - - override fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - swipeEdge: Int - ): ContentTransform = ContentTransform( - // Keep predictive pop transition empty since it's disabled by BackHandler anyway. - targetContentEnter = EnterTransition.None, - initialContentExit = ExitTransition.None, - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform = - // Sync with the default pop transition used in Miuix implementation - defaultPopTransitionSpec().invoke(this) - - override fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform = - // Sync with the default push transition used in Miuix implementation - defaultTransitionSpec().invoke(this) -} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackTransition.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackTransition.kt new file mode 100644 index 000000000..c16600655 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/NoPredictiveBackTransition.kt @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-only +// Copyright (C) 2026 ReSukiSU contributors +package com.resukisu.resukisu.ui.animation.predictiveback + +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.util.fastRoundToInt +import top.yukonga.miuix.kmp.nav.runtime.NavProgrammaticEasing +import top.yukonga.miuix.kmp.nav.transition.NavMotion +import top.yukonga.miuix.kmp.nav.transition.NavRole +import top.yukonga.miuix.kmp.nav.transition.NavSettle +import top.yukonga.miuix.kmp.nav.transition.NavSettlePhase +import top.yukonga.miuix.kmp.nav.transition.NavSettleSpec +import top.yukonga.miuix.kmp.nav.transition.NavTransition +import top.yukonga.miuix.kmp.nav.transition.NavTransitionScope +import top.yukonga.miuix.kmp.nav.transition.NavTransitions +import top.yukonga.miuix.kmp.nav.transition.navDirectionalTransition +import top.yukonga.miuix.kmp.nav.transition.navGraphicsTransition + +private const val NO_PREDICTIVE_POP_DURATION_MILLIS = 450 + +/** + * Keeps the page at the point where back interrupted it instead of handing the in-flight push to + * predictive progress. A committed back slowly plays the page out from that point; cancellation + * lets the interrupted push finish entering. + */ +private val NoPredictivePop: NavTransition = navGraphicsTransition( + opaqueDepth = 1f, + motion = NavMotion( + commit = NavSettleSpec.Tween( + durationMillis = NO_PREDICTIVE_POP_DURATION_MILLIS, + easing = NavProgrammaticEasing, + ), + cancel = NavSettleSpec.Tween( + durationMillis = NO_PREDICTIVE_POP_DURATION_MILLIS, + easing = NavProgrammaticEasing, + ), + ), + scrim = { scope -> 1f - noPredictiveVisualProgress(scope) }, +) { scope -> + applyNoPredictiveTransform(scope, noPredictiveVisualProgress(scope)) +} + +internal val NoPredictiveBackTransition: NavTransition = navDirectionalTransition( + push = NavTransitions.MiuixDefault, + pop = NavTransitions.MiuixDefault, + predictivePop = NoPredictivePop, +) + +/** + * Reconstructs the grab anchor hidden by the shared depth driver. While the finger is active the + * visual progress stays at that anchor. Commit and cancel then animate from the anchor rather than + * from the finger's predictive progress. + */ +private fun noPredictiveVisualProgress(scope: NavTransitionScope): Float { + val gesture = scope.gesture ?: return 0f + val topDepth = when (scope.role) { + NavRole.Covered -> scope.relativeDepth - 1f + NavRole.Top if scope.settle?.phase == NavSettlePhase.Commit -> + scope.relativeDepth - 1f + + else -> scope.relativeDepth + } + val totalProgress = -topDepth + val settle = scope.settle + return when (settle?.phase) { + null -> (totalProgress - gesture.progress).coerceIn(0f, 1f) + NavSettlePhase.Commit -> { + val settleProgress = noPredictiveSettleProgress(settle) + val remaining = 1f - settleProgress + if (remaining <= 0.001f) { + 1f + } else { + val anchor = ( + (totalProgress - settleProgress) / remaining - gesture.progress + ).coerceIn(0f, 1f) + anchor + (1f - anchor) * settleProgress + } + } + + NavSettlePhase.Cancel -> { + val settleProgress = noPredictiveSettleProgress(settle) + val remaining = 1f - settleProgress + if (remaining <= 0.001f) { + 0f + } else { + val anchor = (totalProgress / remaining - gesture.progress).coerceIn(0f, 1f) + anchor * remaining + } + } + + NavSettlePhase.Programmatic -> + (totalProgress - gesture.progress).coerceIn(0f, 1f) + } +} + +private fun noPredictiveSettleProgress(settle: NavSettle): Float { + val fraction = (settle.elapsedMillis / NO_PREDICTIVE_POP_DURATION_MILLIS).coerceIn(0f, 1f) + return NavProgrammaticEasing.transform(fraction).coerceIn(0f, 1f) +} + +private fun GraphicsLayerScope.applyNoPredictiveTransform( + scope: NavTransitionScope, + progress: Float, +) { + val widthPx = scope.layoutSize.width.toFloat() + val direction = if (scope.layoutDirection == LayoutDirection.Rtl) -1f else 1f + val isLowerEntry = scope.role == NavRole.Covered || + scope.role == NavRole.Top && scope.settle?.phase == NavSettlePhase.Commit + if (isLowerEntry) { + translationX = -direction * (1f - progress) * widthPx * 0.25f + alpha = 0.9f + 0.1f * progress + } else { + translationX = (direction * progress * widthPx).fastRoundToInt().toFloat() + } +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/PredictiveBackAnimationHandler.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/PredictiveBackAnimationHandler.kt deleted file mode 100644 index 28d431bd9..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/PredictiveBackAnimationHandler.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigationevent.NavigationEvent -import androidx.navigationevent.NavigationEventTransitionState -import kotlinx.coroutines.CoroutineScope - -interface PredictiveBackAnimationHandler { - /** - * Callback invoked when the back event is committed (e.g., gesture completed or button clicked). - * - * **Implementation Requirements:** - * - Implementation must check the current state of [transitionState]. - * - If a predictive back animation is active (in-progress), this method must play the animations - * to avoid page disappear without any Exit animations - * - This serves as the terminal lifecycle hook before the Navigation Manager - * officially removes the page from the backstack. - * - * @param transitionState The state tracking the current predictive back gesture/animation. - * @param currentPageKey The [NavKey] of the page currently being popped. - */ - suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey?, - ) - - /** - * Callback when page actually pop - * - * **NOTE:** the page will pop from view tree IMMEDIATELY - * after this callback completed - * - * @param contentPageKey The [NavKey] of the page being pop. - * @param animationScope An [CoroutineScope] for reset animation status ONLY - */ - fun onPagePop( - contentPageKey: Any, - animationScope: CoroutineScope - ) { - } - - /** - * A UI decorator applied to every page during the rendering process. - * * Allows for custom modifications to the page layout or graphics layer. - * - * @param transitionState The current state of the predictive back transition. - * @param contentPageKey The [NavKey] of the page being decorated. - * @param currentPageKey The [NavKey]'s toString of the page currently at the top of the stack. - * @return the Modifier will apply to the Box of the content - */ - @Composable - fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier - - /** - * Defines the transition specs specifically for a predictive back (swipe) gesture. - * @param swipeEdge The edge from which the swipe gesture originated (Left or Right). - * @return A [ContentTransform] defining the enter/exit animations. - */ - fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - @NavigationEvent.SwipeEdge swipeEdge: Int - ): ContentTransform - - /** - * Defines the transition specs for a standard pop navigation (e.g., non-gesture back). - */ - fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform - - /** - * Defines the default transition specs for forward navigation (push). - */ - fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScaleNavTransition.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScaleNavTransition.kt new file mode 100644 index 000000000..a2a0636a4 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScaleNavTransition.kt @@ -0,0 +1,122 @@ +package com.resukisu.resukisu.ui.animation.predictiveback + +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.ui.graphics.TransformOrigin +import com.resukisu.resukisu.ui.viewmodel.PredictiveBackExitDirection +import top.yukonga.miuix.kmp.nav.transition.NavGesture +import top.yukonga.miuix.kmp.nav.transition.NavMotion +import top.yukonga.miuix.kmp.nav.transition.NavRole +import top.yukonga.miuix.kmp.nav.transition.NavSettlePhase +import top.yukonga.miuix.kmp.nav.transition.NavSettleSpec +import top.yukonga.miuix.kmp.nav.transition.NavSwipeEdge +import top.yukonga.miuix.kmp.nav.transition.NavTransition +import top.yukonga.miuix.kmp.nav.transition.NavTransitionScope +import top.yukonga.miuix.kmp.nav.transition.NavTransitions +import top.yukonga.miuix.kmp.nav.transition.navDirectionalTransition +import top.yukonga.miuix.kmp.nav.transition.navGraphicsTransition + +private val ScaleExitMotion = NavMotion( + commit = NavSettleSpec.Tween( + durationMillis = 200, + easing = FastOutSlowInEasing, + ), + cancel = NavSettleSpec.Spring(stiffness = 1500f), + programmatic = NavSettleSpec.Tween( + durationMillis = 200, + easing = CubicBezierEasing(0.2f, 0f, 0f, 1f), + ), +) + +internal fun scaleNavTransition(exitDirection: PredictiveBackExitDirection): NavTransition { + val pop = navGraphicsTransition( + opaqueDepth = 1f, + motion = ScaleExitMotion, + scrim = { scope -> + when { + scope.settle?.phase == NavSettlePhase.Commit -> + (1f - (scope.settle?.elapsedMillis ?: 0f) / 200) + .coerceIn(0f, 1f) + + scope.gesture != null -> 1f + else -> coverProgress(scope.relativeDepth) + } + }, + ) { scope -> + val depth = scope.relativeDepth + val widthPx = scope.layoutSize.width.toFloat() + val heightPx = scope.layoutSize.height.toFloat() + val gesture = scope.gesture + val sign = exitDirectionSign(exitDirection, scope) + val committing = scope.settle?.phase == NavSettlePhase.Commit + val outgoingCommit = scope.role == NavRole.Outgoing && committing && gesture != null + if (depth <= 0f) { + val progress = topProgress(depth) + val pageScale = if (outgoingCommit) { + val releaseProgress = (1f - gesture.progress).coerceAtLeast(0.01f) + val post = (1f - progress / releaseProgress).coerceIn(0f, 1f) + val releaseEasedProgress = shapedTopProgress(releaseProgress, gesture) + val committedScale = 0.85f + (1f - 0.85f) * releaseEasedProgress + committedScale + (0.85f - committedScale) * post + } else { + val easedProgress = shapedTopProgress(progress, gesture) + 0.85f + (1f - 0.85f) * easedProgress + } + val pivotX = if (gesture?.swipeEdge == NavSwipeEdge.Left) 0.8f else 0.2f + val pivotY = gesturePivotY(gesture, heightPx) + scaleX = snapScaleToPixelExtent(pageScale, widthPx) + scaleY = scaleX + transformOrigin = TransformOrigin( + pivotFractionX = pivotX, + pivotFractionY = pivotY, + ) + val rawTranslationX = if (gesture != null && scope.settle == null) { + 0f + } else if (outgoingCommit) { + val releaseProgress = (1f - gesture.progress).coerceAtLeast(0.01f) + val post = (1f - progress / releaseProgress).coerceIn(0f, 1f) + sign * post * widthPx + } else { + sign * (1f - progress) * widthPx + } + translationX = snapTranslationToPixelEdge( + translation = rawTranslationX, + scale = scaleX, + extent = widthPx, + pivotFraction = pivotX, + ) + translationY = snapTranslationToPixelEdge( + translation = 0f, + scale = scaleY, + extent = heightPx, + pivotFraction = pivotY, + ) + } + } + return navDirectionalTransition( + push = NavTransitions.MiuixDefault, + pop = pop, + predictivePop = pop, + ) +} + +private fun shapedTopProgress(progress: Float, gesture: NavGesture?): Float = + if (gesture == null) progress else 1f - BackGestureEasing.transform((1f - progress).coerceIn(0f, 1f)) + +private fun exitDirectionSign( + direction: PredictiveBackExitDirection, + scope: NavTransitionScope, +): Float = when (direction) { + PredictiveBackExitDirection.FOLLOW_GESTURE -> + if (scope.gesture?.swipeEdge == NavSwipeEdge.Left) 1f else -1f + + PredictiveBackExitDirection.ALWAYS_RIGHT -> 1f + PredictiveBackExitDirection.ALWAYS_LEFT -> -1f +} + +private fun gesturePivotY(gesture: NavGesture?, height: Float): Float = + if (gesture != null && height > 0f) { + (gesture.touchY / height).coerceIn(0.1f, 0.9f) + } else { + 0.5f + } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScalePredictiveBackAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScalePredictiveBackAnimation.kt deleted file mode 100644 index 4fdbdb0f0..000000000 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/animation/predictiveback/ScalePredictiveBackAnimation.kt +++ /dev/null @@ -1,195 +0,0 @@ -package com.resukisu.resukisu.ui.animation.predictiveback - -import androidx.compose.animation.AnimatedContentTransitionScope -import androidx.compose.animation.ContentTransform -import androidx.compose.animation.EnterExitState -import androidx.compose.animation.EnterTransition -import androidx.compose.animation.ExitTransition -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalWindowInfo -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.scene.Scene -import androidx.navigation3.ui.LocalNavAnimatedContentScope -import androidx.navigationevent.NavigationEvent.Companion.EDGE_LEFT -import androidx.navigationevent.NavigationEventTransitionState -import androidx.navigationevent.NavigationEventTransitionState.InProgress -import com.resukisu.resukisu.ui.util.rememberDeviceCornerRadius -import com.resukisu.resukisu.ui.viewmodel.PredictiveBackExitDirection -import kotlinx.coroutines.CoroutineScope - -class ScalePredictiveBackAnimation( - private val exitDirection: PredictiveBackExitDirection = PredictiveBackExitDirection.ALWAYS_RIGHT -) : PredictiveBackAnimationHandler { - private var exitingPageKey: String? = null - private val exitAnimatable = Animatable(0f) - private var inPredictiveBackAnimation = false - - override suspend fun onBackPressed( - transitionState: NavigationEventTransitionState?, - currentPageKey: NavKey?, - ) { - if (inPredictiveBackAnimation && transitionState is InProgress) { - exitingPageKey = currentPageKey.toString() - exitAnimatable.animateTo( - targetValue = 1f, - animationSpec = tween( - durationMillis = 200, - easing = FastOutSlowInEasing - ) - ) - exitAnimatable.snapTo(0f) - } - } - - override fun onPagePop(contentPageKey: Any, animationScope: CoroutineScope) { - if (exitingPageKey == contentPageKey) { - exitingPageKey = null - } - } - - @Composable - override fun Modifier.predictiveBackAnimationDecorator( - transitionState: NavigationEventTransitionState?, - contentPageKey: Any, - currentPageKey: NavKey?, - ): Modifier { - val windowInfo = LocalWindowInfo.current - val navContent = LocalNavAnimatedContentScope.current - - val containerHeightPx = windowInfo.containerSize.height - val containerWidthPx = windowInfo.containerSize.width.toFloat() - val pageKey = contentPageKey.toString() - val transition = navContent.transition - val deviceCornerRadius = rememberDeviceCornerRadius() - - val modifier = - if (pageKey == currentPageKey.toString() || exitingPageKey == pageKey) { - // Calculate the page scale - val animatedScale by transition.animateFloat( - transitionSpec = { tween(300) }, - label = "PredictiveScale" - ) { state -> - when (state) { - EnterExitState.PostExit -> 0.85f - else -> 1f - } - } - - // navigation 3 break transition.targetState - // its state management is fully shit - // racing racing racing - // fuck fuck fuck - // so, We can't use LaunchedEffect to process that - // Just check transition.animateFloat's result to know currentStatus - inPredictiveBackAnimation = animatedScale != 1f - - // calculate WHERE is the scaled page - val progressInProgress = (transitionState as? InProgress) - val edge = progressInProgress?.latestEvent?.swipeEdge ?: 0 - val touchY = progressInProgress?.latestEvent?.touchY - - // scaled card Y calculation based on touch point - val currentPivotY = if (touchY != null && containerHeightPx > 0) { - (touchY / containerHeightPx).coerceIn(0.1f, 0.9f) - } else 0.5f - - // if the navigation gesture originates from the left edge, we let it scale to right - // otherwise, scale to left - val currentPivotX = if (edge == EDGE_LEFT) 0.8f else 0.2f - - // From the user settings, we use follow_gesture/right/left for the card's exit animation? - val directionMultiplier = when (exitDirection) { - // When user choice follow_gesture, we use this logic for calc them - // navigation gesture left -> exit to right - // navigation gesture right -> exit to left - PredictiveBackExitDirection.FOLLOW_GESTURE -> if (edge == EDGE_LEFT) 1f else -1f - PredictiveBackExitDirection.ALWAYS_RIGHT -> 1f - PredictiveBackExitDirection.ALWAYS_LEFT -> -1f - } - - // if we are playing the exit animation, calculate the scaled Page's TranslationX in here - val exitProgress = - if (pageKey != currentPageKey.toString()) 1f else exitAnimatable.value - val animatedTranslationX = containerWidthPx * exitProgress * directionMultiplier - - // render animation - val modifier = this - .graphicsLayer { - scaleX = animatedScale - scaleY = animatedScale - translationX = animatedTranslationX - transformOrigin = TransformOrigin(currentPivotX, currentPivotY) - } - .then( - if (transitionState is InProgress) { - Modifier.clip(RoundedCornerShape(deviceCornerRadius)) - } else { - Modifier - } - ) - - modifier - } else { - // We calculate the new page's black dim alpha in here - // If we are in PredictiveBackAnimation, always 0.5f dim - // If we are playing the exit animation, dynamic calculate the dim with exit animation's progress - // If we are in interrupting animation(have backState but not rendering predictiveBackAnimation) we shouldn't play dim - // Place 1f here to let dynamicAlpha calced with 0f - // alpha = 0.5 * (1f - animationProgress) (decrease alpha when increase progress) - // so, alpha will always in 0 - 0.5f - val modifier = if (transitionState is InProgress) { - val progress = if (!inPredictiveBackAnimation) 1f else exitAnimatable.value - val dynamicAlpha = 0.5f * (1f - progress) - - this - .graphicsLayer() - .drawWithContent { - drawContent() - drawRect(color = Color.Black.copy(alpha = dynamicAlpha)) - } - } else Modifier - - modifier - } - - return modifier - } - - override fun AnimatedContentTransitionScope>.onPredictivePopTransitionSpec( - swipeEdge: Int - ): ContentTransform = ContentTransform( - targetContentEnter = EnterTransition.None, - initialContentExit = ExitTransition.None, - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onPopTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn(), - initialContentExit = scaleOut(targetScale = 0.9f) + fadeOut(), - sizeTransform = null - ) - - override fun AnimatedContentTransitionScope>.onTransitionSpec(): ContentTransform = - ContentTransform( - targetContentEnter = slideInHorizontally(initialOffsetX = { it }), - initialContentExit = fadeOut(), - sizeTransform = null - ) -} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/FloatingBottomBar.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/FloatingBottomBar.kt new file mode 100644 index 000000000..d29ca3427 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/FloatingBottomBar.kt @@ -0,0 +1,518 @@ +// Adapted from compose-miuix-ui example (IosLiquidGlassNavigationBar) — Apache 2.0. + +package com.resukisu.resukisu.ui.component + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.EaseOut +import androidx.compose.animation.core.spring +import androidx.compose.foundation.background +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.dropShadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.onClick +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.selected +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastCoerceIn +import androidx.compose.ui.util.lerp +import kotlinx.coroutines.launch +import com.resukisu.resukisu.ui.component.liquid.InnerShadow +import com.resukisu.resukisu.ui.component.liquid.innerShadow +import com.resukisu.resukisu.ui.component.liquid.lens +import com.resukisu.resukisu.ui.component.liquid.rememberCombinedBackdrop +import com.resukisu.resukisu.ui.component.liquid.vibrancy +import com.resukisu.resukisu.ui.component.miuix.animation.DampedDragAnimation +import com.resukisu.resukisu.ui.component.miuix.animation.InteractiveHighlight +import com.resukisu.resukisu.ui.theme.ThemeConfig +import com.resukisu.resukisu.ui.theme.isInDarkTheme +import com.resukisu.resukisu.ui.util.LocalBlurState +import org.koin.compose.koinInject +import top.yukonga.miuix.kmp.blur.Backdrop +import top.yukonga.miuix.kmp.blur.blur +import top.yukonga.miuix.kmp.blur.drawBackdrop +import top.yukonga.miuix.kmp.blur.highlight.BloomStroke +import top.yukonga.miuix.kmp.blur.highlight.Highlight +import top.yukonga.miuix.kmp.blur.highlight.LightPosition +import top.yukonga.miuix.kmp.blur.highlight.LightSource +import top.yukonga.miuix.kmp.blur.layerBackdrop +import top.yukonga.miuix.kmp.blur.rememberLayerBackdrop +import top.yukonga.miuix.kmp.blur.sensor.rememberDeviceTilt +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.roundToInt +import kotlin.math.sign +import kotlin.math.sin + +val LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } } + +private val iosIndicatorSpecular: Highlight = Highlight( + width = 1.dp, + alpha = 1f, + style = BloomStroke( + color = Color.White.copy(alpha = 0.12f), + innerBlurRadius = 2.0.dp, + primaryLight = LightSource( + position = LightPosition(0.5f, -0.3f, -0.05f), + color = Color.White, + intensity = 1f, + ), + secondaryLight = LightSource( + position = LightPosition(0.5f, 0.8f, -0.5f), + color = Color.White, + intensity = 0.4f, + ), + dualPeak = true, + ), +) + +// Mirrors miuix-blur HighlightStyle's LIGHT_REF — keep in sync. +private const val LIGHT_REF_X = 0.5f +private const val LIGHT_REF_Y = 0.7f +private const val GRAVITY_DIR_THRESHOLD_SQ = 0.01f // |g_xy| > 0.1, ≈ 6° tilt +private const val GRAVITY_ANGLE_STEP_RAD = (3.0 * PI / 180.0).toFloat() + +/** Tracks gravity for a `dualPeak` highlight's primary light, with an extra UV-clockwise offset on top. */ +@Composable +private fun rememberQuantizedGravityAngle(): State { + val tiltState = rememberDeviceTilt() + return remember(tiltState) { + derivedStateOf { + val tilt = tiltState.value + val magnitudeSquared = tilt.gravityX * tilt.gravityX + tilt.gravityY * tilt.gravityY + if (magnitudeSquared > GRAVITY_DIR_THRESHOLD_SQ) { + (atan2(tilt.gravityY, tilt.gravityX) / GRAVITY_ANGLE_STEP_RAD).roundToInt() * GRAVITY_ANGLE_STEP_RAD + } else { + (-PI / 2).toFloat() + } + } + } +} + +@Composable +private fun rememberGravityRotatedHighlight( + base: Highlight, + extraDegrees: Float = 0f, +): State { + val baseStyle = base.style as BloomStroke + val angle = rememberQuantizedGravityAngle() + return remember(angle, base, extraDegrees) { + derivedStateOf { + val basePrimary = baseStyle.primaryLight + val rad = angle.value + (extraDegrees * PI / 180.0).toFloat() + base.copy( + style = baseStyle.copy( + primaryLight = basePrimary.copy( + position = LightPosition( + x = LIGHT_REF_X + cos(rad), + y = LIGHT_REF_Y + sin(rad), + z = basePrimary.position.z, + ), + ), + ), + ) + } + } +} + +@Composable +fun RowScope.FloatingBottomBarItem( + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit +) { + val scale = LocalFloatingBottomBarTabScale.current + Column( + modifier + .semantics(mergeDescendants = true) { + this.selected = selected + role = Role.Tab + onClick { + onClick() + true + } + } + .onKeyEvent { event -> + val activationKey = event.key == Key.Enter || + event.key == Key.NumPadEnter || event.key == Key.Spacebar + if (activationKey) { + if (event.type == KeyEventType.KeyUp) onClick() + true + } else false + } + .focusable() + .fillMaxHeight() + .weight(1f) + .graphicsLayer { + val scale = scale() + scaleX = scale + scaleY = scale + }, + verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically), + horizontalAlignment = Alignment.CenterHorizontally, + content = content + ) +} + +@Composable +fun FloatingBottomBar( + modifier: Modifier = Modifier, + selectedIndex: Int, + onSelected: (index: Int) -> Unit, + tabsCount: Int, + isBlurEnabled: Boolean = true, + content: @Composable RowScope.((Int) -> Unit) -> Unit +) { + val themeConfig: ThemeConfig = koinInject() + val isInDark = isInDarkTheme(themeConfig.forceDarkMode) + val pillShape = remember { CircleShape } + val accentColor = MaterialTheme.colorScheme.primary + val tabContentColor = MaterialTheme.colorScheme.onSurface + val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer + val containerColor = if (isBlurEnabled) surfaceContainer.copy(alpha = 0.4f) else surfaceContainer + + val backdrop: Backdrop = LocalBlurState.current ?: rememberLayerBackdrop() + val tabsBackdrop = rememberLayerBackdrop() + val density = LocalDensity.current + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val animationScope = rememberCoroutineScope() + + var tabWidthPx by remember { mutableFloatStateOf(0f) } + var totalWidthPx by remember { mutableFloatStateOf(0f) } + + val offsetAnimation = remember { Animatable(0f) } + val rubberBandPx = with(density) { 4.dp.toPx() } + val panelOffset by remember(rubberBandPx) { + derivedStateOf { + if (totalWidthPx == 0f) { + 0f + } else { + val fraction = (offsetAnimation.value / totalWidthPx).fastCoerceIn(-1f, 1f) + rubberBandPx * fraction.sign * EaseOut.transform(abs(fraction)) + } + } + } + + var currentIndex by remember { mutableIntStateOf(selectedIndex) } + val onSelectedUpdated by rememberUpdatedState(onSelected) + + fun indexAt(positionX: Float): Int { + if (tabWidthPx == 0f) return currentIndex + val horizontalPaddingPx = with(density) { 4.dp.toPx() } + val logicalX = if (isLtr) positionX else totalWidthPx - positionX + return ((logicalX - horizontalPaddingPx) / tabWidthPx) + .toInt() + .coerceIn(0, tabsCount - 1) + } + + val dampedDragAnimation = remember(animationScope, tabsCount, density, isLtr) { + DampedDragAnimation( + animationScope = animationScope, + initialValue = selectedIndex.toFloat(), + valueRange = 0f..(tabsCount - 1).toFloat(), + visibilityThreshold = 0.001f, + initialScale = 1f, + pressedScale = 78f / 56f, + canDrag = { offset -> + offset.x in 0f..totalWidthPx + }, + onDragStarted = { position -> + updateValue(indexAt(position.x).toFloat()) + }, + onDragStopped = { + val targetIndex = targetValue.roundToInt().coerceIn(0, tabsCount - 1) + if (currentIndex != targetIndex) { + currentIndex = targetIndex + onSelectedUpdated(targetIndex) + } + updateValue(targetIndex.toFloat()) + animationScope.launch { + offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f)) + } + }, + onDragCancelled = { + updateValue(currentIndex.toFloat()) + animationScope.launch { + offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f)) + } + }, + onDrag = { _, dragAmount -> + if (tabWidthPx > 0f && dragAmount.x != 0f) { + updateValue( + (targetValue + dragAmount.x / tabWidthPx * if (isLtr) 1f else -1f) + .coerceIn(0f, (tabsCount - 1).toFloat()), + ) + animationScope.launch { + offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x) + } + } + } + ) + } + + LaunchedEffect(selectedIndex) { + if (currentIndex != selectedIndex) { + currentIndex = selectedIndex + dampedDragAnimation.animateToValue(selectedIndex.toFloat()) + } + } + + fun activateTab(index: Int) { + if (index !in 0 until tabsCount) return + if (currentIndex != index) { + currentIndex = index + onSelectedUpdated(index) + } + dampedDragAnimation.animateToValue(index.toFloat()) + } + + val interactiveHighlight = remember(animationScope, tabWidthPx, dampedDragAnimation) { + InteractiveHighlight( + animationScope = animationScope, + position = { size, _ -> + Offset( + if (isLtr) (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset + else size.width - (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset, + size.height / 2f + ) + } + ) + } + + val baseHighlight = rememberGravityRotatedHighlight(iosIndicatorSpecular, extraDegrees = -45f) + val pillHighlight = rememberGravityRotatedHighlight(iosIndicatorSpecular, extraDegrees = 90f) + + val combinedBackdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop) + + Box( + modifier = modifier.width(IntrinsicSize.Min), + contentAlignment = Alignment.CenterStart + ) { + Row( + Modifier + .onGloballyPositioned { coords -> + totalWidthPx = coords.size.width.toFloat() + val contentWidthPx = totalWidthPx - with(density) { 8.dp.toPx() } + tabWidthPx = (contentWidthPx / tabsCount).coerceAtLeast(0f) + } + .selectableGroup() + .graphicsLayer { translationX = panelOffset } + .dropShadow( + shape = pillShape, + shadow = Shadow( + radius = 10.dp, + color = Color.Black, + alpha = if (isInDark) 0.2f else 0.1f, + ), + ) + .then( + if (isBlurEnabled) { + Modifier.drawBackdrop( + backdrop = backdrop, + shape = { pillShape }, + effects = { + padding = maxOf(padding, 40.dp.toPx()) + vibrancy() + blur(4.dp.toPx(), 4.dp.toPx()) + lens( + refractionHeight = 24.dp.toPx(), + refractionAmount = 24.dp.toPx(), + ) + }, + highlight = { baseHighlight.value.copy(alpha = 0.75f) }, + layerBlock = { + val width = size.width.coerceAtLeast(1f) + val s = lerp(1f, 1f + 16.dp.toPx() / width, dampedDragAnimation.pressProgress) + scaleX = s + scaleY = s + }, + onDrawSurface = { drawRect(containerColor) }, + ) + } else { + Modifier.background(containerColor, pillShape) + } + ) + .then( + if (isBlurEnabled) { + interactiveHighlight.modifier.then(interactiveHighlight.gestureModifier) + } else Modifier + ) + .then(dampedDragAnimation.modifier) + .height(64.dp) + .padding(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CompositionLocalProvider(LocalContentColor provides tabContentColor) { + content(::activateTab) + } + } + + if (isBlurEnabled) { + CompositionLocalProvider( + LocalFloatingBottomBarTabScale provides { + lerp(1f, 1.2f, dampedDragAnimation.pressProgress) + }, + LocalContentColor provides accentColor, + ) { + Row( + Modifier + .clearAndSetSemantics {} + .alpha(0f) + .layerBackdrop(tabsBackdrop) + .graphicsLayer { translationX = panelOffset } + .drawBackdrop( + backdrop = backdrop, + shape = { pillShape }, + effects = { + vibrancy() + blur(4.dp.toPx(), 4.dp.toPx()) + lens( + refractionHeight = 24.dp.toPx(), + refractionAmount = 24.dp.toPx(), + ) + }, + onDrawSurface = { drawRect(containerColor) }, + ) + .then(interactiveHighlight.modifier) + .height(56.dp) + .padding(horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + content = { content(::activateTab) } + ) + } + } + + if (tabWidthPx > 0f) { + val tabWidthDp = with(density) { tabWidthPx.toDp() } + if (isBlurEnabled) { + Box( + Modifier + .padding(horizontal = 4.dp) + .graphicsLayer { + val progressOffset = dampedDragAnimation.value * tabWidthPx + translationX = if (isLtr) progressOffset + panelOffset else -progressOffset + panelOffset + } + .drawBackdrop( + backdrop = combinedBackdrop, + shape = { pillShape }, + effects = { + val progress = dampedDragAnimation.pressProgress + lens( + refractionHeight = 10.dp.toPx() * progress, + refractionAmount = 14.dp.toPx() * progress, + depthEffect = true, + chromaticAberration = 0.5f, + ) + }, + highlight = { pillHighlight.value.copy(alpha = dampedDragAnimation.pressProgress) }, + layerBlock = { + scaleX = dampedDragAnimation.scaleX + scaleY = dampedDragAnimation.scaleY + val velocity = dampedDragAnimation.velocity / 10f + scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f) + scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f) + }, + onDrawSurface = { + val progress = dampedDragAnimation.pressProgress + drawRect( + color = if (!isInDark) Color.Black.copy(alpha = 0.1f) else Color.White.copy(alpha = 0.1f), + alpha = 1f - progress, + ) + drawRect(Color.Black.copy(alpha = 0.03f * progress)) + }, + ) + .innerShadow(shape = pillShape) { + InnerShadow( + radius = 8.dp * dampedDragAnimation.pressProgress, + color = Color.Black.copy(alpha = 0.15f), + alpha = dampedDragAnimation.pressProgress, + ) + } + .height(56.dp) + .width(tabWidthDp) + ) + } else { + Box( + Modifier + .padding(horizontal = 4.dp) + .graphicsLayer { + val progressOffset = dampedDragAnimation.value * tabWidthPx + translationX = if (isLtr) progressOffset + panelOffset else -progressOffset + panelOffset + } + .clip(pillShape) + .background(accentColor.copy(alpha = 0.15f), pillShape) + .height(56.dp) + .width(tabWidthDp), + contentAlignment = Alignment.CenterStart, + ) { + CompositionLocalProvider(LocalContentColor provides accentColor) { + Row( + Modifier + .clearAndSetSemantics {} + .wrapContentWidth(align = Alignment.Start, unbounded = true) + .requiredWidth(with(density) { (totalWidthPx - 8.dp.toPx()).toDp() }) + .height(56.dp) + .graphicsLayer { + val progressOffset = dampedDragAnimation.value * tabWidthPx + translationX = if (isLtr) -progressOffset else progressOffset + }, + verticalAlignment = Alignment.CenterVertically, + content = { content(::activateTab) }, + ) + } + } + } + } + } +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/KsuIsValidCheck.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/KsuIsValidCheck.kt index c58e32ca3..7df22f01d 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/KsuIsValidCheck.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/KsuIsValidCheck.kt @@ -8,6 +8,6 @@ inline fun KsuIsValid( status: KernelStatus, content: @Composable () -> Unit ) { - if (status.isValid) + if (status.isFullFeatured) content() } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/SearchBar.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/SearchBar.kt index 6f15b45b0..1eceecfc8 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/SearchBar.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/SearchBar.kt @@ -478,10 +478,10 @@ fun SearchAppBar( windowInsets = TopAppBarDefaults.windowInsets.add(WindowInsets(left = 12.dp)), colors = TopAppBarDefaults.topAppBarColors( containerColor = - if (themeConfig.isEnableBlurExp) Color.Transparent + if (themeConfig.isEnableBlur) Color.Transparent else MaterialTheme.colorScheme.surfaceContainer.copy(alpha = cardConfig.cardAlpha), scrolledContainerColor = - if (themeConfig.isEnableBlurExp) Color.Transparent + if (themeConfig.isEnableBlur) Color.Transparent else MaterialTheme.colorScheme.surfaceContainer.copy(alpha = cardConfig.cardAlpha), ), ) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/CombinedBackdrop.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/CombinedBackdrop.kt new file mode 100644 index 000000000..e5706312c --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/CombinedBackdrop.kt @@ -0,0 +1,39 @@ +// Adapted from Kyant0/AndroidLiquidGlass — https://github.com/Kyant0/AndroidLiquidGlass (Apache 2.0). +// Mirrored from compose-miuix-ui example. + +package com.resukisu.resukisu.ui.component.liquid + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.GraphicsLayerScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.unit.Density +import top.yukonga.miuix.kmp.blur.Backdrop + +@Stable +class CombinedBackdrop( + val first: Backdrop, + val second: Backdrop, +) : Backdrop { + + override val isCoordinatesDependent: Boolean = first.isCoordinatesDependent || second.isCoordinatesDependent + + override val offsetResidualX: Float get() = first.offsetResidualX + override val offsetResidualY: Float get() = first.offsetResidualY + + override fun DrawScope.drawBackdrop( + density: Density, + coordinates: LayoutCoordinates?, + layerBlock: (GraphicsLayerScope.() -> Unit)?, + downscaleFactor: Int, + ) { + with(first) { drawBackdrop(density, coordinates, layerBlock, downscaleFactor) } + with(second) { drawBackdrop(density, coordinates, layerBlock, downscaleFactor) } + } +} + +@Composable +fun rememberCombinedBackdrop(first: Backdrop, second: Backdrop): Backdrop = + remember(first, second) { CombinedBackdrop(first, second) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/InnerShadow.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/InnerShadow.kt new file mode 100644 index 000000000..669ab5e8d --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/InnerShadow.kt @@ -0,0 +1,160 @@ +// Adapted from Kyant0/AndroidLiquidGlass — https://github.com/Kyant0/AndroidLiquidGlass (Apache 2.0). +// Mirrored from compose-miuix-ui example. + +package com.resukisu.resukisu.ui.component.liquid + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.BlurEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.layer.CompositingStrategy +import androidx.compose.ui.graphics.layer.GraphicsLayer +import androidx.compose.ui.graphics.layer.drawLayer +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.node.requireGraphicsContext +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + +@Immutable +data class InnerShadow( + val radius: Dp = 24.dp, + val offset: DpOffset = DpOffset(0.dp, radius), + val color: Color = Color.Black.copy(alpha = 0.15f), + val alpha: Float = 1f, + val blendMode: BlendMode = DrawScope.DefaultBlendMode, +) { + companion object { + @Stable + val Default: InnerShadow = InnerShadow() + } +} + +fun Modifier.innerShadow( + shape: Shape, + shadow: () -> InnerShadow?, +): Modifier = this then InnerShadowElement(shape, shadow) + +private class InnerShadowElement( + val shape: Shape, + val shadow: () -> InnerShadow?, +) : ModifierNodeElement() { + + override fun create(): InnerShadowNode = InnerShadowNode(shape, shadow) + + override fun update(node: InnerShadowNode) { + node.shape = shape + node.shadow = shadow + node.invalidateDraw() + } + + override fun InspectorInfo.inspectableProperties() { + name = "innerShadow" + properties["shape"] = shape + properties["shadow"] = shadow + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is InnerShadowElement) return false + if (shape != other.shape) return false + if (shadow != other.shadow) return false + return true + } + + override fun hashCode(): Int { + var result = shape.hashCode() + result = 31 * result + shadow.hashCode() + return result + } +} + +private class InnerShadowNode( + var shape: Shape, + var shadow: () -> InnerShadow?, +) : Modifier.Node(), + DrawModifierNode { + + override val shouldAutoInvalidate: Boolean = false + + private var shadowLayer: GraphicsLayer? = null + private val paint = Paint() + private val clipPath = Path() + private var prevRadius = Float.NaN + + override fun ContentDrawScope.draw() { + drawContent() + + val shadow = shadow() ?: return + val layer = shadowLayer ?: return + + val radius = shadow.radius.toPx() + val offsetX = shadow.offset.x.toPx() + val offsetY = shadow.offset.y.toPx() + + val outline = shape.createOutline(size, layoutDirection, this) + clipPath.reset() + when (outline) { + is Outline.Rectangle -> clipPath.addRect(outline.rect) + is Outline.Rounded -> clipPath.addRoundRect(outline.roundRect) + is Outline.Generic -> clipPath.addPath(outline.path) + } + + paint.color = shadow.color + layer.alpha = shadow.alpha + layer.blendMode = shadow.blendMode + if (prevRadius != radius) { + layer.renderEffect = if (radius > 0f) BlurEffect(radius, radius, TileMode.Decal) else null + prevRadius = radius + } + + layer.record { + drawContext.canvas.let { canvas -> + canvas.save() + canvas.clipPath(clipPath) + canvas.drawOutline(outline, paint) + canvas.translate(offsetX, offsetY) + canvas.drawOutline(outline, ShadowMaskPaint) + canvas.translate(-offsetX, -offsetY) + canvas.restore() + } + } + + drawContext.canvas.let { canvas -> + canvas.save() + canvas.clipPath(clipPath) + drawLayer(layer) + canvas.restore() + } + } + + override fun onAttach() { + shadowLayer = requireGraphicsContext().createGraphicsLayer().apply { + compositingStrategy = CompositingStrategy.Offscreen + } + } + + override fun onDetach() { + shadowLayer?.let { layer -> + requireGraphicsContext().releaseGraphicsLayer(layer) + shadowLayer = null + } + } +} + +private val ShadowMaskPaint: Paint = Paint().apply { + blendMode = BlendMode.Clear +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Lens.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Lens.kt new file mode 100644 index 000000000..269fad265 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Lens.kt @@ -0,0 +1,216 @@ +// Adapted from Kyant0/AndroidLiquidGlass — https://github.com/Kyant0/AndroidLiquidGlass (Apache 2.0). +// Mirrored from compose-miuix-ui example. + +package com.resukisu.resukisu.ui.component.liquid + +import androidx.compose.foundation.shape.CornerBasedShape +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.util.fastCoerceAtMost +import top.yukonga.miuix.kmp.blur.BackdropEffectScope +import top.yukonga.miuix.kmp.blur.isRuntimeShaderSupported +import top.yukonga.miuix.kmp.blur.runtimeShaderEffect + +fun BackdropEffectScope.lens( + refractionHeight: Float, + refractionAmount: Float, + depthEffect: Boolean = false, + chromaticAberration: Float = 0f, +) { + if (!isRuntimeShaderSupported()) return + if (refractionHeight <= 0f || refractionAmount <= 0f) return + + if (padding < refractionAmount) { + padding = refractionAmount + } + + val radii = roundedRectCornerRadii() ?: return + + val dispersionEnabled = chromaticAberration > 0f + val shaderString = + if (dispersionEnabled) { + ROUNDED_RECT_REFRACTION_WITH_DISPERSION_SHADER + } else { + ROUNDED_RECT_REFRACTION_SHADER + } + val key = if (dispersionEnabled) "LiquidGlassLensDispersion" else "LiquidGlassLens" + + val sf = downscaleFactor.coerceAtLeast(1).toFloat() + val scaledSizeW = size.width / sf + val scaledSizeH = size.height / sf + val scaledPadding = padding / sf + val scaledRefractionHeight = refractionHeight / sf + val scaledRefractionAmount = refractionAmount / sf + val scaledRadii = FloatArray(radii.size) { radii[it] / sf } + + runtimeShaderEffect( + key = key, + shaderString = shaderString, + uniformShaderName = "content", + ) { + setFloatUniform("size", scaledSizeW, scaledSizeH) + setFloatUniform("offset", -scaledPadding, -scaledPadding) + setFloatUniform("cornerRadii", scaledRadii) + setFloatUniform("refractionHeight", scaledRefractionHeight) + setFloatUniform("refractionAmount", -scaledRefractionAmount) + setFloatUniform("depthEffect", if (depthEffect) 1f else 0f) + if (dispersionEnabled) { + setFloatUniform("chromaticAberration", chromaticAberration) + } + } +} + +private fun BackdropEffectScope.roundedRectCornerRadii(): FloatArray? { + val cornerShape = shape as? CornerBasedShape ?: return null + val sizePx = size + val maxRadius = sizePx.minDimension / 2f + val isLtr = layoutDirection == LayoutDirection.Ltr + val topLeft = if (isLtr) cornerShape.topStart.toPx(sizePx, this) else cornerShape.topEnd.toPx(sizePx, this) + val topRight = if (isLtr) cornerShape.topEnd.toPx(sizePx, this) else cornerShape.topStart.toPx(sizePx, this) + val bottomRight = if (isLtr) cornerShape.bottomEnd.toPx(sizePx, this) else cornerShape.bottomStart.toPx(sizePx, this) + val bottomLeft = if (isLtr) cornerShape.bottomStart.toPx(sizePx, this) else cornerShape.bottomEnd.toPx(sizePx, this) + return floatArrayOf( + topLeft.fastCoerceAtMost(maxRadius), + topRight.fastCoerceAtMost(maxRadius), + bottomRight.fastCoerceAtMost(maxRadius), + bottomLeft.fastCoerceAtMost(maxRadius), + ) +} + +private const val ROUNDED_RECT_SDF = """ +float radiusAt(float2 coord, float4 radii) { + if (coord.x >= 0.0) { + if (coord.y <= 0.0) return radii.y; + else return radii.z; + } else { + if (coord.y <= 0.0) return radii.x; + else return radii.w; + } +} + +float sdRoundedRect(float2 coord, float2 halfSize, float radius) { + float2 cornerCoord = abs(coord) - (halfSize - float2(radius)); + float outside = length(max(cornerCoord, 0.0)) - radius; + float inside = min(max(cornerCoord.x, cornerCoord.y), 0.0); + return outside + inside; +} + +float2 gradSdRoundedRect(float2 coord, float2 halfSize, float radius) { + float2 cornerCoord = abs(coord) - (halfSize - float2(radius)); + if (cornerCoord.x >= 0.0 || cornerCoord.y >= 0.0) { + return sign(coord) * normalize(max(cornerCoord, 0.0)); + } else { + float gradX = step(cornerCoord.y, cornerCoord.x); + return sign(coord) * float2(gradX, 1.0 - gradX); + } +} +""" + +private const val ROUNDED_RECT_REFRACTION_SHADER = """ +uniform shader content; + +uniform float2 size; +uniform float2 offset; +uniform float4 cornerRadii; +uniform float refractionHeight; +uniform float refractionAmount; +uniform float depthEffect; + +$ROUNDED_RECT_SDF + +float circleMap(float x) { + return 1.0 - sqrt(1.0 - x * x); +} + +half4 main(float2 coord) { + float2 halfSize = size * 0.5; + float2 centeredCoord = (coord + offset) - halfSize; + float radius = radiusAt(coord, cornerRadii); + + float sd = sdRoundedRect(centeredCoord, halfSize, radius); + if (-sd >= refractionHeight) { + return content.eval(coord); + } + sd = min(sd, 0.0); + + float d = circleMap(1.0 - -sd / refractionHeight) * refractionAmount; + float gradRadius = min(radius * 1.5, min(halfSize.x, halfSize.y)); + float2 grad = normalize(gradSdRoundedRect(centeredCoord, halfSize, gradRadius) + depthEffect * normalize(centeredCoord)); + + float2 refractedCoord = coord + d * grad; + return content.eval(refractedCoord); +} +""" + +private const val ROUNDED_RECT_REFRACTION_WITH_DISPERSION_SHADER = """ +uniform shader content; + +uniform float2 size; +uniform float2 offset; +uniform float4 cornerRadii; +uniform float refractionHeight; +uniform float refractionAmount; +uniform float depthEffect; +uniform float chromaticAberration; + +$ROUNDED_RECT_SDF + +float circleMap(float x) { + return 1.0 - sqrt(1.0 - x * x); +} + +half4 main(float2 coord) { + float2 halfSize = size * 0.5; + float2 centeredCoord = (coord + offset) - halfSize; + float radius = radiusAt(coord, cornerRadii); + + float sd = sdRoundedRect(centeredCoord, halfSize, radius); + if (-sd >= refractionHeight) { + return content.eval(coord); + } + sd = min(sd, 0.0); + + float d = circleMap(1.0 - -sd / refractionHeight) * refractionAmount; + float gradRadius = min(radius * 1.5, min(halfSize.x, halfSize.y)); + float2 grad = normalize(gradSdRoundedRect(centeredCoord, halfSize, gradRadius) + depthEffect * normalize(centeredCoord)); + + float2 refractedCoord = coord + d * grad; + float dispersionIntensity = chromaticAberration * ((centeredCoord.x * centeredCoord.y) / (halfSize.x * halfSize.y)); + float2 dispersedCoord = d * grad * dispersionIntensity; + + half4 color = half4(0.0); + + half4 red = content.eval(refractedCoord + dispersedCoord); + color.r += red.r / 3.5; + color.a += red.a / 7.0; + + half4 orange = content.eval(refractedCoord + dispersedCoord * (2.0 / 3.0)); + color.r += orange.r / 3.5; + color.g += orange.g / 7.0; + color.a += orange.a / 7.0; + + half4 yellow = content.eval(refractedCoord + dispersedCoord * (1.0 / 3.0)); + color.r += yellow.r / 3.5; + color.g += yellow.g / 3.5; + color.a += yellow.a / 7.0; + + half4 green = content.eval(refractedCoord); + color.g += green.g / 3.5; + color.a += green.a / 7.0; + + half4 cyan = content.eval(refractedCoord - dispersedCoord * (1.0 / 3.0)); + color.g += cyan.g / 3.5; + color.b += cyan.b / 3.0; + color.a += cyan.a / 7.0; + + half4 blue = content.eval(refractedCoord - dispersedCoord * (2.0 / 3.0)); + color.b += blue.b / 3.0; + color.a += blue.a / 7.0; + + half4 purple = content.eval(refractedCoord - dispersedCoord); + color.r += purple.r / 7.0; + color.b += purple.b / 3.0; + color.a += purple.a / 7.0; + + return color; +} +""" diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Vibrancy.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Vibrancy.kt new file mode 100644 index 000000000..e32c17fd2 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/liquid/Vibrancy.kt @@ -0,0 +1,15 @@ +// Adapted from Kyant0/AndroidLiquidGlass — https://github.com/Kyant0/AndroidLiquidGlass (Apache 2.0). +// Mirrored from compose-miuix-ui example. + +package com.resukisu.resukisu.ui.component.liquid + +import top.yukonga.miuix.kmp.blur.BackdropEffectScope +import top.yukonga.miuix.kmp.blur.colorControls + +fun BackdropEffectScope.vibrancy() { + colorControls( + brightness = 0f, + contrast = 1f, + saturation = 1.5f, + ) +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/DampedDragAnimation.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/DampedDragAnimation.kt new file mode 100644 index 000000000..d4f50e74f --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/DampedDragAnimation.kt @@ -0,0 +1,157 @@ +package com.resukisu.resukisu.ui.component.miuix.animation + +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.spring +import androidx.compose.foundation.MutatorMutex +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.input.pointer.util.VelocityTracker +import androidx.compose.ui.unit.IntSize +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Job +import kotlinx.coroutines.android.awaitFrame +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import com.resukisu.resukisu.ui.component.miuix.modifier.inspectDragGestures +import kotlin.math.abs +import kotlin.time.TimeSource + +class DampedDragAnimation( + private val animationScope: CoroutineScope, + val initialValue: Float, + val valueRange: ClosedRange, + val visibilityThreshold: Float, + val initialScale: Float, + val pressedScale: Float, + val canDrag: (Offset) -> Boolean = { true }, + val onDragStarted: DampedDragAnimation.(position: Offset) -> Unit, + val onDragStopped: DampedDragAnimation.() -> Unit, + val onDragCancelled: DampedDragAnimation.() -> Unit = onDragStopped, + val onDrag: DampedDragAnimation.(size: IntSize, dragAmount: Offset) -> Unit, +) { + + private val valueAnimationSpec = + spring(1f, 1000f, visibilityThreshold) + private val velocityAnimationSpec = + spring(0.5f, 300f, visibilityThreshold * 10f) + private val pressProgressAnimationSpec = + spring(1f, 1000f, 0.001f) + private val scaleXAnimationSpec = + spring(0.6f, 250f, 0.001f) + private val scaleYAnimationSpec = + spring(0.7f, 250f, 0.001f) + + private val valueAnimation = + Animatable(initialValue, visibilityThreshold) + private val velocityAnimation = + Animatable(0f, 5f) + private val pressProgressAnimation = + Animatable(0f, 0.001f) + private val scaleXAnimation = + Animatable(initialScale, 0.001f) + private val scaleYAnimation = + Animatable(initialScale, 0.001f) + + private val mutatorMutex = MutatorMutex() + + private var pressJob: Job? = null + private var releaseJob: Job? = null + + private val velocityTracker = VelocityTracker() + private val startMark = TimeSource.Monotonic.markNow() + + val value: Float get() = valueAnimation.value + val targetValue: Float get() = valueAnimation.targetValue + val pressProgress: Float get() = pressProgressAnimation.value + val scaleX: Float get() = scaleXAnimation.value + val scaleY: Float get() = scaleYAnimation.value + val velocity: Float get() = velocityAnimation.value + + val modifier: Modifier = Modifier.pointerInput(Unit) { + inspectDragGestures( + onDragStart = { down -> + onDragStarted(down.position) + press() + }, + onDragEnd = { + onDragStopped() + release() + }, + onDragCancel = { + onDragCancelled() + release() + } + ) { change, dragAmount -> + val position = change.position + val previousPosition = change.previousPosition + + val isInside = canDrag(position) + val wasInside = canDrag(previousPosition) + + if (isInside && wasInside) { + onDrag(size, dragAmount) + } + } + } + + fun press() { + releaseJob?.cancel() + pressJob?.cancel() + velocityTracker.resetTracking() + pressJob = animationScope.launch { + launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) } + launch { scaleXAnimation.animateTo(pressedScale, scaleXAnimationSpec) } + launch { scaleYAnimation.animateTo(pressedScale, scaleYAnimationSpec) } + } + } + + fun release() { + releaseJob?.cancel() + releaseJob = animationScope.launch { + awaitFrame() + if (value != targetValue) { + val threshold = (valueRange.endInclusive - valueRange.start) * 0.025f + snapshotFlow { valueAnimation.value }.first { abs(it - valueAnimation.targetValue) < threshold } + } + launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) } + launch { scaleXAnimation.animateTo(initialScale, scaleXAnimationSpec) } + launch { scaleYAnimation.animateTo(initialScale, scaleYAnimationSpec) } + } + } + + fun updateValue(value: Float) { + val targetValue = value.coerceIn(valueRange) + animationScope.launch(start = CoroutineStart.UNDISPATCHED) { + valueAnimation.animateTo(targetValue, valueAnimationSpec) { updateVelocity() } + } + } + + fun animateToValue(value: Float) { + animationScope.launch { + mutatorMutex.mutate { + press() + val targetValue = value.coerceIn(valueRange) + launch { valueAnimation.animateTo(targetValue, valueAnimationSpec) } + if (velocity != 0f) { + launch { velocityAnimation.animateTo(0f, velocityAnimationSpec) } + } + release() + } + } + } + + private fun updateVelocity() { + velocityTracker.addPosition( + startMark.elapsedNow().inWholeMilliseconds, + Offset(value, 0f), + ) + val span = (valueRange.endInclusive - valueRange.start).coerceAtLeast(1e-6f) + val targetVelocity = velocityTracker.calculateVelocity().x / span + animationScope.launch(start = CoroutineStart.UNDISPATCHED) { + velocityAnimation.snapTo(targetVelocity) + } + } +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/InteractiveHighlight.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/InteractiveHighlight.kt new file mode 100644 index 000000000..a8786b4e2 --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/animation/InteractiveHighlight.kt @@ -0,0 +1,113 @@ +package com.resukisu.resukisu.ui.component.miuix.animation + +import android.annotation.SuppressLint +import android.graphics.RuntimeShader +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.VectorConverter +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.util.fastCoerceIn +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import com.resukisu.resukisu.ui.component.miuix.modifier.inspectDragGestures +import org.intellij.lang.annotations.Language + +@SuppressLint("NewApi") +class InteractiveHighlight( + val animationScope: CoroutineScope, + val position: (size: Size, offset: Offset) -> Offset = { _, offset -> offset } +) { + + private val pressProgressAnimationSpec = + spring(0.5f, 300f, 0.001f) + private val positionAnimationSpec = + spring(0.5f, 300f, Offset.VisibilityThreshold) + + private val pressProgressAnimation = + Animatable(0f, 0.001f) + private val positionAnimation = + Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold) + + private var startPosition = Offset.Zero + val offset: Offset get() = positionAnimation.value - startPosition + + @Language("AGSL") + private val shader = + RuntimeShader( + """ + uniform float2 size; + layout(color) uniform half4 color; + uniform float radius; + uniform float2 position; + + half4 main(float2 coord) { + float dist = distance(coord, position); + float intensity = smoothstep(radius, radius * 0.5, dist); + return color * intensity; + }""" + ) + + val modifier: Modifier = + Modifier.drawWithContent { + val progress = pressProgressAnimation.value + if (progress > 0f) { + drawRect( + Color.White.copy(0.06f * progress), + blendMode = BlendMode.Plus + ) + shader.apply { + val position = position(size, positionAnimation.value) + setFloatUniform("size", size.width, size.height) + setColorUniform("color", Color.White.copy(0.12f * progress).toArgb()) + setFloatUniform("radius", size.minDimension * 1.2f) + setFloatUniform( + "position", + position.x.fastCoerceIn(0f, size.width), + position.y.fastCoerceIn(0f, size.height) + ) + } + drawRect( + ShaderBrush(shader), + blendMode = BlendMode.Plus + ) + } + + drawContent() + } + + val gestureModifier: Modifier = + Modifier.pointerInput(animationScope) { + inspectDragGestures( + onDragStart = { down -> + startPosition = down.position + animationScope.launch { + launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) } + launch { positionAnimation.snapTo(startPosition) } + } + }, + onDragEnd = { + animationScope.launch { + launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) } + launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) } + } + }, + onDragCancel = { + animationScope.launch { + launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) } + launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) } + } + } + ) { change, _ -> + animationScope.launch { positionAnimation.snapTo(change.position) } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/modifier/DragGestureInspector.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/modifier/DragGestureInspector.kt new file mode 100644 index 000000000..27c0ae7bf --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/miuix/modifier/DragGestureInspector.kt @@ -0,0 +1,84 @@ +package com.resukisu.resukisu.ui.component.miuix.modifier + +import androidx.compose.foundation.gestures.awaitEachGesture +import androidx.compose.foundation.gestures.awaitFirstDown +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.AwaitPointerEventScope +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerId +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed +import androidx.compose.ui.input.pointer.positionChange +import androidx.compose.ui.util.fastFirstOrNull + +suspend fun PointerInputScope.inspectDragGestures( + onDragStart: (down: PointerInputChange) -> Unit = {}, + onDragEnd: (change: PointerInputChange) -> Unit = {}, + onDragCancel: () -> Unit = {}, + onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit +) { + awaitEachGesture { + val initialDown = awaitFirstDown(false, PointerEventPass.Initial) + + val down = awaitFirstDown(false) + + onDragStart(down) + onDrag(initialDown, Offset.Zero) + val upEvent = + drag( + pointerId = initialDown.id, + onDrag = { onDrag(it, it.positionChange()) } + ) + if (upEvent == null) { + onDragCancel() + } else { + onDragEnd(upEvent) + } + } +} + +private suspend inline fun AwaitPointerEventScope.drag( + pointerId: PointerId, + onDrag: (PointerInputChange) -> Unit +): PointerInputChange? { + val isPointerUp = currentEvent.changes.fastFirstOrNull { it.id == pointerId }?.pressed != true + if (isPointerUp) { + return null + } + var pointer = pointerId + while (true) { + val change = awaitDragOrUp(pointer) ?: return null + if (change.isConsumed) { + return null + } + if (change.changedToUpIgnoreConsumed()) { + return change + } + onDrag(change) + pointer = change.id + } +} + +private suspend inline fun AwaitPointerEventScope.awaitDragOrUp( + pointerId: PointerId +): PointerInputChange? { + var pointer = pointerId + while (true) { + val event = awaitPointerEvent() + val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null + if (dragEvent.changedToUpIgnoreConsumed()) { + val otherDown = event.changes.fastFirstOrNull { it.pressed } + if (otherDown == null) { + return dragEvent + } else { + pointer = otherDown.id + } + } else { + val hasDragged = dragEvent.previousPosition != dragEvent.position + if (hasDragged) { + return dragEvent + } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SegmentedColumn.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SegmentedColumn.kt index 46edf7d0e..f92c9d676 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SegmentedColumn.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SegmentedColumn.kt @@ -88,6 +88,7 @@ class SegmentedColumnScope { content: @Composable (Shape) -> Unit ) { val resolvedForceFlatTop = forceFlatTop || isInsideExpandableBody + val resolvedForceFlatBottom = forceFlatBottom || isInsideExpandableBody val resolvedVisible = visible && parentVisibilityMask items.add( @@ -96,7 +97,7 @@ class SegmentedColumnScope { visible = resolvedVisible, customTopPadding = topPadding, forceFlatTop = resolvedForceFlatTop, - forceFlatBottom = forceFlatBottom, + forceFlatBottom = resolvedForceFlatBottom, content = content ) ) @@ -126,8 +127,16 @@ class SegmentedColumnScope { isInsideExpandableBody = true parentVisibilityMask = previousVisibilityMask && animatedVisibility && expanded + val headerIndex = items.lastIndex bottomContent() + if (!previousInsideBody) { + val lastGroupIndex = items.lastIndex + if (lastGroupIndex >= headerIndex) { + items[lastGroupIndex] = items[lastGroupIndex].copy(forceFlatBottom = false) + } + } + isInsideExpandableBody = previousInsideBody parentVisibilityMask = previousVisibilityMask } @@ -196,17 +205,22 @@ fun SegmentedColumn( val baseTopRadius = if (isFirst) 16.dp else 5.dp val baseBottomRadius = if (isLast) 16.dp else 5.dp - val targetTopRadius = if (itemData.forceFlatTop) 0.dp else baseTopRadius + // Blurred backgrounds must be rendered as one continuous group. Keep + // only the outer corners rounded, regardless of item-level overrides. + val forceFlatTop = + if (themeConfig.isEnableBlurExp) !isFirst else itemData.forceFlatTop + val forceFlatBottom = + if (themeConfig.isEnableBlurExp) !isLast else itemData.forceFlatBottom + + val targetTopRadius = if (forceFlatTop) 0.dp else baseTopRadius val targetBottomRadius = - if (itemData.forceFlatBottom) 0.dp else baseBottomRadius + if (forceFlatBottom) 0.dp else baseBottomRadius val isDynamicDpSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU - val currentTopRadius = if (isDynamicDpSupported) { animateDpAsState(targetTopRadius, dpSpring, label = "TopRadius").value } else targetTopRadius - val currentBottomRadius = if (isDynamicDpSupported) { animateDpAsState( targetBottomRadius, @@ -303,4 +317,4 @@ fun SegmentedColumn( } } } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt index 4d9aaf6fb..43a303a35 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsBaseWidget.kt @@ -19,8 +19,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CornerBasedShape @@ -30,11 +29,13 @@ import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemShapes +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.contentColorFor import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocal +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.compositionLocalOf @@ -48,7 +49,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.vector.ImageVector @@ -60,7 +63,9 @@ import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.resukisu.resukisu.ui.component.settings.material3internal.rememberAnimatedShape import com.resukisu.resukisu.ui.theme.CardConfig @@ -140,8 +145,14 @@ fun SettingsBaseWidget( val interactionSource = remember { MutableInteractionSource() } - val density = LocalDensity.current - val dynamicInternalPadding = (4 * density.fontScale).dp + /* + * Material 3 ListItem uses fixed 56dp/72dp minimum heights that do not shrink with fontScale, + * leaving excessive vertical space at smaller system font sizes. Recheck this workaround when + * updating Material 3 in case ListItem starts adapting its minimum height internally. + */ + val fontScale = LocalDensity.current.fontScale + val defaultMinHeight = if (description == null) 56.dp else 72.dp + val adaptiveMinHeight = (defaultMinHeight * fontScale).coerceAtLeast(48.dp) val baseShape = LocalSegmentedItemShape.current @@ -242,6 +253,32 @@ fun SettingsBaseWidget( ) } else RectangleShape + val safeClickShape = if (onClick != null || onLongClick != null) { + remember(clickShape) { + object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density, + ): Outline = clickShape.createOutline(size, layoutDirection, density) + } + } + } else { + RectangleShape + } + val listItemShapes = if (onClick != null || onLongClick != null) { + ListItemDefaults.shapes( + shape = safeClickShape, + selectedShape = safeClickShape, + pressedShape = safeClickShape, + focusedShape = safeClickShape, + hoveredShape = safeClickShape, + draggedShape = safeClickShape, + ) + } else { + shapes + } + val clipShape = if (onClick != null || onLongClick != null) { clickShape } else { @@ -249,6 +286,7 @@ fun SettingsBaseWidget( } var itemModifier = (if (fillMaxWidth) modifier.fillMaxWidth() else modifier) + .heightIn(min = adaptiveMinHeight) if (isOnBackground && themeConfig.isEnableBlurExp) itemModifier = itemModifier .clip(clipShape) @@ -295,10 +333,6 @@ fun SettingsBaseWidget( } descriptionColumnContent?.invoke(this) - - if (description != null || descriptionColumnContent != null) { - Spacer(Modifier.height(dynamicInternalPadding)) - } } } @@ -317,10 +351,6 @@ fun SettingsBaseWidget( Box( modifier = Modifier .alpha(alpha) - .padding( - top = dynamicInternalPadding, - bottom = if (description == null && descriptionColumnContent == null) dynamicInternalPadding else 0.dp - ) ) { Row( verticalAlignment = Alignment.CenterVertically @@ -337,6 +367,26 @@ fun SettingsBaseWidget( } } + // M3E ListItem has bug, supportingContent will cause RectList broken + // and cause application crash. + + // We use headlineContent + Column + supportingContent for workaround, + // Hope Google fix this problem in their new version.... + val expressiveContent: @Composable () -> Unit = { + Column { + headline() + CompositionLocalProvider( + LocalContentColor provides colors.supportingContentColor( + enabled = enabled, + selected = selected, + dragged = false, + ), + ) { + supportingContent() + } + } + } + if (onClick != null || onLongClick != null) { var touchPoint by remember { mutableStateOf(Offset.Zero) } @@ -367,13 +417,12 @@ fun SettingsBaseWidget( } else null, enabled = enabled, colors = colors, - shapes = shapes, + shapes = listItemShapes, verticalAlignment = Alignment.CenterVertically, leadingContent = finalLeadingContent, - supportingContent = supportingContent, trailingContent = trailing, interactionSource = interactionSource, - content = headline + content = expressiveContent ) } else { /* @@ -384,7 +433,6 @@ fun SettingsBaseWidget( * which incorrectly exposes the item as disabled and changes its visual state. */ ListItem( - headlineContent = headline, modifier = itemModifier .clip(baseShape) .then( @@ -394,10 +442,14 @@ fun SettingsBaseWidget( Modifier } ), + enabled = enabled, + verticalAlignment = Alignment.CenterVertically, + shapes = shapes, colors = colors, leadingContent = finalLeadingContent, - supportingContent = supportingContent, - trailingContent = trailing + trailingContent = trailing, + contentPadding = ListItemDefaults.ContentPadding, + content = expressiveContent, ) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsDropdownWidget.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsDropdownWidget.kt index e03b506c8..310ffd9c7 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsDropdownWidget.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/component/settings/SettingsDropdownWidget.kt @@ -3,11 +3,11 @@ package com.resukisu.resukisu.ui.component.settings import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.offset import androidx.compose.material3.DropdownMenuGroup -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MenuDefaults +import androidx.compose.material3.SelectableDropdownMenuItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -81,9 +81,7 @@ fun SettingsDropdownWidget( data.forEachIndexed { index, item -> val isSelected = index == choice - // Utilize the selectable variation of DropdownMenuItem - // MenuDefaults.itemShape(index, count) automatically handles the shapes - DropdownMenuItem( + SelectableDropdownMenuItem( selected = isSelected, onClick = { onChoiceChange(index) @@ -93,7 +91,7 @@ fun SettingsDropdownWidget( shapes = MenuDefaults.itemShape( index = index, count = data.size - ) + ), ) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Navigator.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Navigator.kt index 128008310..54ca8dfa2 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Navigator.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Navigator.kt @@ -3,24 +3,21 @@ package com.resukisu.resukisu.ui.navigation import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.saveable.Saver -import androidx.compose.runtime.saveable.listSaver -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.staticCompositionLocalOf -import androidx.navigation3.runtime.NavKey import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import top.yukonga.miuix.kmp.nav.core.NavBackStack +import top.yukonga.miuix.kmp.nav.core.NavKey /** * Simple navigation helper that owns a back stack and result channels. * Supports push/replace/pop/popUntil and result APIs: navigateForResult/setResult/observeResult/clearResult. */ class Navigator( - initialKey: NavKey + val backStack: NavBackStack ) { - val backStack: SnapshotStateList = mutableStateListOf(initialKey) + constructor(vararg initial: NavKey) : this(mutableStateListOf(*initial)) private val resultBus = mutableMapOf>() @@ -134,28 +131,8 @@ class Navigator( private fun ensureChannel(key: String): MutableSharedFlow { return resultBus.getOrPut(key) { MutableSharedFlow(replay = 1, extraBufferCapacity = 0) } } - - companion object { - val Saver: Saver = listSaver(save = { navigator -> - navigator.backStack.toList() - }, restore = { savedList -> - val initialKey = savedList.firstOrNull() ?: Route.Home - val navigator = Navigator(initialKey) - navigator.backStack.clear() - navigator.backStack.addAll(savedList) - navigator - }) - } -} - - -@Composable -fun rememberNavigator(startRoute: NavKey): Navigator { - return rememberSaveable(startRoute, saver = Navigator.Saver) { - Navigator(startRoute) - } } val LocalNavigator = staticCompositionLocalOf { error("LocalNavigator not provided") -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Routes.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Routes.kt index 441fe86a3..a726e4f84 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Routes.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/navigation/Routes.kt @@ -1,14 +1,15 @@ package com.resukisu.resukisu.ui.navigation import android.os.Parcelable -import androidx.navigation3.runtime.NavKey import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable +import top.yukonga.miuix.kmp.nav.core.NavKey /** - * Type-safe navigation keys for Navigation3. + * Type-safe navigation keys for Navigation. * Each destination is a NavKey (data object/data class) and can be saved/restored in the back stack. */ +@Serializable sealed interface Route : NavKey, Parcelable { @Parcelize @Serializable diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt index c59b73c7b..a9b959105 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/AppProfile.kt @@ -13,14 +13,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape @@ -39,9 +36,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBarColors import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -87,6 +82,7 @@ import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.AppProfileUiAction import com.resukisu.resukisu.ui.viewmodel.AppProfileUiEvent @@ -118,6 +114,7 @@ fun AppProfileScreen( val uiState by viewModel.state.collectAsStateWithLifecycle() val appGroup = uiState.appGroup val appLabel = appGroup?.mainApp?.label ?: packageName + val isSpecial = appGroup?.isWebViewZygote == true val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format( appLabel ) @@ -159,27 +156,37 @@ fun AppProfileScreen( colorScheme.surfaceContainer } - LaunchedEffect(Unit) { - scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit - } - Scaffold( topBar = { - TopBar( - title = appGroup.mainApp.label, - packageName = packageName, + LargeFlexibleTopAppBar( + modifier = Modifier.blurEffect(), + title = { + Text( + text = appGroup.mainApp.label, + ) + }, + subtitle = { + Text( + text = appGroup.mainApp.displayIdentifier + ) + }, colors = TopAppBarDefaults.topAppBarColors( containerColor = cardColor, scrolledContainerColor = cardColor ), - onBack = dropUnlessResumed { navigator.pop() }, + navigationIcon = { + AppBackButton( + onClick = dropUnlessResumed { navigator.pop() } + ) + }, + windowInsets = TopAppBarDefaults.windowInsets.add(WindowInsets(left = 12.dp)), scrollBehavior = scrollBehavior, ) }, snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + contentWindowInsets = adaptiveScaffoldWindowInsets() ) { paddingValues -> AppProfileInner( modifier = Modifier @@ -187,10 +194,12 @@ fun AppProfileScreen( .nestedScroll(scrollBehavior.nestedScrollConnection) .blurSource(), topPadding = paddingValues.calculateTopPadding(), + bottomPadding = paddingValues.calculateBottomPadding(), appGroup = appGroup, + isSpecial = isSpecial, appIcon = { PackageIcon( - packageName = appGroup.mainApp.packageName, + packageName = if (isSpecial) "android" else appGroup.mainApp.packageName, contentDescription = appGroup.mainApp.label, modifier = Modifier .padding(4.dp) @@ -232,7 +241,9 @@ fun AppProfileScreen( private fun AppProfileInner( modifier: Modifier = Modifier, topPadding: Dp, + bottomPadding: Dp = 0.dp, appGroup: InstalledAppGroup, + isSpecial: Boolean = false, appIcon: @Composable () -> Unit, profile: AppProfile, defaultUmountModules: Boolean = profile.umountModules, @@ -245,7 +256,7 @@ private fun AppProfileInner( ) { val cardConfig: CardConfig = koinInject() val themeConfig: ThemeConfig = koinInject() - val isRootGranted = profile.allowSu + val isRootGranted = !isSpecial && profile.allowSu val affectedApplicationsTitle = stringResource(R.string.affected_applications) LazyColumn(modifier = modifier) { @@ -254,48 +265,62 @@ private fun AppProfileInner( } item { - SettingsDropdownWidget( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - title = appGroup.mainApp.label, - description = appGroup.mainApp.packageName, - iconPlaceholder = false, - leadingContent = { - appIcon() - }, - choice = -1, - data = listOf( - stringResource(id = R.string.launch_app), - stringResource(id = R.string.force_stop_app), - stringResource(id = R.string.restart_app) + if (isSpecial) { + SettingsBaseWidget( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + title = appGroup.mainApp.label, + description = appGroup.mainApp.displayIdentifier, + iconPlaceholder = false, + leadingContent = { + appIcon() + }, ) - ) { choice -> - when (choice) { - 0 -> onControlApp(AppControlAction.LAUNCH) - 1 -> onControlApp(AppControlAction.FORCE_STOP) - 2 -> onControlApp(AppControlAction.RESTART) - else -> throw IllegalStateException("Illegal choice: $choice") + } else { + SettingsDropdownWidget( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + title = appGroup.mainApp.label, + description = appGroup.mainApp.displayIdentifier, + iconPlaceholder = false, + leadingContent = { + appIcon() + }, + choice = -1, + data = listOf( + stringResource(id = R.string.launch_app), + stringResource(id = R.string.force_stop_app), + stringResource(id = R.string.restart_app) + ) + ) { choice -> + when (choice) { + 0 -> onControlApp(AppControlAction.LAUNCH) + 1 -> onControlApp(AppControlAction.FORCE_STOP) + 2 -> onControlApp(AppControlAction.RESTART) + else -> throw IllegalStateException("Illegal choice: $choice") + } } } } - item { - Surface( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.surfaceBright.copy( - alpha = cardConfig.cardAlpha - ), - contentColor = MaterialTheme.colorScheme.onSurface, - ) - { - SettingsSwitchWidget( - icon = Icons.TwoTone.Security, - title = stringResource(id = R.string.superuser), - checked = isRootGranted, - onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, + if (!isSpecial) { + item { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.surfaceBright.copy( + alpha = cardConfig.cardAlpha + ), + contentColor = MaterialTheme.colorScheme.onSurface, ) + { + SettingsSwitchWidget( + icon = Icons.TwoTone.Security, + title = stringResource(id = R.string.superuser), + checked = isRootGranted, + onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, + ) + } } } @@ -471,7 +496,11 @@ private fun AppProfileInner( } item { - Spacer(modifier = Modifier.height(6.dp + 48.dp + 6.dp /* SnackBar height */)) + Spacer( + modifier = Modifier.height( + bottomPadding + 6.dp + 48.dp + 6.dp /* SnackBar height */ + ) + ) } } } @@ -483,39 +512,6 @@ private enum class Mode(@param:StringRes private val res: Int) { @Composable get() = stringResource(res) } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) -@Composable -private fun TopBar( - title: String, - packageName: String, - onBack: () -> Unit, - colors: TopAppBarColors, - scrollBehavior: TopAppBarScrollBehavior? = null, -) { - LargeFlexibleTopAppBar( - modifier = Modifier.blurEffect( - ), - title = { - Text( - text = title, - ) - }, - subtitle = { - Text( - text = packageName - ) - }, - colors = colors, - navigationIcon = { - AppBackButton( - onClick = onBack - ) - }, - windowInsets = TopAppBarDefaults.windowInsets.add(WindowInsets(left = 12.dp)), - scrollBehavior = scrollBehavior, - ) -} - @Composable private fun ProfileBox( mode: Mode, @@ -525,6 +521,7 @@ private fun ProfileBox( Column { SettingsBaseWidget( icon = Icons.TwoTone.AccountCircle, + iconColor = MaterialTheme.colorScheme.onSurface, title = stringResource(R.string.profile), description = mode.text, isOnBackground = false, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/DynamicManagerScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/DynamicManagerScreen.kt index e253b0ed7..f63324942 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/DynamicManagerScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/DynamicManagerScreen.kt @@ -63,6 +63,7 @@ import com.resukisu.resukisu.ui.navigation.LocalNavigator import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.DynamicManagerAppItem import com.resukisu.resukisu.ui.viewmodel.DynamicManagerOperation @@ -151,6 +152,7 @@ fun DynamicManagerScreen() { } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { SearchAppBar( title = stringResource(R.string.dynamic_manager_title), diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/ExecuteModuleAction.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/ExecuteModuleAction.kt index c0d655ee4..bdfae687e 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/ExecuteModuleAction.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/ExecuteModuleAction.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons @@ -51,6 +50,7 @@ import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.ExecuteModuleActionUiAction import com.resukisu.resukisu.ui.viewmodel.ExecuteModuleActionUiEvent @@ -141,7 +141,7 @@ fun ExecuteModuleActionScreen(moduleId: String) { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing, + contentWindowInsets = adaptiveScaffoldWindowInsets(), snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) } ) { innerPadding -> KeyEventBlocker { diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Flash.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Flash.kt index 664ec1add..56b96c74f 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Flash.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Flash.kt @@ -69,7 +69,6 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -97,6 +96,7 @@ import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.FlashUiAction import com.resukisu.resukisu.ui.viewmodel.FlashViewModel @@ -441,6 +441,7 @@ fun FlashScreen(flashIt: FlashIt) { } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { TopBar( flashUiState.flashingStatus, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Install.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Install.kt index cae45fa05..462e79557 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Install.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Install.kt @@ -91,6 +91,7 @@ import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.getCardColors import com.resukisu.resukisu.ui.theme.getCardElevation import com.resukisu.resukisu.ui.theme.renderBackgroundBlur +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.viewmodel.InstallUiEvent import com.resukisu.resukisu.ui.viewmodel.InstallViewModel import org.koin.compose.koinInject @@ -263,6 +264,7 @@ fun InstallScreen( } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { TopBar( onBack = { navigator.pop() }, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/SulogScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/SulogScreen.kt index 4d81e03aa..0b73483ee 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/SulogScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/SulogScreen.kt @@ -6,16 +6,9 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.rememberLazyListState @@ -30,7 +23,6 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.DropdownMenuGroup -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -40,6 +32,7 @@ import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuDefaults import androidx.compose.material3.Scaffold +import androidx.compose.material3.SelectableDropdownMenuItem import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -91,6 +84,7 @@ import com.resukisu.resukisu.ui.theme.CardConfig import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalBlurState +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.viewmodel.SulogActions import com.resukisu.resukisu.ui.viewmodel.SulogFileSelector import com.resukisu.resukisu.ui.viewmodel.SulogScreenState @@ -210,17 +204,17 @@ private fun SulogScreenContent( Spacer(modifier = Modifier.height(2.dp)) SulogEventFilter.entries.forEachIndexed { index, filter -> - DropdownMenuItem( + SelectableDropdownMenuItem( selected = filter in state.selectedFilters, - text = { Text(sulogFilterLabel(filter)) }, onClick = { haptic.performHapticFeedback(HapticFeedbackType.VirtualKey) actions.onToggleFilter(filter) }, + text = { Text(sulogFilterLabel(filter)) }, shapes = MenuDefaults.itemShape( index = index, count = SulogEventFilter.entries.size - ) + ), ) Spacer(modifier = Modifier.height(2.dp)) } @@ -232,7 +226,7 @@ private fun SulogScreenContent( searchBarPlaceHolderText = stringResource(R.string.sulog_search_placeholder) ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + contentWindowInsets = adaptiveScaffoldWindowInsets(), containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface ) { innerPadding -> @@ -318,13 +312,7 @@ private fun SulogScreenContent( item { Spacer( - Modifier.height( - WindowInsets.navigationBars.asPaddingValues() - .calculateBottomPadding() + - WindowInsets.captionBar.asPaddingValues() - .calculateBottomPadding() + - 16.dp - ) + Modifier.height(innerPadding.calculateBottomPadding() + 16.dp) ) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Template.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Template.kt index 14400a3cf..bd191ad8f 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Template.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/Template.kt @@ -1,8 +1,5 @@ package com.resukisu.resukisu.ui.screen -import org.koin.compose.koinInject -import com.resukisu.resukisu.ui.theme.CardConfig -import com.resukisu.resukisu.ui.theme.ThemeConfig import android.content.ClipData import android.content.ClipboardManager import android.widget.Toast @@ -19,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -67,7 +63,6 @@ import androidx.compose.ui.unit.dp import androidx.core.content.getSystemService import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.dropUnlessResumed -import org.koin.compose.viewmodel.koinViewModel import com.resukisu.resukisu.R import com.resukisu.resukisu.domain.model.ProfileTemplate import com.resukisu.resukisu.ui.component.NetworkRefreshContent @@ -77,13 +72,18 @@ import com.resukisu.resukisu.ui.component.settings.lazySegmentColumn import com.resukisu.resukisu.ui.navigation.LocalNavigator import com.resukisu.resukisu.ui.navigation.Navigator import com.resukisu.resukisu.ui.navigation.Route +import com.resukisu.resukisu.ui.theme.CardConfig +import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.ActivityResumeEffect +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets +import com.resukisu.resukisu.ui.viewmodel.TemplateUiAction import com.resukisu.resukisu.ui.viewmodel.TemplateUiEvent import com.resukisu.resukisu.ui.viewmodel.TemplateViewModel -import com.resukisu.resukisu.ui.viewmodel.TemplateUiAction import kotlinx.coroutines.launch +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinViewModel /** * @author weishu @@ -203,7 +203,7 @@ fun AppProfileTemplateScreen() { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing, + contentWindowInsets = adaptiveScaffoldWindowInsets(), ) { innerPadding -> if (uiState.templateList.isEmpty()) { LazyColumn( @@ -403,7 +403,7 @@ private fun TopBar( shapes = MenuDefaults.groupShapes() ) { DropdownMenuItem( - selected = false, + shape = MenuDefaults.itemShape(0, 2).shape, text = { Text(stringResource(id = R.string.app_profile_import_from_clipboard)) }, @@ -411,10 +411,9 @@ private fun TopBar( onImport() showDropdown = false }, - shapes = MenuDefaults.itemShape(index = 0, count = 2) ) DropdownMenuItem( - selected = false, + shape = MenuDefaults.itemShape(1, 2).shape, text = { Text(stringResource(id = R.string.app_profile_export_to_clipboard)) }, @@ -422,7 +421,6 @@ private fun TopBar( onExport() showDropdown = false }, - shapes = MenuDefaults.itemShape(index = 1, count = 2) ) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/TemplateEditor.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/TemplateEditor.kt index a497ae717..9132487bf 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/TemplateEditor.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/TemplateEditor.kt @@ -1,24 +1,21 @@ package com.resukisu.resukisu.ui.screen import android.widget.Toast -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.DeleteForever import androidx.compose.material.icons.twotone.Save +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFlexibleTopAppBar @@ -33,8 +30,8 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -45,22 +42,23 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.compose.dropUnlessResumed import com.resukisu.resukisu.Natives.Profile.RootProfileFlag import com.resukisu.resukisu.R import com.resukisu.resukisu.domain.model.AppProfile import com.resukisu.resukisu.domain.model.ProfileTemplate import com.resukisu.resukisu.toRawFlags import com.resukisu.resukisu.toRootProfileFlags -import com.resukisu.resukisu.ui.component.profile.rootProfileConfig import com.resukisu.resukisu.ui.component.NetworkRefreshContent +import com.resukisu.resukisu.ui.component.profile.rootProfileConfig import com.resukisu.resukisu.ui.component.settings.AppBackButton import com.resukisu.resukisu.ui.component.settings.SegmentedColumn import com.resukisu.resukisu.ui.component.settings.SettingsTextFieldWidget import com.resukisu.resukisu.ui.navigation.LocalNavigator import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.viewmodel.TemplateEditorUiAction import com.resukisu.resukisu.ui.viewmodel.TemplateEditorUiEvent import com.resukisu.resukisu.ui.viewmodel.TemplateEditorViewModel @@ -143,7 +141,7 @@ fun TemplateEditorScreen( scrollBehavior = scrollBehavior ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + contentWindowInsets = adaptiveScaffoldWindowInsets(), containerColor = Color.Transparent, ) { innerPadding -> LazyColumn( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/UmountManagerScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/UmountManagerScreen.kt index 3a01b8565..328d72135 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/UmountManagerScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/UmountManagerScreen.kt @@ -67,6 +67,7 @@ import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.UmountManagerScreenViewModel import com.resukisu.resukisu.ui.viewmodel.UmountManagerUiAction @@ -112,6 +113,7 @@ fun UmountManagerScreen() { } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { LargeFlexibleTopAppBar( modifier = Modifier diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/About.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/About.kt index 8dc01db22..90d537657 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/About.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/About.kt @@ -1,8 +1,5 @@ package com.resukisu.resukisu.ui.screen.about -import org.koin.compose.koinInject -import com.resukisu.resukisu.ui.theme.CardConfig -import com.resukisu.resukisu.ui.theme.ThemeConfig import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -62,9 +59,13 @@ import com.resukisu.resukisu.ui.component.settings.SettingsJumpPageWidget import com.resukisu.resukisu.ui.navigation.LocalNavigator import com.resukisu.resukisu.ui.navigation.Navigator import com.resukisu.resukisu.ui.navigation.Route +import com.resukisu.resukisu.ui.theme.CardConfig +import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets +import org.koin.compose.koinInject @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -81,6 +82,7 @@ fun AboutScreen() { ) Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { LargeFlexibleTopAppBar( modifier = Modifier.blurEffect( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/OpenSourceLicenseScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/OpenSourceLicenseScreen.kt index 958cb1a49..a0d4ee7d1 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/OpenSourceLicenseScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/about/OpenSourceLicenseScreen.kt @@ -30,37 +30,41 @@ import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.contentColorFor 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.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties +import com.mikepenz.aboutlibraries.Libs import com.mikepenz.aboutlibraries.entity.Library -import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults -import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries -import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer -import com.mikepenz.aboutlibraries.ui.compose.m3.chipColors -import com.mikepenz.aboutlibraries.ui.compose.m3.libraryColors +import com.mikepenz.aboutlibraries.ui.compose.util.author +import com.mikepenz.aboutlibraries.util.withJson import com.resukisu.resukisu.R import com.resukisu.resukisu.ui.component.WarningCard import com.resukisu.resukisu.ui.component.settings.AppBackButton +import com.resukisu.resukisu.ui.component.settings.SettingsBaseWidget +import com.resukisu.resukisu.ui.component.settings.lazySegmentColumn import com.resukisu.resukisu.ui.navigation.LocalNavigator +import com.resukisu.resukisu.ui.screen.LabelText import com.resukisu.resukisu.ui.theme.CardConfig import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource -import com.resukisu.resukisu.ui.theme.renderBackgroundBlur +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.koin.compose.koinInject @@ -82,13 +86,17 @@ fun OpenSourceLicenseScreen() { } // from https://github.com/mikepenz/AboutLibraries#setup - // Android: Provide resource identifier for the `R.raw.aboutlibraries` file. - // This file is generated by the AboutLibraries Gradle plugin. - val libraries by produceLibraries(R.raw.aboutlibraries) + val context = LocalContext.current + val libraries by produceState(initialValue = Libs(emptyList(), emptySet()), context) { + value = withContext(Dispatchers.IO) { + Libs.Builder().withJson(context, R.raw.aboutlibraries).build() + } + } var selectedLibrary by remember { mutableStateOf(null) } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), @@ -96,8 +104,7 @@ fun OpenSourceLicenseScreen() { contentColor = MaterialTheme.colorScheme.onSurface, topBar = { LargeFlexibleTopAppBar( - modifier = Modifier.blurEffect( - ), + modifier = Modifier.blurEffect(), windowInsets = TopAppBarDefaults.windowInsets.add(WindowInsets(left = 12.dp)), title = { Text(text = stringResource(id = R.string.open_source_license)) }, scrollBehavior = scrollBehavior, @@ -126,35 +133,34 @@ fun OpenSourceLicenseScreen() { ) }, ) { paddingValues -> - val cornerRadius = 16.dp - LibrariesContainer( - libraries = libraries, - libraryModifier = Modifier - .padding(vertical = 4.dp) - .clip(RoundedCornerShape(cornerRadius)) - .renderBackgroundBlur(), + LazyColumn( modifier = Modifier .fillMaxSize() - .padding(horizontal = 16.dp) .blurSource(), - contentPadding = paddingValues,// PaddingValues(horizontal = 16.dp), - colors = LibraryDefaults.libraryColors( - libraryBackgroundColor = if (themeConfig.isEnableBlurExp) Color.Transparent else MaterialTheme.colorScheme.surfaceBright.copy( - alpha = cardConfig.cardAlpha - ), - libraryContentColor = MaterialTheme.colorScheme.onSurface, - // To maintain the original appearance, explicitly set the license chip colors - // to match the old function's default badge colors. - licenseChipColors = LibraryDefaults.chipColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = contentColorFor(MaterialTheme.colorScheme.primary) - ) - ), - onLibraryClick = { library -> - selectedLibrary = library + contentPadding = paddingValues + ) { + lazySegmentColumn(libraries.libraries) { _, lib -> + SettingsBaseWidget( + iconPlaceholder = false, + title = lib.name, + description = lib.author, + descriptionColumnContent = { + Row { + lib.licenses.forEach { + LabelText(it.name) + } + } + }, + onClick = { + selectedLibrary = lib + } + ) { + lib.artifactVersion?.let { + Text(it) + } + } } - ) - + } if (selectedLibrary != null) { val library = selectedLibrary!! val uriHandler = LocalUriHandler.current diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/kernelFlash/KernelFlash.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/kernelFlash/KernelFlash.kt index bf08f5710..da7a08535 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/kernelFlash/KernelFlash.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/kernelFlash/KernelFlash.kt @@ -10,14 +10,10 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -64,6 +60,7 @@ import com.resukisu.resukisu.ui.navigation.LocalNavigator import com.resukisu.resukisu.ui.theme.CardConfig import com.resukisu.resukisu.ui.theme.MonospaceFontFamily import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.KernelFlashUiAction import com.resukisu.resukisu.ui.viewmodel.KernelFlashUiEvent @@ -189,7 +186,7 @@ fun KernelFlashScreen( } }, snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + contentWindowInsets = adaptiveScaffoldWindowInsets(), containerColor = MaterialTheme.colorScheme.background ) { innerPadding -> KeyEventBlocker { @@ -400,7 +397,7 @@ private fun TopBar( ) } }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + windowInsets = adaptiveScaffoldWindowInsets(includeBottom = false), scrollBehavior = scrollBehavior ) } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt index be02ba5bf..7f08bf75d 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/HomePage.kt @@ -17,25 +17,34 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.twotone.MenuBook +import androidx.compose.material.icons.twotone.Android import androidx.compose.material.icons.twotone.Block +import androidx.compose.material.icons.twotone.DeveloperBoard import androidx.compose.material.icons.twotone.Error +import androidx.compose.material.icons.twotone.Extension +import androidx.compose.material.icons.twotone.FilterList +import androidx.compose.material.icons.twotone.Group import androidx.compose.material.icons.twotone.Info +import androidx.compose.material.icons.twotone.Memory import androidx.compose.material.icons.twotone.PowerSettingsNew +import androidx.compose.material.icons.twotone.Security +import androidx.compose.material.icons.twotone.Settings +import androidx.compose.material.icons.twotone.Smartphone +import androidx.compose.material.icons.twotone.Tag import androidx.compose.material.icons.twotone.TaskAlt import androidx.compose.material.icons.twotone.Tune +import androidx.compose.material.icons.twotone.VolunteerActivism import androidx.compose.material.icons.twotone.Warning import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -53,9 +62,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults -import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -64,7 +70,6 @@ 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.geometry.Offset import androidx.compose.ui.graphics.Color @@ -100,6 +105,7 @@ import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.LocalPermissionRequestInterface import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.downloader.downloadManagerUpdate import com.resukisu.resukisu.ui.viewmodel.HomeUiAction import com.resukisu.resukisu.ui.viewmodel.HomeUiEvent @@ -143,7 +149,6 @@ fun HomePage( if (!uiState.isInitialDataLoaded) return - val pullRefreshState = rememberPullToRefreshState() val topAppBarState = rememberTopAppBarState() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(topAppBarState) val scrollState = rememberScrollState() @@ -161,9 +166,7 @@ fun HomePage( }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), + contentWindowInsets = adaptiveScaffoldWindowInsets(includeBottom = false), snackbarHost = { SwipeableSnackbarHost( modifier = Modifier.padding(bottom = bottomPadding), @@ -171,45 +174,27 @@ fun HomePage( ) } ) { innerPadding -> - PullToRefreshBox( - state = pullRefreshState, - isRefreshing = uiState.isRefreshing, - onRefresh = { viewModel.dispatch(HomeUiAction.Refresh()) }, + Column( modifier = Modifier .fillMaxSize() - .blurSource(), - indicator = { - PullToRefreshDefaults.LoadingIndicator( - modifier = Modifier - .padding(top = innerPadding.calculateTopPadding()) - .align(Alignment.TopCenter), - state = pullRefreshState, - isRefreshing = uiState.isRefreshing, - ) - }, + .blurSource() + .nestedScroll(scrollBehavior.nestedScrollConnection) + .verticalScroll(scrollState) + .padding( + top = innerPadding.calculateTopPadding() + 2.dp, + start = 16.dp, + end = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(0.dp) ) { - Column( - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(scrollState) - .padding( - top = innerPadding.calculateTopPadding() + 2.dp, - start = 16.dp, - end = 16.dp - ), - verticalArrangement = Arrangement.spacedBy(0.dp) - ) { // 状态卡片 if (uiState.isCoreDataLoaded) { - if (uiState.systemStatus.requireNewKernel) { - if ((uiState.systemStatus.ksuVersion ?: 0) > BuildConfig.VERSION_CODE) { + if (uiState.systemStatus.isManager && !uiState.systemStatus.isFullFeatured) { + if ((uiState.systemStatus.kernelUAPIVersion + ?: 1) > uiState.systemStatus.managerUAPIVersion + ) { WarningCard( - message = stringResource( - id = R.string.require_manager_version, - BuildConfig.VERSION_CODE, - uiState.systemStatus.ksuVersion ?: 0 - ), + message = stringResource(R.string.require_manager_version), icon = { Icon( imageVector = Icons.TwoTone.Error, @@ -217,15 +202,17 @@ fun HomePage( tint = MaterialTheme.colorScheme.onErrorContainer, modifier = Modifier.size(18.dp) ) + }, + onClick = { + navigator.push(Route.Install(preselectedKernelUri = null)) } ) } else { WarningCard( - message = stringResource( - id = R.string.require_kernel_version, - uiState.systemStatus.ksuVersion ?: 0, - BuildConfig.VERSION_CODE - ), + message = if (uiState.systemStatus.lkmMode == true) + stringResource(R.string.require_kernel_version) + else + stringResource(R.string.require_kernel_version_gki), icon = { Icon( imageVector = Icons.TwoTone.Error, @@ -233,6 +220,9 @@ fun HomePage( tint = MaterialTheme.colorScheme.onErrorContainer, modifier = Modifier.size(18.dp) ) + }, + onClick = { + navigator.push(Route.Install(preselectedKernelUri = null)) } ) } @@ -347,11 +337,8 @@ fun HomePage( ) } Spacer(modifier = Modifier.height(10.dp)) - ManagerUpdateCard(uiState.stableManagerUpdate) - Spacer(modifier = Modifier.height(10.dp)) ManagerUpdateCard(uiState.betaManagerUpdate) - Spacer(modifier = Modifier.height(10.dp)) if (uiState.isBetaManagerUpdateCheckFailed) { WarningCard( message = stringResource(R.string.beta_update_check_failed), @@ -371,17 +358,17 @@ fun HomePage( systemStatus = uiState.systemStatus, systemInfo = uiState.systemInfo, isSimpleMode = uiState.isSimpleMode, + showHomeCardIcons = uiState.showHomeCardIcons, ) } // 链接卡片 if (!uiState.isSimpleMode) { - DonateCard() - LearnMoreCard() + DonateCard(uiState.showHomeCardIcons) + LearnMoreCard(uiState.showHomeCardIcons) } Spacer(Modifier.height(bottomPadding)) - } } } } @@ -466,6 +453,8 @@ private fun ManagerUpdateCardContent(updateInfo: ManagerUpdateInfo) { ) } ) + + Spacer(modifier = Modifier.height(10.dp)) } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -476,13 +465,9 @@ fun RebootDropdownItems( ) { items.onEachIndexed { index, (id, reason) -> DropdownMenuItem( - selected = false, + shape = MenuDefaults.itemShape(index, items.size).shape, text = { Text(stringResource(id)) }, onClick = { onReboot(reason) }, - shapes = MenuDefaults.itemShape( - index = index, - count = items.size - ) ) } } @@ -534,37 +519,39 @@ private fun TopBar( // 重启按钮 var showDropdown by remember { mutableStateOf(false) } KsuIsValid(uiState.systemStatus) { - IconButton(onClick = { - showDropdown = true - }) { - Icon( - imageVector = Icons.TwoTone.PowerSettingsNew, - contentDescription = stringResource(id = R.string.reboot) - ) - - DropdownMenuPopup(expanded = showDropdown, onDismissRequest = { - showDropdown = false + if (uiState.systemStatus.isRootAvailable) { + IconButton(onClick = { + showDropdown = true }) { - DropdownMenuGroup( - shapes = MenuDefaults.groupShapes() - ) { - val pm = - LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? - var methods = mapOf( - R.string.reboot to "", - R.string.reboot_soft to "soft_reboot", - R.string.reboot_recovery to "recovery", - R.string.reboot_bootloader to "bootloader", - R.string.reboot_download to "download", - R.string.reboot_edl to "edl" - ) + Icon( + imageVector = Icons.TwoTone.PowerSettingsNew, + contentDescription = stringResource(id = R.string.reboot) + ) - @Suppress("DEPRECATION") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { - methods = methods + (R.string.reboot_userspace to "userspace") - } + DropdownMenuPopup(expanded = showDropdown, onDismissRequest = { + showDropdown = false + }) { + DropdownMenuGroup( + shapes = MenuDefaults.groupShapes() + ) { + val pm = + LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? + var methods = mapOf( + R.string.reboot to "", + R.string.reboot_soft to "soft_reboot", + R.string.reboot_recovery to "recovery", + R.string.reboot_bootloader to "bootloader", + R.string.reboot_download to "download", + R.string.reboot_edl to "edl" + ) + + @Suppress("DEPRECATION") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { + methods = methods + (R.string.reboot_userspace to "userspace") + } - RebootDropdownItems(methods, onReboot) + RebootDropdownItems(methods, onReboot) + } } } } @@ -681,7 +668,9 @@ private fun StatusCard( } @Composable -fun LearnMoreCard() { +fun LearnMoreCard( + showIcon: Boolean, +) { val uriHandler = LocalUriHandler.current val url = stringResource(R.string.home_learn_kernelsu_url) @@ -692,6 +681,7 @@ fun LearnMoreCard() { ) { item { SettingsBaseWidget( + icon = Icons.AutoMirrored.TwoTone.MenuBook.takeIf { showIcon }, iconPlaceholder = false, title = stringResource(R.string.home_learn_kernelsu), description = stringResource(R.string.home_click_to_learn_kernelsu), @@ -704,7 +694,9 @@ fun LearnMoreCard() { } @Composable -fun DonateCard() { +fun DonateCard( + showIcon: Boolean, +) { val uriHandler = LocalUriHandler.current SegmentedColumn( modifier = Modifier.fillMaxWidth(), @@ -713,6 +705,7 @@ fun DonateCard() { ) { item { SettingsBaseWidget( + icon = Icons.TwoTone.VolunteerActivism.takeIf { showIcon }, iconPlaceholder = false, title = stringResource(R.string.home_support_title), description = stringResource(R.string.home_support_content), @@ -729,6 +722,7 @@ private fun InfoCard( systemStatus: KernelStatus, systemInfo: HomeSystemInfo, isSimpleMode: Boolean, + showHomeCardIcons: Boolean, ) { val managersList = systemInfo.managersList @@ -739,6 +733,7 @@ private fun InfoCard( ) { item { SettingsBaseWidget( + icon = Icons.TwoTone.Smartphone.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_device_model), description = systemInfo.deviceModel, @@ -747,6 +742,7 @@ private fun InfoCard( item { SettingsBaseWidget( + icon = Icons.TwoTone.DeveloperBoard.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_kernel), description = systemInfo.kernelRelease, @@ -757,6 +753,7 @@ private fun InfoCard( visible = !isSimpleMode ) { SettingsBaseWidget( + icon = Icons.TwoTone.Android.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_android_version), description = systemInfo.androidVersion, @@ -765,9 +762,10 @@ private fun InfoCard( item( - visible = systemStatus.isValid + visible = systemStatus.isManager ) { SettingsBaseWidget( + icon = Icons.TwoTone.Memory.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_kernel_version), description = systemStatus.ksuFullVersion.orEmpty(), @@ -776,6 +774,7 @@ private fun InfoCard( item { SettingsBaseWidget( + icon = Icons.TwoTone.Tag.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_manager_version), description = "${systemInfo.managerVersion.first} (${systemInfo.managerVersion.second}/${systemInfo.managerVersion.third})", @@ -786,6 +785,7 @@ private fun InfoCard( visible = !isSimpleMode && systemInfo.susfsEnabled && systemInfo.susfsVersion.isNotEmpty() ) { SettingsBaseWidget( + icon = Icons.TwoTone.Settings.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_susfs_version), description = systemInfo.susfsVersion, @@ -800,6 +800,7 @@ private fun InfoCard( ) { item { SettingsBaseWidget( + icon = Icons.TwoTone.Security.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_selinux_status), description = systemInfo.selinuxStatus, @@ -816,6 +817,7 @@ private fun InfoCard( } SettingsBaseWidget( + icon = Icons.TwoTone.FilterList.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_seccomp_status), description = seccompDisplay, @@ -848,6 +850,7 @@ private fun InfoCard( }.trimEnd(' ', '|') SettingsBaseWidget( + icon = Icons.TwoTone.Group.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.multi_manager_list), description = managersText.ifEmpty { stringResource(R.string.no_active_manager) }, @@ -855,9 +858,10 @@ private fun InfoCard( } item( - visible = !isSimpleMode && systemStatus.isValid + visible = !isSimpleMode && systemStatus.isFullFeatured ) { SettingsBaseWidget( + icon = Icons.TwoTone.Tune.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_hook_type), description = systemStatus.hookType, @@ -868,6 +872,7 @@ private fun InfoCard( visible = !isSimpleMode && systemInfo.zygiskImplement.isNotEmpty() && systemInfo.zygiskImplement != "None" ) { SettingsBaseWidget( + icon = Icons.TwoTone.Extension.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_zygisk_implement), description = systemInfo.zygiskImplement, @@ -878,6 +883,7 @@ private fun InfoCard( visible = !isSimpleMode && systemInfo.metaModuleImplement.isNotEmpty() && systemInfo.metaModuleImplement != "None" ) { SettingsBaseWidget( + icon = Icons.TwoTone.Extension.takeIf { showHomeCardIcons }, iconPlaceholder = false, title = stringResource(R.string.home_meta_module_implement), description = systemInfo.metaModuleImplement, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt index e92b156f7..9cb8de059 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/MainScreen.kt @@ -2,9 +2,9 @@ package com.resukisu.resukisu.ui.screen.main import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.HorizontalPager @@ -39,6 +39,7 @@ import com.resukisu.resukisu.ui.util.LocalBlurState import com.resukisu.resukisu.ui.util.LocalHandlePageChange import com.resukisu.resukisu.ui.util.LocalPagerPage import com.resukisu.resukisu.ui.util.LocalPagerState +import com.resukisu.resukisu.ui.util.LocalPortraitState import com.resukisu.resukisu.ui.util.LocalSelectedPage import com.resukisu.resukisu.ui.util.LocalSnackbarHost import com.resukisu.resukisu.ui.viewmodel.HomeViewModel @@ -52,9 +53,9 @@ import org.koin.compose.viewmodel.koinViewModel fun MainScreen() { val themeConfig: ThemeConfig = koinInject() val homeViewModel = koinViewModel() - val homeState by homeViewModel.state.collectAsStateWithLifecycle() - val pages = remember(homeState.systemStatus.isValid) { - BottomBarDestination.getPages(homeState.systemStatus.isValid) + val homeState by homeViewModel.uiState.collectAsStateWithLifecycle() + val pages = remember(homeState.systemStatus.isFullFeatured) { + BottomBarDestination.getPages(homeState.systemStatus.isFullFeatured) } val coroutineScope = rememberCoroutineScope() @@ -119,86 +120,84 @@ fun MainScreen() { LocalHandlePageChange provides handlePageChange, LocalSelectedPage provides uiSelectedPage ) { - BoxWithConstraints( - modifier = Modifier.fillMaxSize() - ) { - val isPortrait = maxWidth < maxHeight || (maxHeight / maxWidth > 1.4f) - val content = @Composable { paddingBottom: Dp -> - HorizontalPager( - modifier = Modifier - .fillMaxSize(), - state = pagerState, - userScrollEnabled = userScrollEnabled, - beyondViewportPageCount = 1, - ) { pageIndex -> - if (pages.isEmpty()) return@HorizontalPager + val content = @Composable { paddingBottom: Dp -> + HorizontalPager( + modifier = Modifier + .fillMaxSize(), + state = pagerState, + userScrollEnabled = userScrollEnabled, + beyondViewportPageCount = 1, + ) { pageIndex -> + if (pages.isEmpty()) return@HorizontalPager - val snackBarHostState = remember { SnackbarHostState() } - CompositionLocalProvider( - LocalSnackbarHost provides snackBarHostState, - LocalPagerPage provides pageIndex, - LocalBlurState provides rememberMaterial3BlurBackdrop( - enableBlur = themeConfig.isEnableBlur, - pagerState = pagerState, - pagerPage = pageIndex, - ), - ) { - val destination = pages[pageIndex] - destination.direction(paddingBottom) - } + val snackBarHostState = remember { SnackbarHostState() } + CompositionLocalProvider( + LocalSnackbarHost provides snackBarHostState, + LocalPagerPage provides pageIndex, + LocalBlurState provides rememberMaterial3BlurBackdrop( + enableBlur = themeConfig.isEnableBlur, + pagerState = pagerState, + pagerPage = pageIndex, + ), + ) { + val destination = pages[pageIndex] + destination.direction(paddingBottom) } } + } - if (isPortrait) { - Scaffold( - modifier = Modifier.fillMaxSize(), - bottomBar = { - NavigationBar( - destinations = pages, - isBottomBar = true, - ) - }, - containerColor = Color.Transparent, - ) { innerPadding -> - Box( - modifier = Modifier.blurSource() - ) { - content(innerPadding.calculateBottomPadding()) - } + if (LocalPortraitState.current) { + Scaffold( + // The child pages own their top-bar insets. The outer scaffold only reserves the + // measured bottom navigation bar height for the pager content. + contentWindowInsets = WindowInsets(), + modifier = Modifier.fillMaxSize(), + bottomBar = { + NavigationBar( + destinations = pages, + isBottomBar = true, + ) + }, + containerColor = Color.Transparent, + ) { innerPadding -> + Box( + modifier = Modifier.blurSource() + ) { + content(innerPadding.calculateBottomPadding()) } - } else { - var navWidth by remember { mutableIntStateOf(0) } - val density = LocalDensity.current + } + } else { + var navWidth by remember { mutableIntStateOf(0) } + val density = LocalDensity.current - Box( - modifier = Modifier.fillMaxSize() + Box( + modifier = Modifier.fillMaxSize() + ) { + Row( + modifier = Modifier + .fillMaxSize() + .blurSource() ) { - Row( - modifier = Modifier - .fillMaxSize() - .blurSource() - ) { - Spacer( - modifier = Modifier.width( - with(density) { navWidth.toDp() } - ) + Spacer( + modifier = Modifier.width( + with(density) { navWidth.toDp() } ) + ) - Box(Modifier.weight(1f)) { - content(0.dp) - } + Box(Modifier.weight(1f)) { + content(0.dp) } - - NavigationBar( - modifier = Modifier - .align(Alignment.CenterStart) - .onSizeChanged { - navWidth = it.width - }, - destinations = pages, - isBottomBar = false, - ) } + + NavigationBar( + modifier = Modifier + .align(Alignment.CenterStart) + .onSizeChanged { + navWidth = it.width + }, + destinations = pages, + isBottomBar = false, + ) } } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt index 877b83695..65732c94a 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/ModulePage.kt @@ -27,15 +27,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -62,8 +58,8 @@ import androidx.compose.material.icons.twotone.Warning import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CheckableDropdownMenuItem import androidx.compose.material3.DropdownMenuGroup -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -159,6 +155,7 @@ import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur import com.resukisu.resukisu.ui.util.LocalPermissionRequestInterface import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.downloader.download import com.resukisu.resukisu.ui.util.module.Shortcut import com.resukisu.resukisu.ui.util.showReplacingSnackbar @@ -197,7 +194,7 @@ fun ModulePage(bottomPadding: Dp) { val context = LocalContext.current val viewModel = koinViewModel() val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val homeState by koinViewModel().state.collectAsStateWithLifecycle() + val homeState by koinViewModel().uiState.collectAsStateWithLifecycle() val snackBarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() var lastClickTime by remember { mutableStateOf(0L) } @@ -374,9 +371,7 @@ fun ModulePage(bottomPadding: Dp) { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), + contentWindowInsets = adaptiveScaffoldWindowInsets(includeBottom = false), snackbarHost = { SwipeableSnackbarHost( hostState = snackBarHost @@ -499,11 +494,11 @@ private fun ModuleDropdown( DropdownMenuGroup( shapes = MenuDefaults.groupShapes(), ) { - DropdownMenuItem( + CheckableDropdownMenuItem( checked = uiState.sortActionFirst, - onCheckedChange = { checked -> + onCheckedChange = { viewModel.dispatch( - ModuleUiAction.Sort(uiState.sortEnabledFirst, checked) + ModuleUiAction.Sort(uiState.sortEnabledFirst, it) ) }, text = { Text(stringResource(R.string.module_sort_action_first)) }, @@ -512,11 +507,11 @@ private fun ModuleDropdown( count = 2, ), ) - DropdownMenuItem( + CheckableDropdownMenuItem( checked = uiState.sortEnabledFirst, - onCheckedChange = { checked -> + onCheckedChange = { viewModel.dispatch( - ModuleUiAction.Sort(checked, uiState.sortActionFirst) + ModuleUiAction.Sort(it, uiState.sortActionFirst) ) }, text = { Text(stringResource(R.string.module_sort_enabled_first)) }, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt index 0834e6414..488c147b6 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SettingsPage.kt @@ -16,13 +16,10 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape @@ -39,19 +36,15 @@ import androidx.compose.material.icons.twotone.Fence import androidx.compose.material.icons.twotone.FolderDelete import androidx.compose.material.icons.twotone.FolderOff import androidx.compose.material.icons.twotone.Info -import androidx.compose.material.icons.twotone.Language import androidx.compose.material.icons.twotone.Policy -import androidx.compose.material.icons.twotone.RadioButtonChecked -import androidx.compose.material.icons.twotone.RadioButtonUnchecked import androidx.compose.material.icons.twotone.RemoveCircle import androidx.compose.material.icons.twotone.RemoveModerator import androidx.compose.material.icons.twotone.Save +import androidx.compose.material.icons.twotone.Science import androidx.compose.material.icons.twotone.Security import androidx.compose.material.icons.twotone.Settings import androidx.compose.material.icons.twotone.Share import androidx.compose.material.icons.twotone.Update -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon @@ -60,7 +53,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState @@ -87,10 +79,8 @@ import com.resukisu.resukisu.BuildConfig import com.resukisu.resukisu.R import com.resukisu.resukisu.domain.usecase.GenerateBugreportUseCase import com.resukisu.resukisu.ui.component.ConfirmResult -import com.resukisu.resukisu.ui.component.DialogHandle import com.resukisu.resukisu.ui.component.SwipeableSnackbarHost import com.resukisu.resukisu.ui.component.rememberConfirmDialog -import com.resukisu.resukisu.ui.component.rememberCustomDialog import com.resukisu.resukisu.ui.component.rememberLoadingDialog import com.resukisu.resukisu.ui.component.settings.SegmentedColumn import com.resukisu.resukisu.ui.component.settings.SettingsBaseWidget @@ -104,6 +94,7 @@ import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.HomeViewModel import com.resukisu.resukisu.ui.viewmodel.SettingsUiAction @@ -134,7 +125,7 @@ fun SettingsPage(bottomPadding: Dp) { val homeViewModel = koinViewModel() val generateBugreport = koinInject() val uiState by settingsViewModel.uiState.collectAsStateWithLifecycle() - val homeState by homeViewModel.state.collectAsStateWithLifecycle() + val homeState by homeViewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(Unit) { settingsViewModel.dispatch(SettingsUiAction.LoadFeatureSettings) @@ -152,7 +143,7 @@ fun SettingsPage(bottomPadding: Dp) { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + contentWindowInsets = adaptiveScaffoldWindowInsets(includeBottom = false) ) { innerPadding -> val loadingDialog = rememberLoadingDialog() var showBottomsheet by remember { mutableStateOf(false) } @@ -188,7 +179,7 @@ fun SettingsPage(bottomPadding: Dp) { ) ) { // 配置卡片 - if (homeState.systemStatus.isValid) { + if (homeState.systemStatus.isFullFeatured) { item { val modeItems = listOf( stringResource(id = R.string.settings_mode_default), @@ -318,28 +309,6 @@ fun SettingsPage(bottomPadding: Dp) { ) } - item { - val webViewUmountSummary = when (uiState.webViewZygoteUmountStatus) { - "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) - "managed" -> stringResource(id = R.string.feature_status_managed_summary) - else -> stringResource(id = R.string.settings_webview_zygote_umount_summary) - } - SettingsSwitchWidget( - icon = Icons.TwoTone.Language, - title = stringResource(id = R.string.settings_webview_zygote_umount), - description = webViewUmountSummary, - enabled = uiState.webViewZygoteUmountStatus == "supported", - checked = uiState.isWebViewZygoteUmountEnabled, - onCheckedChange = { checked -> - settingsViewModel.dispatch( - SettingsUiAction.SetWebViewZygoteUmountEnabled( - checked - ) - ) - }, - ) - } - item { val selinuxHideSummary = when (uiState.selinuxHideStatus) { "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) @@ -410,6 +379,7 @@ fun SettingsPage(bottomPadding: Dp) { topPadding = 1.dp ) { SettingsSwitchWidget( + icon = Icons.TwoTone.Science, title = stringResource(R.string.settings_check_beta_update), description = stringResource(R.string.settings_check_beta_update_summary), checked = uiState.checkBetaUpdate, @@ -467,10 +437,10 @@ fun SettingsPage(bottomPadding: Dp) { onClick = { showBottomsheet = true } - ) {} + ) } - if (homeState.systemStatus.isValid) { + if (homeState.systemStatus.isFullFeatured) { item { SettingsJumpPageWidget( icon = Icons.TwoTone.Security, @@ -647,30 +617,40 @@ fun UninstallItem( val showTodo = { Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show() } - val uninstallDialog = rememberUninstallDialog { uninstallType -> - scope.launch { - val result = uninstallConfirmDialog.awaitConfirm( - title = context.getString(uninstallType.title), - content = context.getString(uninstallType.message) - ) - if (result == ConfirmResult.Confirmed) { - withLoading { - when (uninstallType) { - UninstallType.TEMPORARY -> showTodo() - UninstallType.PERMANENT -> navigator.push(Route.Flash.uninstall()) - UninstallType.RESTORE_STOCK_IMAGE -> navigator.push(Route.Flash.restore()) - UninstallType.NONE -> Unit - } - } - } - } + val options = remember { + listOf( + UninstallType.PERMANENT, + UninstallType.RESTORE_STOCK_IMAGE + ) } - SettingsJumpPageWidget( + SettingsChooseWidget( icon = Icons.TwoTone.Delete, title = stringResource(id = R.string.settings_uninstall), - onClick = { - uninstallDialog.show() + items = options.map { stringResource(it.title) }, + itemDescriptions = options.map { + if (it.message != 0) stringResource(it.message) else null + }, + selectedIndex = -1, + onSelectedIndexChange = { index -> + options.getOrNull(index)?.let { uninstallType -> + scope.launch { + val result = uninstallConfirmDialog.awaitConfirm( + title = context.getString(uninstallType.title), + content = context.getString(uninstallType.message) + ) + if (result == ConfirmResult.Confirmed) { + withLoading { + when (uninstallType) { + UninstallType.TEMPORARY -> showTodo() + UninstallType.PERMANENT -> navigator.push(Route.Flash.uninstall()) + UninstallType.RESTORE_STOCK_IMAGE -> navigator.push(Route.Flash.restore()) + UninstallType.NONE -> Unit + } + } + } + } + } } ) } @@ -694,128 +674,6 @@ enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector NONE(0, 0, Icons.TwoTone.Delete) } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle { - return rememberCustomDialog { dismiss -> - val options = listOf( - UninstallType.PERMANENT, - UninstallType.RESTORE_STOCK_IMAGE - ) - var selectedOption by remember { mutableStateOf(null) } - - AlertDialog( - onDismissRequest = { - dismiss() - }, - title = { - Text( - text = stringResource(R.string.settings_uninstall), - style = MaterialTheme.typography.headlineSmall, - ) - }, - text = { - Column( - modifier = Modifier.padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - options.forEach { option -> - val isSelected = selectedOption == option - val backgroundColor = if (isSelected) - MaterialTheme.colorScheme.primaryContainer - else - Color.Transparent - val contentColor = if (isSelected) - MaterialTheme.colorScheme.onPrimaryContainer - else - MaterialTheme.colorScheme.onSurface - - Row( - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .background(backgroundColor) - .clickable { - selectedOption = option - } - .padding(vertical = 12.dp, horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = option.icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = stringResource(option.title), - style = MaterialTheme.typography.titleMedium, - ) - if (option.message != 0) { - Text( - text = stringResource(option.message), - style = MaterialTheme.typography.bodyMedium, - color = if (isSelected) - contentColor.copy(alpha = 0.8f) - else - MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - if (isSelected) { - Icon( - imageVector = Icons.TwoTone.RadioButtonChecked, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - } else { - Icon( - imageVector = Icons.TwoTone.RadioButtonUnchecked, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp) - ) - } - } - } - } - }, - confirmButton = { - Button( - onClick = { - selectedOption?.let { onSelected(it) } - dismiss() - }, - enabled = selectedOption != null, - ) { - Text( - text = stringResource(android.R.string.ok) - ) - } - }, - dismissButton = { - TextButton( - onClick = { - dismiss() - } - ) { - Text( - text = stringResource(android.R.string.cancel), - ) - } - }, - shape = MaterialTheme.shapes.extraLarge, - tonalElevation = 4.dp - ) - } -} - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun TopBar( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt index c827f007c..599cbca40 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/main/SuperUserPage.kt @@ -10,14 +10,10 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState @@ -28,7 +24,6 @@ import androidx.compose.material.icons.twotone.ChevronRight import androidx.compose.material.icons.twotone.MoreVert import androidx.compose.material.icons.twotone.SearchOff import androidx.compose.material3.DropdownMenuGroup -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -38,6 +33,7 @@ import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MenuDefaults import androidx.compose.material3.Scaffold +import androidx.compose.material3.SelectableDropdownMenuItem import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults @@ -58,10 +54,7 @@ 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.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -82,6 +75,7 @@ import com.resukisu.resukisu.ui.navigation.Route import com.resukisu.resukisu.ui.screen.LabelText import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.SortType import com.resukisu.resukisu.ui.viewmodel.SuperUserUiAction @@ -98,7 +92,8 @@ import java.util.Locale private data class SuperUserMenuItem( val checked: Boolean = false, val titleRes: Int, - val onClick: () -> Unit + val onClick: () -> Unit, + val closeOnClick: Boolean = true, ) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -185,9 +180,6 @@ fun SuperUserPage(bottomPadding: Dp) { } Scaffold( - modifier = Modifier - .testTag(SUPER_USER_SCREEN_TEST_TAG) - .semantics { testTagsAsResourceId = true }, topBar = { SearchAppBar( title = stringResource(R.string.superuser), @@ -236,7 +228,7 @@ fun SuperUserPage(bottomPadding: Dp) { hostState = snackBarHostState ) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + contentWindowInsets = adaptiveScaffoldWindowInsets(includeBottom = false), ) { innerPadding -> SuperUserContent( innerPadding = innerPadding, @@ -355,7 +347,6 @@ private fun SuperUserContent( state = listState, modifier = Modifier .fillMaxSize() - .testTag(SUPER_USER_LIST_TEST_TAG) .nestedScroll(scrollBehavior.nestedScrollConnection), ) { item { @@ -363,13 +354,14 @@ private fun SuperUserContent( } lazySegmentColumn( items = uiState.appGroupList, - key = { _, appGroup -> "${appGroup.uid}-${appGroup.mainApp.packageName}" }, - contentType = { _, _ -> "AppGroupItem" } + key = { _, appGroup -> "${appGroup.uid}-${appGroup.profileKey}" }, + contentType = { _, appGroup -> "${appGroup.uid}-${appGroup.profileKey}" }, ) { _, appGroup -> AppGroupItem( - appGroup = appGroup + appGroup = appGroup, + isManager = appGroup.uid in uiState.managerUids, ) { - navigator.push(Route.AppProfile(appGroup.uid, appGroup.mainApp.packageName)) + navigator.push(Route.AppProfile(appGroup.uid, appGroup.profileKey)) } } @@ -380,9 +372,6 @@ private fun SuperUserContent( } } -private const val SUPER_USER_LIST_TEST_TAG = "super_user_app_list" -private const val SUPER_USER_SCREEN_TEST_TAG = "super_user_screen" - @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun SuperUserDropdown( @@ -395,13 +384,23 @@ private fun SuperUserDropdown( ) { val menuItems = remember( uiState.showSystemApps, + uiState.reverseOrder, onBackupAllowlist, onRestoreAllowlist, ) { listOf( + SuperUserMenuItem( + checked = uiState.reverseOrder, + titleRes = R.string.reverse_order, + closeOnClick = false, + onClick = { + viewModel.dispatch(SuperUserUiAction.SetReverseOrder(!uiState.reverseOrder)) + } + ), SuperUserMenuItem( checked = uiState.showSystemApps, titleRes = R.string.show_system_apps, + closeOnClick = false, onClick = { viewModel.dispatch(SuperUserUiAction.SetShowSystemApps(!uiState.showSystemApps)) } @@ -425,12 +424,12 @@ private fun SuperUserDropdown( shapes = MenuDefaults.groupShapes(), ) { SortType.entries.forEachIndexed { index, sortType -> - DropdownMenuItem( + SelectableDropdownMenuItem( selected = uiState.currentSortType == sortType, - text = { Text(stringResource(sortType.displayNameRes)) }, onClick = { viewModel.dispatch(SuperUserUiAction.SetSort(sortType)) }, + text = { Text(stringResource(sortType.displayNameRes)) }, shapes = MenuDefaults.itemShape( index = index, count = SortType.entries.size, @@ -445,13 +444,13 @@ private fun SuperUserDropdown( shapes = MenuDefaults.groupShapes(), ) { menuItems.forEachIndexed { index, menuItem -> - DropdownMenuItem( + SelectableDropdownMenuItem( selected = menuItem.checked, - text = { Text(stringResource(menuItem.titleRes)) }, onClick = { - onDismissRequest() + if (menuItem.closeOnClick) onDismissRequest() menuItem.onClick() }, + text = { Text(stringResource(menuItem.titleRes)) }, shapes = MenuDefaults.itemShape( index = index, count = menuItems.size, @@ -466,6 +465,7 @@ private fun SuperUserDropdown( @Composable private fun AppGroupItem( appGroup: InstalledAppGroup, + isManager: Boolean, onClick: () -> Unit, ) { val mainApp = appGroup.mainApp @@ -477,7 +477,7 @@ private fun AppGroupItem( description = if (appGroup.apps.size > 1) { stringResource(R.string.group_contains_apps, appGroup.apps.size) } else { - mainApp.packageName + mainApp.displayIdentifier }, descriptionColumnContent = { Spacer(modifier = Modifier.height(5.dp)) @@ -507,6 +507,12 @@ private fun AppGroupItem( containerColor = MaterialTheme.colorScheme.primaryContainer ) } + if (isManager) { + LabelText( + label = "MANAGER", + containerColor = MaterialTheme.colorScheme.errorContainer, + ) + } if (appGroup.apps.size > 1) { appGroup.userName?.let { LabelText( @@ -525,7 +531,7 @@ private fun AppGroupItem( }, leadingContent = { PackageIcon( - packageName = mainApp.packageName, + packageName = if (appGroup.isWebViewZygote) "android" else mainApp.packageName, contentDescription = mainApp.label, modifier = Modifier .padding(4.dp) diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/ModuleRepo.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/ModuleRepo.kt index 188337e4a..97a435eef 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/ModuleRepo.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/ModuleRepo.kt @@ -10,15 +10,11 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -32,8 +28,8 @@ import androidx.compose.material.icons.twotone.Extension import androidx.compose.material.icons.twotone.MoreVert import androidx.compose.material.icons.twotone.Star import androidx.compose.material.icons.twotone.WebAsset +import androidx.compose.material3.CheckableDropdownMenuItem import androidx.compose.material3.DropdownMenuGroup -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuPopup import androidx.compose.material3.ElevatedCard import androidx.compose.material3.ExperimentalMaterial3Api @@ -107,6 +103,7 @@ import com.resukisu.resukisu.ui.theme.renderBackgroundBlur import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalPermissionRequestInterface import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.downloader.download import com.resukisu.resukisu.ui.viewmodel.ModuleRepoUiAction import com.resukisu.resukisu.ui.viewmodel.ModuleRepoUiState @@ -196,9 +193,7 @@ fun ModuleRepoScreen() { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), + contentWindowInsets = adaptiveScaffoldWindowInsets(), snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) } ) { innerPadding -> if (isLoading) { @@ -303,10 +298,10 @@ private fun ModuleRepoDropdown( DropdownMenuGroup( shapes = MenuDefaults.groupShapes(), ) { - DropdownMenuItem( + CheckableDropdownMenuItem( checked = uiState.sortStargazerCountFirst, - onCheckedChange = { checked -> - viewModel.dispatch(ModuleRepoUiAction.SetStarsFirst(checked)) + onCheckedChange = { + viewModel.dispatch(ModuleRepoUiAction.SetStarsFirst(it)) }, text = { Text(stringResource(R.string.module_sort_star_first)) }, shapes = MenuDefaults.itemShape( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/OnlineModuleDetail.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/OnlineModuleDetail.kt index 0353a8b4a..e4e709061 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/OnlineModuleDetail.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/moduleRepo/OnlineModuleDetail.kt @@ -19,14 +19,11 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -104,6 +101,7 @@ import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur import com.resukisu.resukisu.ui.util.LocalPermissionRequestInterface import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.viewmodel.ModuleDetailUiAction import com.resukisu.resukisu.ui.viewmodel.ModuleDetailViewModel import com.resukisu.resukisu.ui.viewmodel.formatFileSize @@ -238,9 +236,7 @@ private fun OnlineModuleDetailContent(module: CatalogModule) { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), + contentWindowInsets = adaptiveScaffoldWindowInsets(), snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) } ) { innerPadding -> Column( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/susfs/SuSFSConfig.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/susfs/SuSFSConfig.kt index 4c7e076fe..65b5f3fe5 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/susfs/SuSFSConfig.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/susfs/SuSFSConfig.kt @@ -9,13 +9,10 @@ import androidx.compose.animation.shrinkHorizontally import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState @@ -62,6 +59,7 @@ import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.util.ActivityResumeEffect import com.resukisu.resukisu.ui.util.LocalSnackbarHost +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.util.showReplacingSnackbar import com.resukisu.resukisu.ui.viewmodel.SuSFSUiAction import com.resukisu.resukisu.ui.viewmodel.SuSFSUiEvent @@ -305,9 +303,7 @@ fun SuSFSConfigScreen() { }, containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.onSurface, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), + contentWindowInsets = adaptiveScaffoldWindowInsets(), snackbarHost = { SwipeableSnackbarHost(hostState = snackBarHost) } ) { innerPadding -> PullToRefreshBox( diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt index d5957f724..5ca6b1637 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/ThemeSettings.kt @@ -33,6 +33,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.twotone.Android import androidx.compose.material.icons.twotone.Animation +import androidx.compose.material.icons.twotone.Badge import androidx.compose.material.icons.twotone.BlurOn import androidx.compose.material.icons.twotone.Brush import androidx.compose.material.icons.twotone.Check @@ -40,6 +41,7 @@ import androidx.compose.material.icons.twotone.ColorLens import androidx.compose.material.icons.twotone.Contrast import androidx.compose.material.icons.twotone.DarkMode import androidx.compose.material.icons.twotone.DesignServices +import androidx.compose.material.icons.twotone.Dock import androidx.compose.material.icons.twotone.Draw import androidx.compose.material.icons.twotone.FormatColorFill import androidx.compose.material.icons.twotone.FormatSize @@ -47,6 +49,7 @@ import androidx.compose.material.icons.twotone.Info import androidx.compose.material.icons.twotone.LightMode import androidx.compose.material.icons.twotone.Opacity import androidx.compose.material.icons.twotone.Palette +import androidx.compose.material.icons.twotone.Pin import androidx.compose.material.icons.twotone.Style import androidx.compose.material.icons.twotone.SwapHoriz import androidx.compose.material.icons.twotone.Translate @@ -75,13 +78,13 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation3.ui.LocalNavAnimatedContentScope import com.materialkolor.PaletteStyle import com.materialkolor.dynamiccolor.ColorSpec import com.resukisu.resukisu.R @@ -102,11 +105,13 @@ import com.resukisu.resukisu.ui.screen.themeSettings.component.LanguageSelection import com.resukisu.resukisu.ui.screen.themeSettings.component.ThemeSettingsDialogs import com.resukisu.resukisu.ui.screen.themeSettings.crop.BackgroundCropActivity import com.resukisu.resukisu.ui.theme.BackgroundManager +import com.resukisu.resukisu.ui.theme.BottomBarStyle import com.resukisu.resukisu.ui.theme.CardConfig import com.resukisu.resukisu.ui.theme.ThemeConfig import com.resukisu.resukisu.ui.theme.blurEffect import com.resukisu.resukisu.ui.theme.blurSource import com.resukisu.resukisu.ui.theme.renderBackgroundBlur +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.resukisu.resukisu.ui.viewmodel.HomeUiAction import com.resukisu.resukisu.ui.viewmodel.HomeUiState import com.resukisu.resukisu.ui.viewmodel.HomeViewModel @@ -130,8 +135,7 @@ import kotlin.math.roundToInt @SuppressLint( - "LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt", - "RestrictedApi" + "LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt" ) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -294,6 +298,7 @@ fun ThemeSettingsScreen( } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), topBar = { LargeFlexibleTopAppBar( @@ -352,31 +357,18 @@ fun ThemeSettingsScreen( item { // Predictive Back Settings - val transition = LocalNavAnimatedContentScope.current.transition - SegmentedColumn( title = stringResource(R.string.predictive_back_settings) ) { item { PredictiveBackAnimationWidget(settingsState) { animation -> - // Hey Google - // Why you keep playing the animation even we are already play completed? - - // This is very dirty, We are using RestrictedApi, but we don't have other choice - transition.setPlaytimeAfterInitialAndTargetStateEstablished( - transition.targetState, - transition.targetState, - transition.playTimeNanos - ) - settingsViewModel.dispatch( SettingsUiAction.SetPredictiveBackAnimation(animation) ) } } item( - visible = settingsState.predictiveBackAnimation == PredictiveBackAnimation.Scale || - settingsState.predictiveBackAnimation == PredictiveBackAnimation.AOSP + visible = settingsState.predictiveBackAnimation == PredictiveBackAnimation.Scale ) { PredictiveBackAnimationDirectionWidget(settingsState) { direction -> settingsViewModel.dispatch( @@ -497,6 +489,9 @@ private fun AppearanceSettings( val cardConfig: CardConfig = koinInject() val backgroundManager: BackgroundManager = koinInject() val paletteStyles = state.dynamicColorSpec.availablePaletteStyles() + val configuration = LocalConfiguration.current + val isPortrait = configuration.screenWidthDp < configuration.screenHeightDp || + (configuration.screenHeightDp.toFloat() / configuration.screenWidthDp > 1.4f) SegmentedColumn(title = stringResource(R.string.appearance_settings)) { item { // 语言设置 @@ -516,25 +511,27 @@ private fun AppearanceSettings( ) } - item { - // 动态颜色开关 - SettingsSwitchWidget( - icon = Icons.TwoTone.ColorLens, - title = stringResource(R.string.dynamic_color_title), - description = stringResource(R.string.dynamic_color_summary), - checked = state.useDynamicColor, - onCheckedChange = { enabled -> - viewModel.dispatch(SettingsUiAction.SetDynamicColor(enabled)) - } - ) - } - - item( - visible = !state.useDynamicColor, - topPadding = 1.dp, + expandableItem( + expanded = !state.useDynamicColor, + topContent = { + SettingsSwitchWidget( + icon = Icons.TwoTone.ColorLens, + title = stringResource(R.string.dynamic_color_title), + description = stringResource(R.string.dynamic_color_summary), + checked = state.useDynamicColor, + onCheckedChange = { enabled -> + viewModel.dispatch(SettingsUiAction.SetDynamicColor(enabled)) + } + ) + } ) { - // 主题色选择 - ThemeColorSelection(viewModel = viewModel) + item( + visible = !state.useDynamicColor, + topPadding = 1.dp, + ) { + // 主题色选择 + ThemeColorSelection(viewModel = viewModel) + } } item { @@ -571,7 +568,9 @@ private fun AppearanceSettings( ) } - item { + item( + forceFlatBottom = true, + ) { SettingsBaseWidget( icon = Icons.TwoTone.FormatSize, title = stringResource(R.string.app_dpi_title), @@ -588,6 +587,7 @@ private fun AppearanceSettings( item( topPadding = 1.dp, + forceFlatTop = true, ) { shape -> Surface( modifier = Modifier @@ -596,7 +596,6 @@ private fun AppearanceSettings( color = if (themeConfig.isEnableBlurExp) Color.Transparent else MaterialTheme.colorScheme.surfaceBright.copy( alpha = cardConfig.cardAlpha ), - shape = shape ) { Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { DpiSliderControls( @@ -608,6 +607,34 @@ private fun AppearanceSettings( } } + item(visible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + SettingsSwitchWidget( + icon = Icons.TwoTone.BlurOn, + title = stringResource(id = R.string.settings_config_enable_blur), + description = stringResource(id = R.string.settings_config_enable_blur_summary), + checked = themeConfig.isEnableBlur, + onCheckedChange = { isChecked -> + backgroundManager.saveEnableBlur(isChecked) + if (!isChecked) + backgroundManager.saveEnableBlurExp(false) + } + ) + } + + item(visible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && isPortrait) { + SettingsSwitchWidget( + icon = Icons.TwoTone.Dock, + title = stringResource(R.string.enable_floating_bottom_bar), + description = stringResource(R.string.enable_floating_bottom_bar_summary), + checked = themeConfig.bottomBarStyle == BottomBarStyle.FLOATING, + onCheckedChange = { enabled -> + val style = if (enabled) BottomBarStyle.FLOATING else BottomBarStyle.MATERIAL3_EXPRESSIVE + backgroundManager.saveBottomBarStyle(style) + } + ) + } + + expandableItem( expanded = state.isCustomBackgroundEnabled, topContent = { @@ -689,6 +716,30 @@ private fun CustomizationSettings( } ) } + + item { + SettingsSwitchWidget( + icon = Icons.TwoTone.Pin, + title = stringResource(R.string.navigation_bar_badge), + description = stringResource(R.string.navigation_bar_badge_summary), + checked = homeUiState.showNavigationBarBadge, + onCheckedChange = { enabled -> + homeViewModel.dispatch(HomeUiAction.SetNavigationBarBadge(enabled)) + } + ) + } + + item { + SettingsSwitchWidget( + icon = Icons.TwoTone.Badge, + title = stringResource(R.string.home_card_icons), + description = stringResource(R.string.home_card_icons_summary), + checked = homeUiState.showHomeCardIcons, + onCheckedChange = { enabled -> + homeViewModel.dispatch(HomeUiAction.SetHomeCardIcons(enabled)) + } + ) + } } } @@ -870,40 +921,18 @@ private fun SegmentedColumnScope.backgroundAdjustmentControls( ) } - expandableItem( - animatedVisibility = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S, - expanded = themeConfig.isEnableBlur, - topPadding = 1.dp, - topContent = { - SettingsSwitchWidget( - icon = Icons.TwoTone.BlurOn, - title = stringResource(id = R.string.settings_config_enable_blur), - description = stringResource(id = R.string.settings_config_enable_blur_summary), - checked = themeConfig.isEnableBlur, - onCheckedChange = { isChecked -> - backgroundManager.saveEnableBlur(isChecked) - if (!isChecked) - backgroundManager.saveEnableBlurExp(false) - } - ) - }, - bottomContent = { - item( - topPadding = 1.dp, - ) { - SettingsSwitchWidget( - icon = Icons.TwoTone.Draw, - title = stringResource(id = R.string.settings_exp_draw_background_to_blur), - description = stringResource(id = R.string.settings_exp_draw_background_to_blur_description), - isError = true, - checked = themeConfig.isEnableBlurExp, - onCheckedChange = { isChecked -> - backgroundManager.saveEnableBlurExp(isChecked) - } - ) + item(visible = themeConfig.isEnableBlur, topPadding = 1.dp) { + SettingsSwitchWidget( + icon = Icons.TwoTone.Draw, + title = stringResource(id = R.string.settings_exp_draw_background_to_blur), + description = stringResource(id = R.string.settings_exp_draw_background_to_blur_description), + isError = true, + checked = themeConfig.isEnableBlurExp, + onCheckedChange = { isChecked -> + backgroundManager.saveEnableBlurExp(isChecked) } - } - ) + ) + } item( visible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && state.useDynamicColor, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/crop/BackgroundCropActivity.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/crop/BackgroundCropActivity.kt index a9af7749c..880bdc9ae 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/crop/BackgroundCropActivity.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/screen/themeSettings/crop/BackgroundCropActivity.kt @@ -72,6 +72,7 @@ import androidx.compose.ui.window.PopupPositionProvider import com.resukisu.resukisu.R import com.resukisu.resukisu.ui.component.KeyPointSlider import com.resukisu.resukisu.ui.theme.KernelSUTheme +import com.resukisu.resukisu.ui.util.adaptiveScaffoldWindowInsets import com.yalantis.ucrop.UCrop import com.yalantis.ucrop.callback.BitmapCropCallback import com.yalantis.ucrop.view.OverlayView @@ -252,6 +253,7 @@ private fun BackgroundCropScreen( } Scaffold( + contentWindowInsets = adaptiveScaffoldWindowInsets(), topBar = { LargeFlexibleTopAppBar( title = { Text(stringResource(R.string.background_crop_title)) }, diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/BottomBarStyle.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/BottomBarStyle.kt new file mode 100644 index 000000000..22a72a07c --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/BottomBarStyle.kt @@ -0,0 +1,11 @@ +package com.resukisu.resukisu.ui.theme + +enum class BottomBarStyle { + MATERIAL3_EXPRESSIVE, + FLOATING; + + companion object { + fun fromOrdinal(ordinal: Int): BottomBarStyle = + entries.getOrElse(ordinal) { MATERIAL3_EXPRESSIVE } + } +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/Theme.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/Theme.kt index ced4b18a9..068a5c35b 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/Theme.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/theme/Theme.kt @@ -139,6 +139,7 @@ class ThemeConfig( var isEnableBlur by mutableStateOf(false) var isEnableBlurExp by mutableStateOf(false) var isUseBackgroundSeedColor by mutableStateOf(false) + var bottomBarStyle by mutableStateOf(BottomBarStyle.MATERIAL3_EXPRESSIVE) // 主题变化检测 private var lastDarkModeState: Boolean? = null @@ -221,6 +222,11 @@ class BackgroundManager( settings.putBoolean("enable_blur_exp", enable) } + fun saveBottomBarStyle(style: BottomBarStyle) { + config.bottomBarStyle = style + settings.putInt("bottom_bar_style", style.ordinal) + } + fun saveUseBackgroundSeedColor(enable: Boolean) { config.isUseBackgroundSeedColor = enable settings.putBoolean("use_background_seed_color", enable) @@ -283,7 +289,6 @@ class BackgroundManager( } config.backgroundDim = prefs.getFloat("background_dim", 0f).coerceIn(0f, 1f) - config.isEnableBlur = prefs.getBoolean("enable_blur", false) config.isEnableBlurExp = prefs.getBoolean("enable_blur_exp", false) config.isUseBackgroundSeedColor = prefs.getBoolean("use_background_seed_color", false) config.isHighContrastMode = prefs.getBoolean("high_contrast_mode", false) @@ -360,6 +365,7 @@ fun KernelSUTheme( themeRepository = themeRepository, backgroundManager = backgroundManager, cardConfig = cardConfig, + settings = settings, ) // 创建颜色方案 @@ -409,6 +415,7 @@ private fun ThemeInitializer( themeRepository: ThemeRepository, backgroundManager: BackgroundManager, cardConfig: CardConfig, + settings: AppSettingsRepository, ) { val themeChanged = themeConfig.detectThemeChange(systemIsDark) val scope = rememberCoroutineScope() @@ -441,6 +448,8 @@ private fun ThemeInitializer( themeConfig.dynamicPaletteStyle = themeRepository.loadDynamicPaletteStyle( themeConfig.dynamicColorSpec, ) + themeConfig.isEnableBlur = settings.getBoolean("enable_blur", false) + themeConfig.bottomBarStyle = BottomBarStyle.fromOrdinal(settings.getInt("bottom_bar_style", 0)) cardConfig.load() if (!themeConfig.backgroundImageLoaded && !themeConfig.preventBackgroundRefresh) { @@ -618,8 +627,13 @@ fun Modifier.blurEffect( } return LocalBlurState.current?.let { backdrop -> + // 0.8f like haze, for material design without custom background enable + val blurTintAlpha = if (cardConfig.isCustomBackgroundEnabled) + cardConfig.cardAlpha + else 0.8f + val blendColor = - MaterialTheme.colorScheme.surfaceContainer.copy(alpha = cardConfig.cardAlpha) + MaterialTheme.colorScheme.surfaceContainer.copy(alpha = blurTintAlpha) this.then( Modifier diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/util/CompositionProvider.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/util/CompositionProvider.kt index bdc5a7878..e77dea6a1 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/util/CompositionProvider.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/util/CompositionProvider.kt @@ -18,6 +18,7 @@ val LocalBlurState = compositionLocalOf { } val LocalPagerState = compositionLocalOf { error("No pager state") } +val LocalPortraitState = compositionLocalOf { error("No portrait state") } val LocalPagerPage = staticCompositionLocalOf { null } val LocalHandlePageChange = compositionLocalOf<(Int) -> Unit> { error("No handle page change") } val LocalSelectedPage = compositionLocalOf { error("No selected page") } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/util/ScaffoldWindowInsets.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/util/ScaffoldWindowInsets.kt new file mode 100644 index 000000000..5a3bbbf7f --- /dev/null +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/util/ScaffoldWindowInsets.kt @@ -0,0 +1,16 @@ +package com.resukisu.resukisu.ui.util + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.runtime.Composable + +@Composable +fun adaptiveScaffoldWindowInsets(includeBottom: Boolean = true): WindowInsets { + return if (includeBottom) { + WindowInsets.safeDrawing + } else { + WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + } +} diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/AppProfileViewModel.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/AppProfileViewModel.kt index 163ed8744..53138e507 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/AppProfileViewModel.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/AppProfileViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.viewModelScope import com.resukisu.resukisu.domain.model.AppControlAction import com.resukisu.resukisu.domain.model.AppProfile import com.resukisu.resukisu.domain.model.InstalledAppGroup +import com.resukisu.resukisu.domain.model.WEBVIEW_ZYGOTE_UID import com.resukisu.resukisu.domain.usecase.ControlAppUseCase import com.resukisu.resukisu.domain.usecase.GetAppProfileUseCase import com.resukisu.resukisu.domain.usecase.GetAppSepolicyUseCase @@ -76,7 +77,10 @@ class AppProfileViewModel( mutableState.update { it.copy(isLoading = true) } runCatching { val profile = getProfile(packageName, uid) - val loadedProfile = if (profile.allowSu) { + val isSpecial = uid == WEBVIEW_ZYGOTE_UID + val loadedProfile = if (isSpecial) { + profile.copy(allowSu = false) + } else if (profile.allowSu) { profile.copy( rules = runCatching { getSepolicy(packageName) } .getOrDefault(profile.rules) @@ -105,29 +109,37 @@ class AppProfileViewModel( is AppProfileUiAction.Save -> { val previous = mutableState.value.profile - mutableState.update { it.copy(profile = action.profile) } + val isSpecial = uid == WEBVIEW_ZYGOTE_UID + val profileToSave = if (isSpecial) { + action.profile.copy(allowSu = false) + } else { + action.profile + } + mutableState.update { it.copy(profile = profileToSave) } viewModelScope.launch { saveMutex.withLock { - val sepolicyKey = action.profile.rootTemplate ?: action.profile.name - if (action.profile.allowSu && !action.profile.rootUseDefault && - action.profile.rules.isNotEmpty() && - !setSepolicy(sepolicyKey, action.profile.rules) - ) { - rollbackIfCurrent(action.profile, previous) - mutableEvents.emit(AppProfileUiEvent.SepolicyUpdateFailed) - return@withLock + if (!isSpecial) { + val sepolicyKey = profileToSave.rootTemplate ?: profileToSave.name + if (profileToSave.allowSu && !profileToSave.rootUseDefault && + profileToSave.rules.isNotEmpty() && + !setSepolicy(sepolicyKey, profileToSave.rules) + ) { + rollbackIfCurrent(profileToSave, previous) + mutableEvents.emit(AppProfileUiEvent.SepolicyUpdateFailed) + return@withLock + } } - runCatching { setProfile(action.profile) } + runCatching { setProfile(profileToSave) } .onSuccess { saved -> if (saved) { mutableEvents.tryEmit(AppProfileUiEvent.Saved) } else { - rollbackIfCurrent(action.profile, previous) + rollbackIfCurrent(profileToSave, previous) mutableEvents.tryEmit(AppProfileUiEvent.Error()) } } .onFailure { - rollbackIfCurrent(action.profile, previous) + rollbackIfCurrent(profileToSave, previous) mutableEvents.tryEmit(AppProfileUiEvent.Error(it)) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt index 6a277028f..c316199c6 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/HomeViewModel.kt @@ -2,6 +2,9 @@ package com.resukisu.resukisu.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.resukisu.resukisu.data.module.ModuleRepository +import com.resukisu.resukisu.data.packageinfo.SuperUserRepository +import com.resukisu.resukisu.data.shell.KsuCliRepository import com.resukisu.resukisu.data.system.HomeStateRepository import com.resukisu.resukisu.domain.model.HomeDashboardState import com.resukisu.resukisu.domain.model.HomeSystemInfo @@ -9,8 +12,6 @@ import com.resukisu.resukisu.domain.model.ManagerUpdateChannel import com.resukisu.resukisu.domain.usecase.CheckManagerUpdateUseCase import com.resukisu.resukisu.domain.usecase.GetBooleanPreferenceUseCase import com.resukisu.resukisu.domain.usecase.GetHomeBasicInfoUseCase -import com.resukisu.resukisu.domain.usecase.GetHomeModuleOverviewUseCase -import com.resukisu.resukisu.domain.usecase.GetHomeSuperuserCountUseCase import com.resukisu.resukisu.domain.usecase.GetKernelStatusUseCase import com.resukisu.resukisu.domain.usecase.GetManagerRuntimeInfoUseCase import com.resukisu.resukisu.domain.usecase.GetSuSFSStatusUseCase @@ -22,7 +23,10 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -33,6 +37,8 @@ sealed interface HomeUiAction { data object AwaitInitialData : HomeUiAction data class Refresh(val showIndicator: Boolean = true) : HomeUiAction data class SetSimpleMode(val enabled: Boolean) : HomeUiAction + data class SetNavigationBarBadge(val enabled: Boolean) : HomeUiAction + data class SetHomeCardIcons(val enabled: Boolean) : HomeUiAction data class Reboot(val reason: String) : HomeUiAction } @@ -41,21 +47,39 @@ sealed interface HomeUiEvent { } class HomeViewModel( - private val homeStateRepository: HomeStateRepository, + val homeStateRepository: HomeStateRepository, + superUserRepository: SuperUserRepository, + moduleRepository: ModuleRepository, + private val ksuCliRepository: KsuCliRepository, private val checkManagerUpdate: CheckManagerUpdateUseCase, private val getKernelStatus: GetKernelStatusUseCase, private val getManagerRuntimeInfo: GetManagerRuntimeInfoUseCase, private val getSuSFSStatus: GetSuSFSStatusUseCase, private val getBasicInfo: GetHomeBasicInfoUseCase, - private val getModuleOverview: GetHomeModuleOverviewUseCase, - private val getSuperuserCount: GetHomeSuperuserCountUseCase, private val isNetworkAvailable: IsNetworkAvailableUseCase, private val getBooleanPreference: GetBooleanPreferenceUseCase, private val setBooleanPreference: SetBooleanPreferenceUseCase, private val reboot: RebootUseCase, ) : ViewModel() { - val state = homeStateRepository.state - val uiState = state + val uiState = combine( + homeStateRepository.state, + superUserRepository.state, + moduleRepository.installedModules, + ) { homeState, superUserState, moduleState -> + homeState.copy( + systemInfo = homeState.systemInfo.copy( + moduleCount = moduleState.modules.size, + superuserCount = superUserState.groups.filter { it.allowSu }.size, + zygiskImplement = ksuCliRepository.getZygiskImplement(), + metaModuleImplement = ksuCliRepository.getMetaModuleImplement(), + ) + ) + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000), + initialValue = HomeUiState() + ) + private val mutableEvents = MutableSharedFlow(extraBufferCapacity = 1) val events: SharedFlow = mutableEvents.asSharedFlow() @@ -75,7 +99,7 @@ class HomeViewModel( fun refreshData(refreshUI: Boolean = false): Job { if (!refreshUI) { refreshJob?.takeIf(Job::isActive)?.let { return it } - if (state.value.isInitialDataLoaded) return completedJob() + if (uiState.value.isInitialDataLoaded) return completedJob() } refreshManagerUpdates(force = refreshUI) return viewModelScope.launch { @@ -84,19 +108,21 @@ class HomeViewModel( try { applyUserSettings() val kernelStatus = runCatching { getKernelStatus() } - .getOrElse { state.value.systemStatus } + .getOrElse { uiState.value.systemStatus } homeStateRepository.update { it.copy(systemStatus = kernelStatus, isCoreDataLoaded = true) } - val basic = async { getBasicInfo(kernelStatus.managerUAPIVersion) } - val module = async { getModuleOverview() } - val superusers = async { getSuperuserCount() } + val includeSelinuxStatus = !uiState.value.isInitialDataLoaded + val basic = async { + getBasicInfo( + managerUapiVersion = kernelStatus.managerUAPIVersion, + includeSelinuxStatus = includeSelinuxStatus, + ) + } val managers = async { getManagerRuntimeInfo() } val susfs = async { getSuSFSStatus() } val basicInfo = basic.await() - val moduleInfo = module.await() - val superuserCount = superusers.await() val managerInfo = managers.await() val susfsInfo = susfs.await() homeStateRepository.update { current -> @@ -106,17 +132,15 @@ class HomeViewModel( androidVersion = basicInfo.androidVersion, deviceModel = basicInfo.deviceModel, managerVersion = basicInfo.managerVersion, - selinuxStatus = basicInfo.selinuxStatus, + selinuxStatus = current.systemInfo.selinuxStatus.ifEmpty { + basicInfo.selinuxStatus + }, susfsEnabled = susfsInfo.enabled, susfsVersionSupported = susfsInfo.enabled, susfsVersion = susfsInfo.version, susfsFeatures = susfsInfo.enabledFeatures, - superuserCount = superuserCount, - moduleCount = moduleInfo.count, managersList = managerInfo, isDynamicSignEnabled = managerInfo.dynamicSignatureEnabled, - zygiskImplement = moduleInfo.zygiskImplementation, - metaModuleImplement = moduleInfo.metaModuleImplementation, seccompStatus = basicInfo.seccompStatus, ), isInitialDataLoaded = true, @@ -138,11 +162,23 @@ class HomeViewModel( fun handleSimpleModeChange(enabled: Boolean) = updatePreference(PREF_SIMPLE_MODE, enabled) { it.copy(isSimpleMode = enabled) } + fun handleNavigationBarBadgeChange(enabled: Boolean) = + updatePreference(PREF_SHOW_NAVIGATION_BAR_BADGE, enabled) { + it.copy(showNavigationBarBadge = enabled) + } + + fun handleHomeCardIconsChange(enabled: Boolean) = + updatePreference(PREF_SHOW_HOME_CARD_ICONS, enabled) { + it.copy(showHomeCardIcons = enabled) + } + fun dispatch(action: HomeUiAction) { when (action) { HomeUiAction.AwaitInitialData -> viewModelScope.launch { awaitInitialData() } is HomeUiAction.Refresh -> refreshData(action.showIndicator) is HomeUiAction.SetSimpleMode -> handleSimpleModeChange(action.enabled) + is HomeUiAction.SetNavigationBarBadge -> handleNavigationBarBadgeChange(action.enabled) + is HomeUiAction.SetHomeCardIcons -> handleHomeCardIconsChange(action.enabled) is HomeUiAction.Reboot -> viewModelScope.launch { reboot(action.reason).onFailure { mutableEvents.tryEmit(HomeUiEvent.Error(it.message.orEmpty())) @@ -189,6 +225,11 @@ class HomeViewModel( homeStateRepository.update { it.copy( isSimpleMode = getBooleanPreference(PREF_SIMPLE_MODE), + showNavigationBarBadge = getBooleanPreference( + PREF_SHOW_NAVIGATION_BAR_BADGE, + true, + ), + showHomeCardIcons = getBooleanPreference(PREF_SHOW_HOME_CARD_ICONS), ) } } @@ -208,5 +249,7 @@ class HomeViewModel( const val PREF_CHECK_UPDATE = "check_update" const val PREF_CHECK_BETA_UPDATE = "check_beta_update" const val PREF_SIMPLE_MODE = "is_simple_mode" + const val PREF_SHOW_NAVIGATION_BAR_BADGE = "show_navigation_bar_badge" + const val PREF_SHOW_HOME_CARD_ICONS = "show_home_card_icons" } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SettingsViewModel.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SettingsViewModel.kt index 644990c8b..9276eff49 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SettingsViewModel.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SettingsViewModel.kt @@ -17,7 +17,6 @@ import com.resukisu.resukisu.domain.usecase.SetDefaultUmountModulesUseCase import com.resukisu.resukisu.domain.usecase.SetKernelUmountEnabledUseCase import com.resukisu.resukisu.domain.usecase.SetSelinuxHideEnabledUseCase import com.resukisu.resukisu.domain.usecase.SetSuEnabledUseCase -import com.resukisu.resukisu.domain.usecase.SetWebViewZygoteUmountEnabledUseCase import com.resukisu.resukisu.domain.usecase.UpdateAppearanceUseCase import com.resukisu.resukisu.domain.usecase.UpdatePlatformSettingUseCase import kotlinx.coroutines.flow.MutableSharedFlow @@ -97,8 +96,6 @@ data class SettingsUiState( val isSuLogEnabled: Boolean = false, val selinuxHideStatus: String = "", val isSelinuxHideEnabled: Boolean = false, - val webViewZygoteUmountStatus: String = "", - val isWebViewZygoteUmountEnabled: Boolean = false, val defaultUmountModules: Boolean = false, val useBuiltinMonoFont: Boolean = false, ) @@ -139,7 +136,6 @@ sealed interface SettingsUiAction { data class SetAdbRoot(val enabled: Boolean) : SettingsUiAction data class SetSuLog(val enabled: Boolean) : SettingsUiAction data class SetDefaultUmountModules(val enabled: Boolean) : SettingsUiAction - data class SetWebViewZygoteUmountEnabled(val enabled: Boolean) : SettingsUiAction } sealed interface SettingsUiEvent { @@ -159,7 +155,6 @@ class SettingsViewModel( private val setSuLogEnabled: ConfigureSuLogUseCase, private val setSelinuxHideEnabled: SetSelinuxHideEnabledUseCase, private val setDefaultUmountModules: SetDefaultUmountModulesUseCase, - private val setWebViewZygoteUmountEnabled: SetWebViewZygoteUmountEnabledUseCase, ) : ViewModel() { private val mutableState = MutableStateFlow(SettingsUiState()) val state: StateFlow = mutableState.asStateFlow() @@ -199,8 +194,6 @@ fun initialize() { isSuLogEnabled = features.suLogEnabled, selinuxHideStatus = platform.selinuxHideStatus, isSelinuxHideEnabled = features.selinuxHideEnabled, - webViewZygoteUmountStatus = platform.webViewZygoteUmountStatus, - isWebViewZygoteUmountEnabled = features.webViewZygoteUmountEnabled, defaultUmountModules = features.defaultUmountModules, ) } @@ -408,15 +401,6 @@ fun initialize() { } } - fun handleWebViewZygoteUmountChange(checked: Boolean) { - viewModelScope.launch { - if (setWebViewZygoteUmountEnabled(checked)) { - mutableState.update { it.copy( isWebViewZygoteUmountEnabled = checked) } - } - } - } - - fun dispatch(action: SettingsUiAction) { when (action) { SettingsUiAction.Initialize -> initialize() @@ -458,7 +442,6 @@ fun dispatch(action: SettingsUiAction) { is SettingsUiAction.SetSuLog -> handleSuLogChange(action.enabled) is SettingsUiAction.SetDefaultUmountModules -> handleDefaultUmountModulesChange(action.enabled) - is SettingsUiAction.SetWebViewZygoteUmountEnabled -> handleWebViewZygoteUmountChange(action.enabled) } } diff --git a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt index 879173bd3..000f05e57 100644 --- a/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/resukisu/resukisu/ui/viewmodel/SuperUserViewModel.kt @@ -7,6 +7,7 @@ import com.resukisu.resukisu.domain.model.AllowlistOperationResult import com.resukisu.resukisu.domain.model.InstalledAppGroup import com.resukisu.resukisu.domain.usecase.BackupAllowlistUseCase import com.resukisu.resukisu.domain.usecase.GetBooleanPreferenceUseCase +import com.resukisu.resukisu.domain.usecase.GetManagerRuntimeInfoUseCase import com.resukisu.resukisu.domain.usecase.GetStringPreferenceUseCase import com.resukisu.resukisu.domain.usecase.ImportAllowlistUseCase import com.resukisu.resukisu.domain.usecase.ObserveSuperUserStateUseCase @@ -27,16 +28,14 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch enum class SortType(val displayNameRes: Int, val persistKey: String) { - NAME_ASC(R.string.sort_name_asc, "NAME_ASC"), - NAME_DESC(R.string.sort_name_desc, "NAME_DESC"), - INSTALL_TIME_NEW(R.string.sort_install_time_new, "INSTALL_TIME_NEW"), - INSTALL_TIME_OLD(R.string.sort_install_time_old, "INSTALL_TIME_OLD"), - SIZE_DESC(R.string.sort_size_desc, "SIZE_DESC"), - SIZE_ASC(R.string.sort_size_asc, "SIZE_ASC"), + NAME(R.string.sort_name, "NAME"), + INSTALL_TIME(R.string.sort_install_time, "INSTALL_TIME"), + UPDATE_TIME(R.string.sort_update_time, "UPDATE_TIME"), + SIZE(R.string.sort_size, "SIZE"), USAGE_FREQ(R.string.sort_usage_freq, "USAGE_FREQ"); companion object { - fun fromPersistKey(key: String): SortType = entries.find { it.persistKey == key } ?: NAME_ASC + fun fromPersistKey(key: String): SortType = entries.find { it.persistKey == key } ?: NAME } } @@ -44,7 +43,9 @@ data class SuperUserUiState( val appGroupList: List = emptyList(), val search: String = "", val showSystemApps: Boolean = false, - val currentSortType: SortType = SortType.NAME_ASC, + val currentSortType: SortType = SortType.NAME, + val reverseOrder: Boolean = false, + val managerUids: Set = emptySet(), val isRefreshing: Boolean = false, ) @@ -55,6 +56,7 @@ sealed interface SuperUserUiAction { data class Search(val query: String) : SuperUserUiAction data class SetShowSystemApps(val enabled: Boolean) : SuperUserUiAction data class SetSort(val sortType: SortType) : SuperUserUiAction + data class SetReverseOrder(val enabled: Boolean) : SuperUserUiAction data object StatusChanged : SuperUserUiAction } @@ -69,7 +71,8 @@ sealed interface SuperUserUiEvent { private data class SuperUserControls( val search: String = "", val showSystemApps: Boolean = false, - val sortType: SortType = SortType.NAME_ASC, + val sortType: SortType = SortType.NAME, + val reverseOrder: Boolean = false, ) class SuperUserViewModel( @@ -82,32 +85,48 @@ class SuperUserViewModel( private val setBooleanPreference: SetBooleanPreferenceUseCase, private val setStringPreference: SetStringPreferenceUseCase, private val transliterateText: TransliterateTextUseCase, + private val getManagerRuntimeInfo: GetManagerRuntimeInfoUseCase, ) : ViewModel() { private val sourceState = observeSuperUserState() private val controls = MutableStateFlow( SuperUserControls( showSystemApps = getBooleanPreference(KEY_SHOW_SYSTEM_APPS, false), sortType = SortType.fromPersistKey( - getStringPreference(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) - ?: SortType.NAME_ASC.persistKey + getStringPreference(KEY_CURRENT_SORT_TYPE, SortType.NAME.persistKey) + ?: SortType.NAME.persistKey ), + reverseOrder = getBooleanPreference(KEY_REVERSE_ORDER, false), ) ) private val mutableEvents = MutableSharedFlow(extraBufferCapacity = 1) private var refreshJob: Job? = null val events: SharedFlow = mutableEvents.asSharedFlow() - val state: StateFlow = combine(sourceState, controls) { source, local -> + private val managerUids = MutableStateFlow>(emptySet()) + + init { + viewModelScope.launch { + val info = runCatching { getManagerRuntimeInfo() }.getOrNull() + managerUids.value = info?.managers?.map { it.uid }?.toSet().orEmpty() + } + } + + val state: StateFlow = combine( + sourceState, controls, managerUids, + ) { source, local, uids -> SuperUserUiState( appGroupList = buildAppGroupList( groups = source.groups, search = local.search, showSystemApps = local.showSystemApps, currentSortType = local.sortType, + reverseOrder = local.reverseOrder, ), search = local.search, showSystemApps = local.showSystemApps, currentSortType = local.sortType, + reverseOrder = local.reverseOrder, + managerUids = uids, isRefreshing = source.refreshing, ) }.stateIn(viewModelScope, SharingStarted.Eagerly, SuperUserUiState()) @@ -151,6 +170,11 @@ class SuperUserViewModel( controls.value = controls.value.copy(sortType = action.sortType) } + is SuperUserUiAction.SetReverseOrder -> { + setBooleanPreference(KEY_REVERSE_ORDER, action.enabled) + controls.value = controls.value.copy(reverseOrder = action.enabled) + } + SuperUserUiAction.StatusChanged -> notifySuperuserStatusChanged() } } @@ -177,33 +201,40 @@ class SuperUserViewModel( search: String, showSystemApps: Boolean, currentSortType: SortType, + reverseOrder: Boolean, ): List = groups .filter { group -> group.apps.any { app -> app.label.contains(search, true) || - app.packageName.contains(search, true) || + app.displayIdentifier.contains(search, true) || transliterateText(app.label).contains(search, true) } } .filter { group -> - group.uid == 2000 || showSystemApps || group.apps.any { !it.isSystem } + group.isWebViewZygote || group.uid == 2000 || showSystemApps || group.apps.any { !it.isSystem } } .sortedWith { first, second -> val priority = groupPriority(first).compareTo(groupPriority(second)) if (priority != 0) { priority } else { - when (currentSortType) { - SortType.NAME_ASC -> first.mainApp.label.compareTo(second.mainApp.label, true) - SortType.NAME_DESC -> second.mainApp.label.compareTo(first.mainApp.label, true) - SortType.INSTALL_TIME_NEW -> - second.mainApp.firstInstallTime.compareTo(first.mainApp.firstInstallTime) + val base = when (currentSortType) { + SortType.NAME -> + first.mainApp.label.compareTo(second.mainApp.label, true) - SortType.INSTALL_TIME_OLD -> + SortType.INSTALL_TIME -> first.mainApp.firstInstallTime.compareTo(second.mainApp.firstInstallTime) - else -> first.mainApp.label.compareTo(second.mainApp.label, true) + SortType.UPDATE_TIME -> + first.mainApp.lastUpdateTime.compareTo(second.mainApp.lastUpdateTime) + + SortType.SIZE -> + first.mainApp.label.compareTo(second.mainApp.label, true) + + SortType.USAGE_FREQ -> + first.mainApp.label.compareTo(second.mainApp.label, true) } + if (reverseOrder) -base else base } } @@ -217,5 +248,6 @@ class SuperUserViewModel( private companion object { const val KEY_SHOW_SYSTEM_APPS = "show_system_apps" const val KEY_CURRENT_SORT_TYPE = "current_sort_type" + const val KEY_REVERSE_ORDER = "reverse_order" } } diff --git a/manager/app/src/main/res/font/monospace.xml b/manager/app/src/main/res/font/monospace.xml deleted file mode 100644 index ea8dcd70f..000000000 --- a/manager/app/src/main/res/font/monospace.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/manager/app/src/main/res/values-ar/strings.xml b/manager/app/src/main/res/values-ar/strings.xml index 181333424..bcedccc8b 100644 --- a/manager/app/src/main/res/values-ar/strings.xml +++ b/manager/app/src/main/res/values-ar/strings.xml @@ -287,12 +287,6 @@ الإضافة %s معطلّة، او بإنتظار الإزالة تعديل ظلمة الخلفية تحتاج لـ Metamodule - ترتيب اسماء تصاعدي - الاسم تنازليًا - وقت التثبيت (جديد) - وقت التثبيت (قديم) - ترتيب تنازلي حسب الحجم - ترتيب تصاعدي حسب الحجم تكرار الاستخدام لا يوجد تطبيق في هذه الفئة مستمر diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index 268a80185..c92b8ca1b 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -6,7 +6,7 @@ Appuyer ici pour installer En cours d\'exécution Non pris en charge - Aucun pilote KernelSU détecté dans le noyau installé sur cet appareil, pas le bon noyau ? + Aucun pilote KernelSU détecté dans le noyau installé sur cet appareil, pas le bon noyau ? Noyau Version de SuSFS Version du gestionnaire @@ -31,9 +31,9 @@ Redémarrer en mode téléchargement Redémarrer en mode EDL À propos - Désinstaller le module %s ? + Désinstaller le module %s ? %s a été désinstallé - Échec de la désinstallation : %s + Échec de la désinstallation : %s Auteur Afficher les applications système Envoi des journaux @@ -41,7 +41,7 @@ Continuer (%1$d) Jailbreak automatique Redémarrer pour appliquer les modifications - Modules indisponibles en raison d\'un conflit avec Magisk ! + Modules indisponibles en raison d\'un conflit avec Magisk ! Découvrir KernelSU Découvrir comment installer KernelSU et utiliser les modules Nous soutenir @@ -81,7 +81,7 @@ Importer/exporter Importer à partir du presse-papiers Exporter vers le presse-papiers - Aucun modèle local à exporter ! + Aucun modèle local à exporter ! Importation réussie Synchroniser les modèles en ligne Échec de l\'enregistrement du modèle @@ -92,7 +92,7 @@ Installation directe (recommandé) Sélectionner un fichier d\'image à modifier Installer dans l\'emplacement inactif (après OTA) - Le démarrage de cet appareil sera **FORCÉ** sur l\'emplacement inactif actuel après un redémarrage ! \nN\'utiliser cette option qu\'une fois la mise à jour OTA terminée. \nContinuer ? + Le démarrage de cet appareil sera **FORCÉ** sur l\'emplacement inactif actuel après un redémarrage ! \nN\'utiliser cette option qu\'une fois la mise à jour OTA terminée. \nContinuer ? Suivant Image de la partition %1$s recommandée Sélectionner une KMI @@ -102,7 +102,7 @@ Restaurer l\'image d\'origine Désinstalle KernelSU temporairement et rétablit l\'état d\'origine au prochain redémarrage Désinstallation complète et permanente de KernelSU (root et tous les modules) - Restaure l\'image d\'usine (s\'il en existe une sauvegarde). Généralement utilisé avant une mise à jour OTA ; pour désinstaller KernelSU, utiliser plutôt l\'option \"Désinstaller définitivement\" + Restaure l\'image d\'usine (s\'il en existe une sauvegarde). Généralement utilisé avant une mise à jour OTA ; pour désinstaller KernelSU, utiliser plutôt l\'option \"Désinstaller définitivement\" Flashage en cours Flashage réussi Échec du flashage @@ -113,11 +113,11 @@ Confirmer Annuler Sauvegarde de la liste d\'autorisations root réussie - Échec de la sauvegarde de la liste d\'autorisations root : %1$s + Échec de la sauvegarde de la liste d\'autorisations root : %1$s Confirmer la restauration de la liste d\'autorisations root - Cette opération écrasera la liste des applications autorisées à s\'exécuter en tant que root actuelle. Continuer ? + Cette opération écrasera la liste des applications autorisées à s\'exécuter en tant que root actuelle. Continuer ? Restauration de la liste d\'autorisations root réussie - Échec de la restauration de la liste d\'autorisations root : %1$s + Échec de la restauration de la liste d\'autorisations root : %1$s Sauvegarder la liste d\'autorisations root Restaurer la liste d\'autorisations root Arrière-plan personnalisé @@ -126,7 +126,7 @@ Modèle de l\'appareil Octroi des privilèges superutilisateur à %s non autorisé Commande su classique - Permet aux applications ayant l\'autorisation superutilisateur dans le profil d\'application d\'obtenir un shell superutilisateur en exécutant /system/bin/su ; effectif uniquement pour les nouveaux processus. + Permet aux applications ayant l\'autorisation superutilisateur dans le profil d\'application d\'obtenir un shell superutilisateur en exécutant /system/bin/su ; effectif uniquement pour les nouveaux processus. Démontage du noyau Comportement de démontage des modules au niveau du noyau contrôlé par KernelSU dans le profil d\'application Mode simplifié @@ -150,7 +150,7 @@ Flashage terminé Sélection de l\'emplacement de flashage Sélectionner l\'emplacement cible pour le flashage de l\'image de démarrage - Emplacement sélectionné : %1$s + Emplacement sélectionné : %1$s Échec de la copie Erreur inconnue Échec du flashage @@ -176,7 +176,7 @@ Personnalisable Appliquer les paramètres de densité Modification de la densité d\'affichage - Passer la densité d\'affichage de l\'application de %1$d PPP à %2$d PPP ? + Passer la densité d\'affichage de l\'application de %1$d PPP à %2$d PPP ? Langue de l\'application Langue du système code d\'erreur @@ -218,20 +218,20 @@ Licences open source Affiche la liste des bibliothèques tierces en open source et leurs licences Consulter le site - Licence : %s + Licence : %s Aucun texte de licence disponible. - Désinstaller le module %s ? Cette action affectera tous les modules et certaines fonctionnalités fournies par le métamodule (comme le montage de volumes) ne seront plus disponibles. + Désinstaller le module %s ? Cette action affectera tous les modules et certaines fonctionnalités fournies par le métamodule (comme le montage de volumes) ne seront plus disponibles. Version Mode jailbreak Jailbreak Le jailbreak a peut-être échoué, consulter les journaux - Appareil en **mode jailbreak**. Flasher une partition sur un appareil avec un **bootloader verrouillé** désactivera AVB (Android Verified Boot) et pourrait **empêcher le démarrage** de l\'appareil.\n\nVérifier que le bootloader de cet appareil est déverrouillé avant de continuer ! + Appareil en **mode jailbreak**. Flasher une partition sur un appareil avec un **bootloader verrouillé** désactivera AVB (Android Verified Boot) et pourrait **empêcher le démarrage** de l\'appareil.\n\nVérifier que le bootloader de cet appareil est déverrouillé avant de continuer ! Utilise automatiquement Magica pour l\'élévation de privilèges lorsque SELinux en mode permissif est détecté au démarrage. Nécessite l\'autorisation de démarrage automatique pour cette application. Exécute le démon adbd avec les privilèges root Dissimulation des modifications SELinux Empêche les applications de détecter les modifications SELinux Redémarrer pour appliquer les modifications - Erreur : %d + Erreur : %d Espace de noms de montage Hérité Global @@ -252,7 +252,7 @@ Recherche automatiquement les mises à jour du gestionnaire Recherche des mises à jour de modules Recherche automatiquement les mises à jour des modules installés - Ceci est une version de débogage. Ne PAS utiliser en production ! + Ceci est une version de débogage. Ne PAS utiliser en production ! Sélectionner la partition Utiliser un fichier local d\'image LKM Seuls les fichier .ko sont pris en charge @@ -276,7 +276,7 @@ Affiche une ombre autour du texte Transparence de la carte Fonctionnalité non prise en charge par le noyau - Emplacement actuel du système par défaut : %1$s + Emplacement actuel du système par défaut : %1$s Densité d\'affichage définie à %1$d PPP Ajustement de la luminosité de l\'arrière-plan https://kernelsu.org/guide/what-is-kernelsu.html @@ -307,12 +307,6 @@ Module %s désactivé ou en attente de suppression Métamodule requis Ce module tente de monter le volume /system, montage géré par métamodule. Sans métamodule, ce module risque de ne pas fonctionner - Par nom - Par nom (décroissant) - Par date d\'installation (décroissant) - Par date d\'installation - Par taille (décroissant) - Par taille Par fréquence d\'utilisation Aucune application dans cette catégorie Persistante @@ -325,10 +319,10 @@ Affiche des informations complémentaires sur les modules telles que les URL des fichiers JSON de mise à jour Ajouter Configuration de Kstat - - add_sus_kstat_statically : Statistiques statiques des fichiers/répertoires - - add_sus_kstat : Ajoute le chemin avant le montage de liaison, en stockant les statistiques d\'origine - - update_sus_kstat : Met à jour l\'inode cible, conserve la taille et le nombre de blocs - - update_sus_kstat_full_clone : Met à jour l\'inode uniquement, conserve les autres valeurs d\'origine + - add_sus_kstat_statically : Statistiques statiques des fichiers/répertoires + - add_sus_kstat : Ajoute le chemin avant le montage de liaison, en stockant les statistiques d\'origine + - update_sus_kstat : Met à jour l\'inode cible, conserve la taille et le nombre de blocs + - update_sus_kstat_full_clone : Met à jour l\'inode uniquement, conserve les autres valeurs d\'origine Crée une sauvegarde de toutes les configurations de SuSFS. Le fichier de sauvegarde contiendra tous les paramètres, chemins de fichiers/répertoires, et configurations Restaurer Restaure les configurations de SuSFS depuis un fichier de sauvegarde. Tous les paramètres actuels seront écrasés @@ -336,7 +330,7 @@ Rechercher des applications Rechercher des modules Configuration du gestionnaire dynamique - Activé (Taille : %s) + Activé (Taille : %s) Désactivé Taille de la signature du gestionnaire dynamique Hachage de la signature du gestionnaire dynamique @@ -369,22 +363,22 @@ Appareils pris en charge Versions Informations d\'états - Superutilisateur : %1$d, Modules : %2$d + Superutilisateur : %1$d, Modules : %2$d Version du pilote de noyau Aucun contenu trouvé Nouvelle version bêta %1$d disponible, cliquer ici pour l\'installer Échec de recherche de version bêta. Faire glisser votre doigt vers le bas pour actualiser et réessayer. Mise à jour de version stable Mise à jour de version bêta - Version : %1$s (%2$d)\nArchitecture : %3$s + Version : %1$s (%2$d)\nArchitecture : %3$s Recherche de mises à jour de version bêta du gestionnaire Recherche automatiquement les versions bêta du gestionnaire depuis la branche principale du dépôt GitHub Hors connexion Réessayer Vérifier la connexion Internet et réessayer - Installer le module %s ? + Installer le module %s ? Sélectionner les éléments à installer - Taille : %1$s, Téléchargés : %2$s + Taille : %1$s, Téléchargés : %2$s Aucun élément sélectionné Installé Ouvrir la page d\'accueil du module dans le navigateur @@ -417,9 +411,9 @@ Annuler Installer Autorisation d\'affichage des notifications requise pour afficher la progression de téléchargement. - Échec du téléchargement : Autorisation d\'affichage des notifications requise + Échec du téléchargement : Autorisation d\'affichage des notifications requise Autorisation d\'écriture sur stockage externe requise pour écrire le fichier. - Échec du téléchargement : Autorisation d\'écriture sur stockage externe requise + Échec du téléchargement : Autorisation d\'écriture sur stockage externe requise Gestion de la signature du gestionnaire dynamique Configuration actuelle Configuration manuelle de la signature @@ -427,13 +421,11 @@ Effacer la configuration Désactive et supprime la configuration du gestionnaire dynamique %1$s\ngéré par %2$s - Confirmer l\'autorisation ? + Confirmer l\'autorisation ? L\'application sélectionnée deviendra le gestionnaire dynamique. Si un gestionnaire dynamique est déjà configuré, il sera immédiatement invalidé.\n\nCette action va lui octroyer le niveau d\'autorisation le plus élevé pour cet appareil. Si vous ne comprenez pas ce que cela signifie, annulez. Effacement de la configuration du gestionnaire dynamique - Effacer la configuration du gestionnaire dynamique ? + Effacer la configuration du gestionnaire dynamique ? Gestionnaires dynamiques - La version installée du gestionnaire KernelSU (%1$d) est trop ancienne pour que KernelSU fonctionne correctement. La mise à jour du gestionnaire en version %2$d ou ultérieure est nécessaire ! - Version de KernelSU (%1$d) obsolète pour garantir un fonctionnement correct du gestionnaire. Mise à jour en version %2$d ou ultérieure requise ! Gestion des chemins de démontage Gère les chemins de démontage du noyau Aucun chemin de démontage existant @@ -442,7 +434,7 @@ Drapeaux de démontage 0 = démontage normal, 2 = MNT_DETACH Confirmer la suppression - Supprimer le chemin %s ? + Supprimer le chemin %s ? Chemin de démontage ajouté Chemin de démontage supprimé Échec de l\'opération @@ -452,8 +444,8 @@ non monté car le métamodule est en cours de désinstallation non monté car le métamodule n\'est pas installé Masque le fichier réel mappé en mémoire de divers mappings dans /proc/self/ - Masque les chemins réels des fichiers associés aux mappages mémoire dans /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Remarque : cette fonctionnalité ne permet pas de masquer les mappages mémoire anonymes, ni les hooks d\'interception \"inline\" ou PLT générés par la bibliothèque injectée elle-même - Avertissement important : pour les applications dotées de mécanismes de détection d’injection bien implémentés, cette fonctionnalité peut ne pas contourner efficacement la détection + Masque les chemins réels des fichiers associés aux mappages mémoire dans /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Remarque : cette fonctionnalité ne permet pas de masquer les mappages mémoire anonymes, ni les hooks d\'interception \"inline\" ou PLT générés par la bibliothèque injectée elle-même + Avertissement important : pour les applications dotées de mécanismes de détection d’injection bien implémentés, cette fonctionnalité peut ne pas contourner efficacement la détection Commencer par identifier le PID et l\'UID de l\'application cible à l\'aide de la commande **ps -enf**, puis vérifier les chemins correspondants dans /proc/<pid>/maps et comparer les numéros de périphériques avec ceux figurant dans /proc/1/mountinfo pour garantir la cohérence. La fonctionnalité de masquage du mappage ne peut fonctionner correctement que si les numéros de périphériques correspondent Slot A Slot B @@ -485,7 +477,7 @@ Informations d\'emplacements de démarrage non trouvées Fichiers cmdline/bootconfig Chemin d\'accès aux faux fichiers cmdline et bootconfig - Actuel : %1$s + Actuel : %1$s Non défini Ajouter une entrée Aucune entrée @@ -498,7 +490,7 @@ %1$d importés, %2$d échecs Importer depuis un fichier Échec de lecture du fichier - Fichier incorrect : ne contient pas de texte au format UTF-8 + Fichier incorrect : ne contient pas de texte au format UTF-8 Chemin SUS normal Chemin de boucles SUS Boucle @@ -512,7 +504,7 @@ Chemin cible Schéma d\'UID Ouverture de la redirection du chemin cible vers un chemin défini par l\'utilisateur pour les processus correspondant au schéma d\'UID sélectionné. - Schémas d\'UID:\n0 : Processus non-applicatifs (UID < 10000)\n1 : Processus root à UID 0 en dehors du domaine SU\n2 : Tous les processus non-SU (utiliser avec précaution)\n3 : Processus des applications non montées avec UID ≥ 10000 (utiliser avec précaution)\n4 : Tous les processus non montés, y compris la plupart des processus créés par init (utiliser avec précaution) + Schémas d\'UID:\n0 : Processus non-applicatifs (UID < 10000)\n1 : Processus root à UID 0 en dehors du domaine SU\n2 : Tous les processus non-SU (utiliser avec précaution)\n3 : Processus des applications non montées avec UID ≥ 10000 (utiliser avec précaution)\n4 : Tous les processus non montés, y compris la plupart des processus créés par init (utiliser avec précaution) %1$s · %2$s Non applicatif Root sauf SU @@ -531,12 +523,24 @@ Échec de l\'exportation de la configuration Échec de l\'importation de la configuration Confirmation de l\'importation - Cette opération écrasera la configuration SuSFS actuelle. Continuer ? + Cette opération écrasera la configuration SuSFS actuelle. Continuer ? Importer Configuration par défaut Supprime la configuration SuSFS actuelle et restaure la configuration par défaut - Remarques importantes :\n• Les chemins cible et redirigé doivent exister avant l’ajout d’une entrée\n• les permissions SELinux pour les deux chemins doivent être configurées.\n• La redirection affecte uniquement les processus correspondant au schéma d\'UID sélectionné. - Empêche les fuites d\'informations du processus WebView, mais peut causer le dysfonctionnement de certains modules. Redémarrer pour appliquer les modifications - Démontage pour WebView - + Remarques importantes :\n• Les chemins cible et redirigé doivent exister avant l’ajout d’une entrée\n• les permissions SELinux pour les deux chemins doivent être configurées.\n• La redirection affecte uniquement les processus correspondant au schéma d\'UID sélectionné. + + Badges de la barre de navigation + Affiche le nombre d\'applications superutilisateur et de modules dans la barre de navigation + Icônes dans le panneau d\'accueil + Ajoute des icônes aux cartes du panneau d\'accueil + Par nom + Par date d\'installation + Par date de mise à jour + Par taille + Ordre inverse + Mise à jour du gestionnaire requise + Mise à jour du noyau requise. Appuyer ici pour installer. + Mise à jour du noyau requise + Bar de navigation flottante + Barre de navigation flottante dans le style de celle d\'appel. diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index 41891d914..d40608309 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -278,12 +278,6 @@ Kernel telepítés Metamodul szükséges Ez a modul adatot akar csatolni a fájlrendszer /system névterébe, ehhez metamodul szükséges. Másképp a megfelelő működés nem garantált - ABC sorrendben növekvő - ABC sorrendben csökkenő - Telepítés ideje szerint (újabbak előre) - Telepítés ideje szerint (régebbiek előre) - Tárhelyben elfoglalt méretük szerint csökkenő - Tárhelyben elfoglalt méretük szerint növekvő Leggyakrabban használtak Ebben a kategóriában applikáció nem található Keresés naplóban @@ -353,8 +347,6 @@ Fejlesztő(k) Leírás Támogatott készülékek - Úgy tűnik a root menedzser verzió %1$d túl alacsony. Jelen esetben a KernelSU implementáció megfelelő működéséhez minimum %2$d verzió szükséges! - Úgy tűnik a kernelbe telepített KernelSU implementáció verzió %1$d túl alacsony. Jelen esetben a root menedzser megfelelő működéséhez minimum %2$d verzió szükséges! Leválasztandó elérési útvonalak Fájlrendszer elérési útvonalak kezelése a leválasztáshoz Nincs megadva elérési útvonal @@ -536,7 +528,19 @@ SuSFS Menedzser Beépített SuSFS Menedzser engedélyezése vagy letiltása. Valamely harmadik féltől származó modullal összeférhetetlen lehet. Beépített SuSFS Menedzser letiltva. Engedélyezd, vagy telepíts egy harmadik féltől származó modult a SuSFS funkciók használatához. - WebView leválasztás - Megakadályozza az információszivárgást a WebView folyamatból, de modulok működését zavarhatja. Az alkalmazáshoz újraindítás szükséges + Root menedzser frissítése szükséges + Kernel frissítése szükséges. Kattints a telepítéshez. + Kernel frissítése szükséges + Kezdőlap kártyáinak ikonosítása + Ikonok megjelenítése a kezdőlap kártyáin + Superuser folyamatok és modulok számának megjelenítése a navigációs sávon + Bélyegek a navigációs sávon + Méret szerint + Fordított sorrendben + Legutóbbi frissítés ideje szerint + Telepítés ideje szerint + Név szerint + Lebegő alsó sáv + Apple iOS stílusú lebegő alsó sáv használata. diff --git a/manager/app/src/main/res/values-in/strings.xml b/manager/app/src/main/res/values-in/strings.xml index 55c3cbd15..d33138bdb 100644 --- a/manager/app/src/main/res/values-in/strings.xml +++ b/manager/app/src/main/res/values-in/strings.xml @@ -193,12 +193,6 @@ Modul yang dipasang %1$d/%2$d %d Gagal memasang modul baru Memasang Kernel - Urutan nama dari A-Z - Urutan nama dari Z-A - Waktu pemasangan (baru) - Waktu pemasangan (lama) - Urutan ukuran dari terkecil - Urutan ukuran dari terbesar Frekuensi penggunaan Tidak ada aplikasi dalam kategori ini Konfigurasi SuSFS @@ -362,8 +356,6 @@ Sembunyikan jalur asli dari pemetaan memori di /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Catatan: Fitur ini tidak mendukung penyembunyian pemetaan memori anonim, serta tidak dapat menyembunyikan inline hook atau PLT hook yang disebabkan oleh library yang diinjeksi itu sendiri Pemberitahuan Penting: Untuk aplikasi dengan mekanisme deteksi injeksi yang dirancang dengan baik, fitur ini mungkin tidak akan efektif untuk melewati deteksi Pertama, temukan PID dan UID aplikasi target menggunakan ps -enf, lalu cek jalur yang relevan di /proc//maps dan bandingkan nomor perangkat dengan yang ada di /proc/1/mountinfo untuk memastikan konsistensi. Fungsi penyembunyian pemetaan memori hanya dapat bekerja dengan benar jika nomor perangkat tersebut cocok - Versi manajer (%1$d) terlalu rendah. Perbarui ke versi %2$d atau lebih tinggi agar KernelSU berjalan normal! - Versi KernelSU (%1$d) terlalu rendah. Perbarui ke versi %2$d atau lebih tinggi agar manajer berjalan normal! Manajemen Jalur Umount Kelola jalur unmount kernel Tidak ada jalur umount yang ditemukan diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index 8b9c885a8..89f0794c3 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -132,10 +132,10 @@ Android バージョン デバイスモデル 「%s」にスーパーユーザー権限を付与することはできません - 古典 su 命令 - /system/bin/su 経由での 根源権限(特権)を 許可します(新規 手順/工程 のみ) - module の umount - App Profile に基づき、kernel 側で module を umount します (あぷり ぷろふぁいる に もとづき、かーねる がわで もじゅーる を あんまうんと します) + 従来の su コマンド + 新規プロセスにおいて、/system/bin/su を経由して root アクセスを許可します。 + モジュールのアンマウント(カーネルレベル) + アプリ プロファイルに基づいて、カーネルからモジュールをアンマウントします カーネルはこの機能に対応していません この機能はモジュールによって管理されています デフォルト @@ -200,12 +200,6 @@ カーネルをフラッシュ中 メタモジュールが必要です このモジュールは /system をマウントしようとしますが、メタモジュールがそれを処理します。そうでなければ動作しない可能性があります - 名前の昇順 - 名前の降順 - インストール日時 (新しい) - インストール日時 (古い) - サイズの降順 - サイズの昇順 使用頻度 このカテゴリーにアプリはありません SuSFS の構成 @@ -266,4 +260,18 @@ 構成を削除 操作が失敗しました %d 個のアプリが含まれています + Seccomp のステータス + Not supported + Disabled + Strict + Filter + Unknown + SU ログ + ログファイル + SU ログの読み込みに失敗しました + SU ログは有効になっていません + SU ログはサポートされていません + 有効化 + 種類で絞り込む + ログを消去 diff --git a/manager/app/src/main/res/values-ko/strings.xml b/manager/app/src/main/res/values-ko/strings.xml index 0b905a04c..18c7f8f13 100644 --- a/manager/app/src/main/res/values-ko/strings.xml +++ b/manager/app/src/main/res/values-ko/strings.xml @@ -210,8 +210,6 @@ 커널 플래싱 메타 모듈 필요 이 모듈은 /system 마운트를 시도하며, 메타 모듈이 이를 처리합니다. 메타 모듈이 없으면 작동하지 않을 수 있습니다. - 이름 오름차순 - 이름 내림차순 이 카테고리에 앱이 없습니다. 영구적 일시적 diff --git a/manager/app/src/main/res/values-pl/strings.xml b/manager/app/src/main/res/values-pl/strings.xml index 6e0db262e..bda325f14 100644 --- a/manager/app/src/main/res/values-pl/strings.xml +++ b/manager/app/src/main/res/values-pl/strings.xml @@ -303,12 +303,6 @@ %d nie udało się zainstalować nowego modułu Wymagany meta moduł Ten moduł montuje /system i wymaga meta modułu, aby działać poprawnie - Alfabetycznie (A-Z) - Alfabetycznie (Z-A) - Czas instalacji (Nowe) - Czas instalacji (Stare) - Rozmiar - malejąco - Rozmiar - rosnąco Częstotliwość używania Brak aplikacji w tej kategorii Trwały @@ -367,8 +361,6 @@ Ukrywa prawdziwe ścieżki plików map pamięci przed /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Ta funkcja nie wspiera ukrywania anonimowy map pamięci, ani nie może ukryć inline hooków lub PLT haków wywołanych przez wstrzykniętą bibliotekę Dla aplikacji z dobrą implementacją mechanizmu wykrywania wstrzyknięć ta funkcja może nie być skuteczna Najpierw znajdź PID oraz UID wybranej aplikacji używając ps -enf, a następnie sprawdź znaczące ścieżki w /proc/<pod>/maps i porównaj liczby urządzenia z tymi, które znajdują się w /proc/1/mountinfo. Funkcja może działać poprawnie tylko, gdy te liczby się zgadzają - Aktualna wersja menadżera KernelSU %1$d jest zbyt niska aby KernelSU działało poprawnie. Zaktualizuj menadżer do wersji %2$d lub wyższej! - Aktualna wersja KernelSU %1$d jest zbyt niska aby menadżer działał poprawnie. Zaktualizuj KernelSU do wersji %2$d lub wyższej! Zarządzanie ścieżkami odmontowywania Zarządzaj ścieżkami odmontowywania przez jądra Brak ścieżek diff --git a/manager/app/src/main/res/values-pt-rBR/strings.xml b/manager/app/src/main/res/values-pt-rBR/strings.xml index a773477c7..05f0a399b 100644 --- a/manager/app/src/main/res/values-pt-rBR/strings.xml +++ b/manager/app/src/main/res/values-pt-rBR/strings.xml @@ -222,8 +222,6 @@ Versão do Android Modelo do dispositivo Não é permitido conceder privilégios de superusuário a %s - Desmontar para WebView - Impede o vazamento de informações do processo WebView, mas pode causar problemas em alguns módulos. Reinicie o sistema para aplicar as alterações O kernel não suporta essa funcionalidade Desativar até a reinicialização Desativar sempre @@ -312,11 +310,6 @@ Instalando o kernel Módulo Meta necessário Este módulo deseja montar /system, o que é gerenciado pelo metamódulo. Sem ele, o módulo pode não funcionar - Ordem crescente de nomes - Nome em ordem decrescente - Tempo de instalação (Novo) - Tempo de instalação (Antigo) - Ordem decrescente de tamanho Frequência de utilização Não há candidaturas nesta categoria Persistente @@ -375,8 +368,6 @@ Oculta os caminhos reais dos mapeamentos de memória em /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Observação: este recurso não suporta a ocultação de mapeamentos de memória anônimos, nem pode ocultar hooks embutidos ou hooks PLT causados pela própria biblioteca injetada Aviso importante: Para aplicações com mecanismos de detecção de injeção bem implementados, este recurso pode não ser eficaz para contornar a detecção Primeiro, encontre o PID e o UID do aplicativo de destino usando o comando `ps -enf`. Em seguida, verifique os caminhos relevantes em `/proc/<pid>/maps` e compare os números dos dispositivos com os de `/proc/1/mountinfo` para garantir a consistência. A função de ocultação do mapa só funcionará corretamente se os números dos dispositivos coincidirem - A versão atual do gerenciador KernelSU, %1$d, é muito antiga para o funcionamento correto do KernelSU. Atualize o gerenciador para a versão %2$d ou superior! - A versão atual do KernelSU, %1$d, é muito antiga para o gerenciador funcionar corretamente. Atualize para a versão %2$d ou superior! Gestão de Caminhos de Umount Gerenciar caminhos de desmontagem do kernel Não existem caminhos de desmontagem @@ -538,5 +529,4 @@ Versão: %1$s (%2$d)\nArquitetura: %3$s Confira as atualizações beta Verificação automática de versões beta a partir da ramificação principal - Ordem decrescente de tamanho diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index 03c52d104..c5bbf3561 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -4,7 +4,7 @@ Узнать больше Не установлен Нажмите, чтобы установить - Работает + Активен Не поддерживается Драйвера KernelSU не найдены в ядре, неверное ядро? Версия ядра @@ -12,8 +12,8 @@ Версия менеджера Состояние SELinux Отключен - Блокирующий - Предупреждающий + Принудительный + Разрешающий Статус Seccomp Не поддерживается Отключено @@ -39,7 +39,7 @@ Неизвестное событие Поиск лога Не удалось включить модуль %s - Не удалось отключить модуль %s + Не удалось отключить модуль: %s Нет установленных модулей Модули Репозиторий модулей @@ -47,20 +47,20 @@ Сортировка (по звездам) Сортировать (Сначала включённые) Удалить - Установка + Установить Перезагрузить Настройки Мягкая перезагрузка Мягкая перезагрузка Перезагрузить в Recovery - Перезагрузить в Bootloader + Перезагрузить в загрузчик Перезагрузить в Download Перезагрузить в EDL О приложении - Проверить исходный код - Проверить исходный код на GitHub + Изучить исходный код + Изучить исходный код на GitHub Присоединиться к сообществу - Присоединиться к нашему Telegram-каналу + Присоединиться к нашему Telegram-сообществу Open Source лицензия Просмотреть сторонние open-source библиотеки и их лицензии Посетить домашнюю страницу @@ -69,7 +69,7 @@ Вы уверены, что хотите удалить модуль %s? Вы уверены, что хотите удалить модуль %s? Это действие повлияет на все модули, и некоторые функции мета модуля (например, монтирование) больше не будут работать %s удалён - Не удалось удалить %s + Не удалось удалить: %s Версия Автор Показать системные приложения @@ -81,7 +81,7 @@ Вы находитесь в **режиме Jailbreak**. Прошивка раздела на устройстве с **заблокированным загрузчиком** нарушит защиту AVB (Android Verified Boot) и может привести к тому, что устройство **не загрузится**.\n\nПеред тем как продолжить, убедитесь, что ваш загрузчик разблокирован! Продолжить (%1$d) Автоматический Jailbreak - Автоматически повышать права через Magica, если при загрузке обнаружен предупреждающий режим SELinux. Требуется разрешение на автозапуск приложения. + Автоматически повышать права через Magica, если при загрузке обнаружен разрешающий режим SELinux. Требуется разрешение на автозапуск приложения. Запустить сервис adbd с Root правами Скрыть модификации SELinux Предотвратить обнаружение приложениями изменений SELinux @@ -128,7 +128,7 @@ Управление локальным и онлайн-шаблоном профиля приложения Создать шаблон Редактирование шаблона - Идентификационный номер + ID Неверный ID шаблона Название Описание @@ -147,13 +147,13 @@ Затрагиваемые приложения Не удалось получить список изменений: %s Не удалось выдать root! - Это debug-сборка из Pull Request. НЕ используйте в продакшене! + Это debug-сборка из PR. Не рекомендуется для публичного релиза Действие Закрыть Прямая установка (Рекомендуется) Выбрать файл Установка в неактивный слот (После OTA) - Ваше устройство будет **ПРИНУДИТЕЛЬНО** загружено в текущий неактивный слот после перезагрузки! \n Используйте эту опцию только после завершения OTA. \n Продолжить? + Ваше устройство будет **ПРИНУДИТЕЛЬНО** загружено в текущий неактивный слот после перезагрузки!\nИспользуйте эту опцию только после завершения OTA.\nПродолжить? Далее Выбрать раздел Использовать локальный файл LKM @@ -167,23 +167,23 @@ Временно удалить KernelSU, восстановить исходное состояние после следующей перезагрузки. Удалить KernelSU (рут и все модули) полностью. Восстановить исходный заводской образ (если существует резервная копия), обычно используется перед OTA; если вам нужно удалить KernelSU, используйте «Удалить полностью». - Установка + Прошивка Установка выполнена Установка не выполнена Выбран LKM: %s Сохранить логи Логи сохранены - неизвестный модуль + Неизвестный модуль Подтвердить Отмена Резервная копия создана успешно Ошибка резервного копирования списка: %1$s - Подтвердите восстановление списка + Подтверждение восстановления белого списка Эта операция перезапишет текущий список разрешений. Продолжить? Список успешно восстановлен Не удалось восстановить список: %1$s Резервное копирование списка - Восстановить список + Белый список восстановления ключей Пользовательский фон приложения Выберите изображение в качестве фона Включить блюр @@ -219,7 +219,7 @@ Использовать акцентные цвета системы Выберите цвет темы Установка AnyKernel3 - Прошить файл ядра AnyKernel3 + Прошить архив AnyKernel3 Требуется root-права Не удалось перезагрузиться Персонализация @@ -284,12 +284,6 @@ Прошить ядро Требуется мета-модуль Этот модуль пытается смонтировать /system, что обрабатывается мета-модулем. Без него модуль может не работать - Название (по возрастанию) - Название (по убыванию) - Время установки (новые) - Время установки (старые) - Размер (по убыванию) - Размер (по возрастанию) Частота использования В этой категории нет приложений Постоянный @@ -331,7 +325,7 @@ Реализация мета-модуля Конфигурация путей монтирования loop Пути цикла повторно отмечены как SUS_PATH в каждом пользовательском приложении, не являющемся root, или изолированном запуске службы. Это помогает решить проблемы, в которых добавленные пути могут иметь сброс статуса inode или повторно созданные inode в ядре. - Типы хуков + Тип хука Подтвердите установку Подтвердите установку (%d файлов) Установить @@ -427,10 +421,8 @@ Настройки темы Стиль палитры Спецификация цвета - AOSP стиль - Текущая версия %1$d KernelSU слишком низкая для корректной работы. Пожалуйста обновите менеджер до %2$d и выше - Текущая версия %1$d KernelSU слишком низкая для корректной работы. Обновите пожалуйста менеджер до версии %2$d и выше - Ридми + AOSP + Прочти меня Статус Стандарт SUS путь @@ -530,13 +522,25 @@ Бета обновление Версия: %1$s (%2$d)\nАрхитектура: %3$s Проверка бета обновлений - Автоматически проверять бета-сборки из главной ветки + Автоматически проверять бета-сборки из основной ветки Использовать встроенный моноширинный шрифт Использовать шрифт JetBrains Mono для отображения логов во избежание проблем с системным моноширинным шрифтом менеджер SUSFS Включите встроенный менеджер SUSFS. Может конфликтовать со сторонними модулями. Встроенный менеджер SUSFS отключен. Включите его или установите сторонний модуль для использования функций SUSFS. - Отключать монтирование для WebView - Предотвращает утечку информации из процесса WebView, но может нарушить работу модулей. Перезагрузите для применения - + + Требуется обновление менеджера + Требуется обновление ядра. Нажмите, чтобы установить. + Требуется обновление ядра + Добавить иконки на информационные карточки главного экрана + Показывать иконки на карточках главного экрана + Показывать количество суперпользователь/модуль в панели навигации + Значки панели навигации + Имя + Время установки + Время обновления + Размер + Обратный порядок + Плавающая нижняя панель + Использовать Apple-стиль для плавающей нижней панели diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index 0ecb78aea..db400ef56 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -261,12 +261,6 @@ Çekirdek Yükleniyor Meta modül gerektirir Bu modül, meta modül tarafından yönetilen /system bölümünü bağlamak istiyor. Bu modül meta modül olmadan çalışmayabilir - İsme göre artan sırada - İsme göre azalan sırada - Kurulum zamanı (yeni) - Kurulum zamanı (eski) - Boyuta göre azalan sırada - Boyuta göre artan sırada Kullanım sıklığına göre Bu kategoride uygulama yok Kalıcı @@ -400,8 +394,6 @@ KernelSU Klasik Hareketi Takip Et Çekirdek - KernelSU yöneticisinin mevcut sürümü %1$d, bu KernelSU\'nun düzgün çalışması için çok düşük. Lütfen yöneticiyi %2$d veya daha yüksek bir sürüme yükseltin! - Mevcut KernelSU sürümü %1$d yöneticinin düzgün çalışması için çok düşük. Lütfen %2$d veya daha yüksek bir sürüme yükseltin! Modül İndirmeleri İndiriliyor %s İndirildi @@ -490,7 +482,7 @@ Dinamik yöneticiyi temizle Dinamik yönetici yapılandırmasını silmek istediğinize emin misiniz? Yöneticileri yönet - Hakkında + Sürüm Bilgisi Durum Bilgisi SüperKullanıcı: %1$d, Modüller: %2$d Çekirdek sürücüsü sürümü @@ -532,11 +524,12 @@ Veri Yok Yönlendirmeyi Aç - WebView bağlamasını kaldır - WebView işleminden bilgi sızıntılarını önler ancak bazı modüllerin çalışmasını bozabilir. Uygulamak için yeniden başlatın Yerleşik eş aralıklı yazı tipini kullan Günlükleri görüntülerken sistemin eş aralıklı yazı tipiyle ilgili sorunları önlemek için yerleşik JetBrains Mono yazı tipini kullanır SUSFS Yöneticisi Yerleşik SUSFS yöneticisini etkinleştirir. Üçüncü taraf modüllerle çakışabilir. Yerleşik SUSFS yöneticisi devre dışı. SUSFS özelliklerini kullanmak için bunu etkinleştirin veya üçüncü taraf bir modül yükleyin. + Yönetici güncellemesi gerekiyor + Çekirdek güncellemesi gerekiyor. Kurmak için dokunun. + Çekirdek güncellemesi gerekiyor diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 9ad647f06..49f7e91ee 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -303,12 +303,6 @@ Прошивка ядра Потрібен метамодуль Цей модуль хоче монтувати /system; цим займатиметься метамодуль. Без нього модуль може не працювати - Назва за зростанням - Назва за спаданням - Час встановлення (нові) - Час встановлення (старі) - Розмір за спаданням - Розмір за зростанням Частота використання Немає додатків у цій категорії Постійний @@ -367,8 +361,6 @@ Приховати реальні шляхи до файлів відображень пам\'яті у /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Зверніть увагу: ця функція не підтримує приховання анонімних відображень пам\'яті та не може приховати inline-хуки або PLT-хуки, спричинені самою впровадженою бібліотекою Важливе попередження: для додатків з добре реалізованими механізмами виявлення впровадження ця функція може не ефективно обійти виявлення Спочатку знайдіть PID та UID цільового додатка за допомогою ps -enf, потім перевірте відповідні шляхи в /proc/<pid>/maps та порівняйте номери пристроїв із /proc/1/mountinfo для забезпечення відповідності. Тільки при збігу номерів пристроїв функція приховання map працюватиме правильно - Поточна версія менеджера KernelSU %1$d є занадто низькою для коректної роботи KernelSU. Будь ласка, оновіть менеджер до версії %2$d або вище! - Поточна версія KernelSU %1$d є занадто низькою для коректної роботи менеджера. Будь ласка, оновіть до версії %2$d або вище! Керування шляхами розмонтування Керувати шляхами розмонтування ядра Шляхи розмонтування відсутні @@ -515,8 +507,8 @@ Очистити динамічний менеджер Ви впевнені, що хочете очистити налаштування динамічного менеджера? Керування менеджерами - Про - Інформація про стан + Інформація + Стан Суперкористувач: %1$d, Модулі: %2$d Версія драйвера ядра Жодного збігу не знайдено @@ -536,7 +528,19 @@ Увімкнути вбудований менеджер SUSFS. Може конфліктувати зі сторонніми модулями. Менеджер SUSFS Використовувати вбудований моноширинний шрифт - Запобігає витоку інформації з процесу WebView, але це може порушити роботу модулів. Перезавантажте, щоб застосувати зміни. - Розмонтувати WebView + Потрібне оновлення ядра + Потрібне оновлення менеджера + Потрібне оновлення ядра. Натисніть, щоб встановити. + Додати іконки до інформаційних карток на головному екрані + Показувати іконки на картках головного екрана + Показувати кількість прав суперкористувача / модулів на панелі навігації + Позначки на панелі навігації + Час встановлення + Час оновлення + Розмір + Зворотний порядок + Ім\'я + Плаваюча нижня панель + Використовувати плаваючу нижню панель у стилі Apple. diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index 2082e5090..255c0553f 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -204,12 +204,6 @@ Cài đặt module %d thất bại Yêu cầu Meta-module Module này muốn mount /system, Meta-module sẽ xử lý việc đó. Nếu không, nó có thể không hoạt động - Tên (Tăng dần) - Tên (Giảm dần) - Thời gian cài đặt (Mới) - Thời gian cài đặt (Cũ) - Kích thước (Giảm dần) - Kích thước (Tăng dần) Tần suất sử dụng Không có ứng dụng nào trong danh mục này Cấu hình SuSFS @@ -362,8 +356,6 @@ Tìm module Module Kernel - Phiên bản %1$d của trình quản lý KernelSU đã quá lỗi thời để KernelSU hoạt động đúng cách. Hãy nâng cấp trình quản lý lên phiên bản %2$d hoặc cao hơn! - Phiên bản %1$d của KernelSU đã quá cũ để trình quản lý có thể hoạt động bình thường. Hãy cập nhật lên phiên bản %2$d hoặc cao hơn! Không có đường dẫn umount nào tồn tại 0=Unmount bình thường, 2=MNT_DETACH Tất cả các thay đổi sẽ có hiệu lực ngay lập tức. @@ -402,4 +394,8 @@ Cài đặt Cần có Quyền truy cập vào thông báo để có thể hiện tiến trình tải. Tải thất bại: Cần có Quyền truy cập thông báo + Cờ + Chọn 1 phương thức crop + Cắt ảnh thất bại + 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 9ad38e9d1..e83aefbb9 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -208,6 +208,8 @@ 旋转角度 启用模糊 对此 App 启用模糊处理 + 悬浮底栏 + 使用 Apple 风格的悬浮底栏 将自定义背景渲染到模糊 实验性功能,后果自负 从自定义背景图片中取色 @@ -222,8 +224,6 @@ 允许通过 /system/bin/su 获取 Root 权限。 内核处理卸载模块 在内核给需要的应用卸载模块 - 为 WebView 卸载模块 - 防止 WebView 进程泄露信息,但可能会导致模块失效。重启后生效 内核不支持此功能 禁用直到下次重启 始终禁用 @@ -236,6 +236,10 @@ 使用内置等宽字体显示日志,解决某些 ROM 上字体对齐异常的问题 简洁模式 开启后将隐藏不必要的卡片 + 导航栏角标 + 在导航栏展示超级用户 / 模块数量 + 主页卡片显示图标 + 主页信息卡片添加图标展示 主题模式 跟随系统 浅色 @@ -312,13 +316,12 @@ 内核刷写 需要元模块 这个模块需要挂载一些文件。需要安装元模块,使它正常工作 - 名称升序 - 名称降序 - 安装时间(新) - 安装时间(旧) - 大小降序 - 大小升序 + 名称 + 安装时间 + 更新时间 + 大小 使用频率 + 倒序 此分类中没有应用 持久 临时 @@ -376,8 +379,9 @@ 从 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隐藏内存映射的真实文件路径。请注意:此功能不支持隐藏匿名内存映射,也无法隐藏由注入库本身产生的内联钩子或 PLT 钩子 重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测 首先通过 ps -enf 查找目标应用的 PID 和 UID,然后检查 /proc/<pid>/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作 - 当前 KernelSU 管理器版本 %1$d 过低,KernelSU 无法正常工作. 请将 KernelSU 管理器版本升级至 %2$d 或以上! - 当前 KernelSU 版本 %1$d 过低,管理器无法正常工作,请将内核 KernelSU 版本升级至 %2$d 或以上! + 需要更新管理器 + 需要更新内核,点击此处安装。 + 需要更新内核 Umount 路径管理 管理内核卸载路径 没有任何内核卸载路径 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 a8c780703..382d1d98e 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -266,12 +266,6 @@ 核心刷寫 需要元模組 此模組想掛載 /system,此操作由 meta 模組處理。若缺少它,模組可能無法正常運作 - 名稱升序 - 名稱降序 - 安裝時間(新) - 安裝時間(舊) - 大小降序 - 大小升序 使用頻率 此分類中冇應用程式 持久 @@ -398,6 +392,8 @@ 旋轉角度 啟用模糊 為此應用程式啟用模糊處理 + 懸浮底部欄 + 使用 Apple 風格懸浮底部欄 繪製自訂背景以實現模糊效果 實驗性功能,使用風險自負 從自訂背景選取顏色 @@ -417,8 +413,6 @@ 跟隨手勢 固定靠右 固定靠左 - 當前 KernelSU 管理器版本 %1$d 過低,無法正常運作,請升級至 %2$d 或更高版本! - 當前 KernelSU 版本 %1$d 過低,管理器無法正常運作,請升級至 %2$d 或更高版本! 模組下載 正在下載 %s 下載完成 @@ -534,6 +528,4 @@ SUSFS 管理器 啟用內置 SUSFS 管理器,可能與第三方模組衝突。 內置 SUSFS 管理器已停用。請啟用它或安裝第三方模組以使用 SUSFS 功能。 - 為 WebView 解除掛載 - 防止 WebView 程序洩露資訊,但可能會令部分模組失效。重新啟動以套用變更 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 e64612863..1c6317f4c 100644 --- a/manager/app/src/main/res/values-zh-rTW/strings.xml +++ b/manager/app/src/main/res/values-zh-rTW/strings.xml @@ -197,6 +197,8 @@ 選擇一張圖片作為應用程式背景 啟用模糊 對這個 App 啟用模糊處理 + 浮動底欄 + 使用 Apple 風格的浮動底欄 模糊渲染自定義背景 實驗性功能,請自行承擔風險 從自定義背景影像中取色 @@ -297,12 +299,6 @@ 正在刷寫內核 需要元模組 這個模組需要元模組掛載 /system。否則,它將無法運作 - 名稱遞增 - 名稱遞減 - 安裝時間(新) - 安裝時間(舊) - 大小遞減 - 大小遞增 使用頻率 無此分類中的應用程式 永久 @@ -361,8 +357,6 @@ 從 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隱藏記憶體映射的真實檔案路徑。請注意:此功能不支援隱藏「匿名記憶體映射」,也無法隱藏由程式庫本身注入產生的內聯掛鉤或 PLT 掛鉤 重要提示:對於具備完整注入偵測機制的應用程式,此功能可能無法有效繞過偵測 首先透過 ps -enf 搜尋目標應用程式的 PID 和 UID,然後檢查 /proc/<pid>/maps 中的相關路徑,並與 /proc/1/mountinfo 中的裝置號碼進行比對確保一致性。只有當裝置號碼一致時,隱藏映射才能正常運作 - 目前 KernelSU 管理器版本 %1$d 過低,KernelSU 無法正常運作。請將 KernelSU 管理器版本升級至 %2$d 或以上! - 目前 KernelSU 版本 %1$d 過低,管理器無法正常運作。請將內核 KernelSU 版本升級至 %2$d 或以上! 卸載路徑管理 管理內核卸載路徑 沒有既有的內核卸載路徑 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index ba222504e..2ba899e34 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -225,8 +225,6 @@ Allow root access via /system/bin/su, in new processes. Module unmounting Unmount modules from kernel in App Profile - Unmount for WebView - Prevent information leaks from WebView process but may break modules. Reboot to apply Kernel does not support this feature Disable until Reboot Always disable @@ -239,6 +237,10 @@ Use built-in JetBrains Mono font for log display to avoid system monospace issues Simplicity mode Hides unnecessary cards when turned on + Navigation bar badges + Show superuser / module counts in the navigation bar + Show icons on home cards + Add icons to the home information cards Theme Follow system Light @@ -316,13 +318,12 @@ Kernel Flashing Require Meta module This module wants to mount /system, which is handled by the meta module. Without it, the module might not work - Ascending order of name - Name descending - Installation time (New) - Installation time (Old) - Descending order of size - Ascending order of size + Name + Install time + Update time + Size Frequency of use + Reverse order No application in this category Persistent Temporary @@ -380,8 +381,9 @@ 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 - The current KernelSU manager version %1$d is too low for KernelSU to work properly. Please upgrade manager to version %2$d or higher! - The current KernelSU version %1$d is too low for the manager to work properly. Please upgrade to version %2$d or higher! + Manager update required + Kernel update required. Tap to install. + Kernel update required Umount Path Management Manage kernel unmount paths No existing umount paths @@ -543,4 +545,6 @@ Version: %1$s (%2$d)\nArchitecture: %3$s Check beta updates Auto-check beta builds from the main branch + Floating bottom bar + Use Apple style floating bottom bar. diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index 4af8c16a3..50c8d3282 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -1,37 +1,35 @@ [versions] accompanist-drawablepainter = "0.37.3" -agp = "9.3.0" +agp = "9.4.0" gson = "2.14.0" -kotlin = "2.4.0" -materialKolor = "4.1.1" +kotlin = "2.4.20" +materialKolor = "5.0.1" monetCompat = "0.4.1" materialComponents = "1.14.0" capsule = "2.1.3" -compose-bom = "2026.05.01" +compose-bom = "2026.09.00" lifecycle = "2.10.0" -navigation3 = "1.2.0-alpha04" -navigationevent = "1.1.1" activity-compose = "1.13.0" core-splashscreen = "1.2.0" kotlinx-coroutines = "1.11.0" coil-compose = "2.7.0" ucrop = "2.2.11" markdown = "4.6.2" -webkit = "1.16.0" +webkit = "1.17.0" appiconloader = "1.5.0" hiddenapibypass = "6.1" parcelablelist = "2.0.1" libsu = "6.0.0" apksign = "1.4" -compose-material3 = "1.5.0-alpha21" +compose-material3 = "1.5.0-alpha28" compose-ui = "1.11.2" documentfile = "1.1.0" ndk = "29.0.14206865" foundation = "1.11.2" -aboutLibraries = "14.2.1" -miuix = "0.9.2" +aboutLibraries = "15.2.0" +miuix = "0.9.4-rc01" datastore = "1.2.1" -benchmark = "1.5.0-alpha07" +benchmark = "1.5.0" profileinstaller = "1.4.1" koin-bom = "4.2.2" @@ -74,7 +72,6 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } -androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycle" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } @@ -95,10 +92,8 @@ me-zhanghai-android-appiconloader = { group = "me.zhanghai.android.appiconloader me-zhanghai-android-appiconloader-coil = { group = "me.zhanghai.android.appiconloader", name = "appiconloader-coil", version.ref = "appiconloader" } org-lsposed-hiddenapibypass = { group = "org.lsposed.hiddenapibypass", name = "hiddenapibypass", version.ref = "hiddenapibypass" } -androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "navigation3" } -miuix-navigation = { group = "top.yukonga.miuix.kmp", name = "miuix-navigation3-ui-android", version.ref = "miuix" } +miuix-nav = { group = "top.yukonga.miuix.kmp", name = "miuix-nav", version.ref = "miuix" } miuix-blur = { group = "top.yukonga.miuix.kmp", name = "miuix-blur-android", version.ref = "miuix" } -androidx-navigationevent = { module = "androidx.navigationevent:navigationevent", version.ref = "navigationevent" } markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } @@ -113,4 +108,4 @@ androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profi # Core module (required for accessing library data) aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutLibraries" } # Compose UI modules (choose one or both) -aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" } # Material 3 UI +aboutlibraries-compose-m3 = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "aboutLibraries" } diff --git a/manager/gradle/wrapper/gradle-wrapper.jar b/manager/gradle/wrapper/gradle-wrapper.jar index b1b8ef56b..eddabd2ee 100644 Binary files a/manager/gradle/wrapper/gradle-wrapper.jar and b/manager/gradle/wrapper/gradle-wrapper.jar differ diff --git a/manager/gradle/wrapper/gradle-wrapper.properties b/manager/gradle/wrapper/gradle-wrapper.properties index 52ad5e715..c56b367ba 100644 --- a/manager/gradle/wrapper/gradle-wrapper.properties +++ b/manager/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/manager/gradlew b/manager/gradlew index b9bb139f7..249efbb03 100755 --- a/manager/gradlew +++ b/manager/gradlew @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: diff --git a/manager/gradlew.bat b/manager/gradlew.bat index 24c62d56f..a51ec4f58 100644 --- a/manager/gradlew.bat +++ b/manager/gradlew.bat @@ -19,7 +19,7 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## @@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2 -@rem Execute Gradle +@rem Execute gradlew @rem endlocal doesn't take effect until after the line is parsed and variables are expanded @rem which allows us to clear the local environment before executing the java command endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel diff --git a/scripts/ksubot.py b/scripts/ksubot.py index b2d591667..163887446 100644 --- a/scripts/ksubot.py +++ b/scripts/ksubot.py @@ -99,7 +99,7 @@ def get_caption(): commit_line=commit_line, run_url=RUN_URL, ) - if BRANCH != "main": + if BRANCH != "main" and GITHUB_REF_TYPE != "tag": msg += "\n⚠️⚠️DEV VERSION, PLEASE BACKUP BEFORE INSTALLATION⚠️⚠️" msg += "\n⚠️⚠️测试版,安装前请备份⚠️⚠️" return msg @@ -113,7 +113,7 @@ def get_caption_for_debug(): commit_line=commit_line, run_url=RUN_URL, ) - if BRANCH != "main": + if BRANCH != "main" and GITHUB_REF_TYPE != "tag": msg += "\n⚠️⚠️DEV VERSION, PLEASE BACKUP BEFORE INSTALLATION⚠️⚠️" msg += "\n⚠️⚠️测试版,安装前请备份⚠️⚠️" return msg diff --git a/uapi/feature.h b/uapi/feature.h index 1049457c2..df2f68c15 100644 --- a/uapi/feature.h +++ b/uapi/feature.h @@ -7,7 +7,6 @@ enum ksu_feature_id { KSU_FEATURE_SULOG = 2, KSU_FEATURE_ADB_ROOT = 3, KSU_FEATURE_SELINUX_HIDE = 4, - KSU_FEATURE_WEBVIEW_ZYGOTE_UMOUNT = 5, KSU_FEATURE_MAX }; diff --git a/uapi/supercall.h b/uapi/supercall.h index 4eb2dfad5..25ab7976a 100644 --- a/uapi/supercall.h +++ b/uapi/supercall.h @@ -15,7 +15,9 @@ #define KSU_FULL_VERSION_STRING 255 // 2: allowlist v4 root profile flags -static const __u32 KERNEL_SU_UAPI_VERSION = 2; +// 3: scoped su-session driver fd +// 4: add KSU_GET_INFO_FLAG_BUNDLED +static const __u32 KERNEL_SU_UAPI_VERSION = 4; /* Magic numbers for reboot hook to install fd */ DEFINE_KSU_UAPI_CONST(__u32, KSU_INSTALL_MAGIC1, 0xDEADBEEF) @@ -33,6 +35,7 @@ DEFINE_KSU_UAPI_CONST(__u32, KSU_GET_INFO_FLAG_LKM, (1U << 0)) DEFINE_KSU_UAPI_CONST(__u32, KSU_GET_INFO_FLAG_MANAGER, (1U << 1)) DEFINE_KSU_UAPI_CONST(__u32, KSU_GET_INFO_FLAG_LATE_LOAD, (1U << 2)) DEFINE_KSU_UAPI_CONST(__u32, KSU_GET_INFO_FLAG_PR_BUILD, (1U << 3)) +DEFINE_KSU_UAPI_CONST(__u32, KSU_GET_INFO_FLAG_BUNDLED, (1U << 4)) struct ksu_get_info_cmd { __u32 version; /* Output: KERNEL_SU_VERSION */ diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock index 55141b10e..d7b70ed73 100644 --- a/userspace/ksud/Cargo.lock +++ b/userspace/ksud/Cargo.lock @@ -5,7 +5,7 @@ version = 4 [[package]] name = "adb_client" version = "3.1.1" -source = "git+https://github.com/Kernel-SU/adb_client#d97a966435bebaa55017834869dec08150826aa7" +source = "git+https://github.com/ReSukiSU/adb_client#d97a966435bebaa55017834869dec08150826aa7" dependencies = [ "byteorder", "log", @@ -44,13 +44,13 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android-bootimg" version = "0.1.0" -source = "git+https://github.com/5ec1cff/android_bootimg?rev=150425b027c76ea104c82e408571651f2181b2c2#150425b027c76ea104c82e408571651f2181b2c2" +source = "git+https://github.com/ReSukiSU/android_bootimg?rev=150425b027c76ea104c82e408571651f2181b2c2#150425b027c76ea104c82e408571651f2181b2c2" dependencies = [ "anyhow", "bytemuck", "bzip2", "flate2", - "itertools 0.14.0", + "itertools", "lz4", "lzma-rust2 0.15.8", "num-traits", @@ -148,7 +148,7 @@ checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -165,22 +165,21 @@ checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "bindgen" -version = "0.72.1" +version = "0.73.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +checksum = "787ef8ef523575546b106a58213d6e6b06198a05c2f757258c68a74273670cfa" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "cexpr", "clang-sys", - "itertools 0.13.0", "log", "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", - "shlex 1.3.0", - "syn 2.0.119", + "shlex", + "syn 3.0.5", ] [[package]] @@ -191,9 +190,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "block-buffer" @@ -248,14 +247,14 @@ dependencies = [ [[package]] name = "cc" -version = "1.4.4" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +checksum = "a3eb0f42d6c360dc3f8a821f6bf2fdea7f72bfd36b3076eb0e6d1e9e0752fff4" dependencies = [ "find-msvc-tools", "jobserver", "libc", - "shlex 2.0.1", + "shlex", ] [[package]] @@ -310,9 +309,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" dependencies = [ "clap_builder", "clap_derive", @@ -320,9 +319,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" dependencies = [ "anstream", "anstyle", @@ -332,21 +331,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.4" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] name = "clap_lex" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" [[package]] name = "colorchoice" @@ -387,6 +386,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -407,18 +412,18 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" dependencies = [ "cfg-if", ] [[package]] name = "crossbeam" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +checksum = "e71406cd8807725f7ac2f999a4cdd32e98f829fdf65f528343cebf945e41df1e" dependencies = [ "crossbeam-channel", "crossbeam-deque", @@ -429,18 +434,18 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.16" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5181e0de7b61eb03a81e347d6dd8797bae9da5146707b51077e2d71a54ec0ceb" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" dependencies = [ "crossbeam-epoch", "crossbeam-utils", @@ -448,27 +453,27 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.20" +version = "0.9.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-queue" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" [[package]] name = "crypto-common" @@ -547,11 +552,17 @@ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" [[package]] name = "encoding_rs" -version = "0.8.35" +version = "0.8.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +checksum = "7b5ef0006ac9ab233c38522f5ae99cae3625151de8f706cacee1cba4b8e2832a" dependencies = [ "cfg-if", + "core_detect", + "multiversion", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", ] [[package]] @@ -648,15 +659,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.11" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" [[package]] name = "flate2" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" dependencies = [ "crc32fast", "miniz_oxide", @@ -675,17 +686,6 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" -[[package]] -name = "futures-macro" -version = "0.3.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.4", -] - [[package]] name = "futures-task" version = "0.3.34" @@ -699,7 +699,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", - "futures-macro", "futures-task", "pin-project-lite", "slab", @@ -793,9 +792,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" dependencies = [ "typenum", ] @@ -859,36 +858,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" dependencies = [ "equivalent", "hashbrown 0.17.1", ] -[[package]] -name = "inotify" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592" -dependencies = [ - "bitflags 2.13.1", - "futures-util", - "inotify-sys", - "libc", - "tokio", -] - -[[package]] -name = "inotify-sys" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d" -dependencies = [ - "libc", -] - [[package]] name = "is_executable" version = "1.0.6" @@ -904,15 +881,6 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -931,7 +899,7 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "java-properties" version = "2.0.0" -source = "git+https://github.com/Kernel-SU/java-properties.git?branch=master#42a4aa941b70ded2dd3be9e9f892471023e70229" +source = "git+https://github.com/ReSukiSU/java-properties.git?branch=master#42a4aa941b70ded2dd3be9e9f892471023e70229" dependencies = [ "encoding_rs", "lazy_static", @@ -950,13 +918,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.99" +version = "0.3.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] @@ -973,7 +940,7 @@ dependencies = [ [[package]] name = "kernlog" version = "0.3.1" -source = "git+https://github.com/kstep/kernlog.rs#68caa7bf1e27baea35b00ebba786cafae0bca90f" +source = "git+https://github.com/ReSukiSU/kernlog.rs#68caa7bf1e27baea35b00ebba786cafae0bca90f" dependencies = [ "libc", "log", @@ -1004,7 +971,7 @@ dependencies = [ "anyhow", "base16ct", "bindgen", - "bitflags 2.13.1", + "bitflags 2.13.2", "cc", "chrono", "clap", @@ -1016,7 +983,6 @@ dependencies = [ "figlet-rs", "getopts", "humansize", - "inotify", "is_executable", "java-properties", "jwalk", @@ -1073,9 +1039,9 @@ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libflate" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4da9b700e758e57152a1fd1c52cbdc5727c1aa6d8743dc1acda917398f1d76c" +checksum = "561a8da1a50e1428d3c51321dafeca849df992a5bb67720c386131234caba82e" dependencies = [ "adler32", "crc32fast", @@ -1205,25 +1171,41 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.9" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" dependencies = [ "adler2", "simd-adler32", ] [[package]] -name = "mio" -version = "1.2.2" +name = "multiversion" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +checksum = "b4ca4bea16ffc3f443cf7d866912118196bfef4c6a1556ca00f9f9b00bb43f7c" dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", + "multiversion-macros", +] + +[[package]] +name = "multiversion-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d416831a7317ef4b08bee00b69cbbb9c8763da7959a7026244d6266869f9c83" +dependencies = [ + "proc-macro2", + "quote", + "rustversion", + "syn 3.0.5", ] +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + [[package]] name = "no_std_io2" version = "0.9.4" @@ -1333,12 +1315,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "prettyplease" -version = "0.2.37" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" dependencies = [ "proc-macro2", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1352,9 +1334,9 @@ dependencies = [ [[package]] name = "proc-macro-error-attr3" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0084e6206a967a2dad822180626b2f6b07a3b379325e8f1ec0438e33a469ba7" +checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da" dependencies = [ "proc-macro2", "quote", @@ -1362,14 +1344,14 @@ dependencies = [ [[package]] name = "proc-macro-error3" -version = "3.1.0" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cf066225f2373bc711684792b69bdeac0356019b007e721090c24d92d5d5a50" +checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233" dependencies = [ "proc-macro-error-attr3", "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1384,7 +1366,7 @@ dependencies = [ [[package]] name = "prop-rs" version = "0.2.0" -source = "git+https://github.com/Kernel-SU/ksu_props?rev=6f5723105d8d4cacad31d83d343defbf032c7b33#6f5723105d8d4cacad31d83d343defbf032c7b33" +source = "git+https://github.com/ReSukiSU/ksu_props?rev=ddb6ee7294467f7f25bad2118e9e24eee104144b#ddb6ee7294467f7f25bad2118e9e24eee104144b" dependencies = [ "prost", ] @@ -1392,7 +1374,7 @@ dependencies = [ [[package]] name = "prop-rs-android" version = "0.2.0" -source = "git+https://github.com/Kernel-SU/ksu_props?rev=6f5723105d8d4cacad31d83d343defbf032c7b33#6f5723105d8d4cacad31d83d343defbf032c7b33" +source = "git+https://github.com/ReSukiSU/ksu_props?rev=ddb6ee7294467f7f25bad2118e9e24eee104144b#ddb6ee7294467f7f25bad2118e9e24eee104144b" dependencies = [ "libc", "log", @@ -1417,7 +1399,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", - "itertools 0.14.0", + "itertools", "proc-macro2", "quote", "syn 2.0.119", @@ -1562,9 +1544,9 @@ checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" [[package]] name = "rustix" version = "0.38.34" -source = "git+https://github.com/Kernel-SU/rustix.git?rev=4a53fbc#4a53fbc7cb7a07cabe87125cc21dbc27db316259" +source = "git+https://github.com/ReSukiSU/rustix.git?rev=4a53fbc#4a53fbc7cb7a07cabe87125cc21dbc27db316259" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno 0.3.14", "libc", "linux-raw-sys 0.4.15", @@ -1577,7 +1559,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.13.1", + "bitflags 2.13.2", "errno 0.3.14", "libc", "linux-raw-sys 0.12.1", @@ -1599,6 +1581,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "scroll" version = "0.13.0" @@ -1610,13 +1598,13 @@ dependencies = [ [[package]] name = "scroll_derive" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" +checksum = "e1a36a382ed65dbcc0ab47fd5e9a94112417ccd34560a392ef3b7b0f0ec39148" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", ] [[package]] @@ -1646,7 +1634,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1708,12 +1696,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "shlex" version = "2.0.1" @@ -1727,20 +1709,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] -name = "slab" -version = "0.4.12" +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] -name = "socket2" -version = "0.6.5" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "strsim" @@ -1761,9 +1739,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.4" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", @@ -1806,7 +1784,7 @@ checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.4", + "syn 3.0.5", ] [[package]] @@ -1836,11 +1814,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", - "libc", - "mio", "pin-project-lite", - "socket2", - "windows-sys 0.61.2", ] [[package]] @@ -1854,9 +1828,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.13+spec-1.1.0" +version = "0.25.15+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +checksum = "1340ea94a5856333492c9064b02c778b191dd2c853778d9609debdcdfea3a614" dependencies = [ "indexmap", "toml_datetime", @@ -1931,17 +1905,11 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - [[package]] name = "wasm-bindgen" -version = "0.2.122" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" dependencies = [ "cfg-if", "once_cell", @@ -1952,9 +1920,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.122" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1962,22 +1930,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.122" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.5", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.122" +version = "0.2.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" dependencies = [ "unicode-ident", ] @@ -2203,9 +2171,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" [[package]] name = "zmij" @@ -2236,18 +2204,18 @@ dependencies = [ [[package]] name = "zstd-safe" -version = "7.2.4" +version = "7.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882" dependencies = [ "zstd-sys", ] [[package]] name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +version = "2.1.0+zstd.1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index 0c70020dc..43c1eedf9 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -20,7 +20,7 @@ sha256 = "1" tempfile = "3" chrono = "0.4" regex-lite = "0.1" -android-bootimg = { git = "https://github.com/5ec1cff/android_bootimg", rev = "150425b027c76ea104c82e408571651f2181b2c2" } +android-bootimg = { git = "https://github.com/ReSukiSU/android_bootimg", rev = "150425b027c76ea104c82e408571651f2181b2c2" } memmap2 = "0.9.10" bitflags = "2.11.0" base16ct = { version = "1.0.0", features = ["alloc"] } @@ -43,7 +43,7 @@ zip = { version = "8", features = [ "lzma", "xz", ], default-features = false } -java-properties = { git = "https://github.com/Kernel-SU/java-properties.git", branch = "master", default-features = false } +java-properties = { git = "https://github.com/ReSukiSU/java-properties.git", branch = "master", default-features = false } serde_json = "1" encoding_rs = "0.8" humansize = "2" @@ -56,10 +56,9 @@ derive-new = "0.7" getopts = "0.2" serde = { version = "1.0", features = ["derive"] } ksuinit = { path = "../ksuinit" } -adb_client = { git = "https://github.com/Kernel-SU/adb_client" } +adb_client = { git = "https://github.com/ReSukiSU/adb_client" } num_enum = "0.7" -inotify = "0.11.2" -prop-rs-android = { git = "https://github.com/Kernel-SU/ksu_props", rev = "6f5723105d8d4cacad31d83d343defbf032c7b33" } +prop-rs-android = { git = "https://github.com/ReSukiSU/ksu_props", rev = "ddb6ee7294467f7f25bad2118e9e24eee104144b" } [target.'cfg(not(target_os = "android"))'.dependencies] env_logger = { version = "0.11.10", default-features = false } @@ -71,5 +70,5 @@ lto = true codegen-units = 1 [build-dependencies] -bindgen = "0.72.1" +bindgen = "0.73.2" cc = "1" diff --git a/userspace/ksud/src/android/cli.rs b/userspace/ksud/src/android/cli.rs index fe48bcd1a..d87995c4e 100755 --- a/userspace/ksud/src/android/cli.rs +++ b/userspace/ksud/src/android/cli.rs @@ -461,7 +461,7 @@ enum Profile { enum Feature { /// Get feature value and support status Get { - /// Feature ID or name (su_compat, kernel_umount, sulog, adb_root, selinux_hide, webview_zygote_umount) + /// Feature ID or name (su_compat, kernel_umount, sulog, adb_root, selinux_hide) id: String, /// Read from config file #[arg(long, default_value_t = false)] @@ -481,7 +481,7 @@ enum Feature { /// Check feature status (supported/unsupported/managed) Check { - /// Feature ID or name (su_compat, kernel_umount, sulog, adb_root, selinux_hide, webview_zygote_umount) + /// Feature ID or name (su_compat, kernel_umount, sulog, adb_root, selinux_hide) id: String, }, @@ -572,6 +572,8 @@ pub fn run() -> Result<()> { .with_tag("KernelSU"), ); + ksucalls::setup_sigsys_handler(); + // the kernel executes su with argv[0] = "su" and replace it with us let arg0 = std::env::args().next().unwrap_or_default(); if arg0 == "su" || arg0.ends_with("/su") { @@ -811,6 +813,10 @@ pub fn run() -> Result<()> { println!("uapi_version: {}", info.uapi_version); println!("features: 0x{:x}", info.features); println!("lkm: {}", ksucalls::is_lkm()); + println!( + "bundled: {}", + (info.flags & uapi::KSU_GET_INFO_FLAG_BUNDLED) != 0 + ); println!("late_load: {}", ksucalls::is_late_load()); println!("runtime_mode: {}", ksucalls::runtime_mode()); println!( @@ -871,7 +877,7 @@ pub fn run() -> Result<()> { Kernel::Umount { command } => match command { UmountOp::Add { mnt, flags } => ksucalls::umount_list_add(&mnt, flags), UmountOp::Del { mnt } => ksucalls::umount_list_del(&mnt), - UmountOp::Wipe => ksucalls::umount_list_wipe().map_err(Into::into), + UmountOp::Wipe => ksucalls::umount_list_wipe(), UmountOp::List => { let list = ksucalls::umount_list_list()?; println!("{}", serde_json::to_string(&list)?); diff --git a/userspace/ksud/src/android/feature.rs b/userspace/ksud/src/android/feature.rs index ef6eb6b4f..5d8964eea 100644 --- a/userspace/ksud/src/android/feature.rs +++ b/userspace/ksud/src/android/feature.rs @@ -26,7 +26,6 @@ pub enum FeatureId { Sulog = 2, AdbRoot = 3, SelinuxHide = 4, - WebviewZygoteUmount = 5, } impl FeatureId { @@ -37,7 +36,6 @@ impl FeatureId { 2 => Some(Self::Sulog), 3 => Some(Self::AdbRoot), 4 => Some(Self::SelinuxHide), - 5 => Some(Self::WebviewZygoteUmount), _ => None, } } @@ -49,7 +47,6 @@ impl FeatureId { Self::Sulog => "sulog", Self::AdbRoot => "adb_root", Self::SelinuxHide => "selinux_hide", - Self::WebviewZygoteUmount => "webview_zygote_umount", } } @@ -68,9 +65,6 @@ impl FeatureId { Self::SelinuxHide => { "SELinux Hide - sanitize /sys/fs/selinux access results for app UIDs" } - Self::WebviewZygoteUmount => { - "WebView Zygote Umount - unmount modules from WebView zygote and its isolated children" - } } } } @@ -82,7 +76,6 @@ fn parse_feature_id(name: &str) -> Result { "sulog" | "2" => Ok(FeatureId::Sulog), "adb_root" | "3" => Ok(FeatureId::AdbRoot), "selinux_hide" | "4" => Ok(FeatureId::SelinuxHide), - "webview_zygote_umount" | "5" => Ok(FeatureId::WebviewZygoteUmount), _ => bail!("Unknown feature: {name}"), } } @@ -329,7 +322,6 @@ pub fn list_features() { FeatureId::Sulog, FeatureId::AdbRoot, FeatureId::SelinuxHide, - FeatureId::WebviewZygoteUmount, ]; for feature_id in &all_features { @@ -393,7 +385,6 @@ pub fn save_config() -> Result<()> { FeatureId::Sulog, FeatureId::AdbRoot, FeatureId::SelinuxHide, - FeatureId::WebviewZygoteUmount, ]; for feature_id in &all_features { diff --git a/userspace/ksud/src/android/ksucalls.rs b/userspace/ksud/src/android/ksucalls.rs index 8cf217fd6..db3523e01 100644 --- a/userspace/ksud/src/android/ksucalls.rs +++ b/userspace/ksud/src/android/ksucalls.rs @@ -1,46 +1,133 @@ #![allow(clippy::unreadable_literal)] -use anyhow::bail; +use anyhow::{Result, bail}; -use std::{fs, os::fd::RawFd, sync::OnceLock}; +use std::{cell::Cell, fs, io, os::fd::RawFd, sync::OnceLock}; use crate::{android::uapi, defs::MountInfo}; +// sigsys handler +std::thread_local! { + #[allow(clippy::missing_const_for_thread_local)] + static SVC_IN_FLIGHT: Cell = const { Cell::new(false) }; + #[allow(clippy::missing_const_for_thread_local)] + static SIGSYS_OCCURRED: Cell = const { Cell::new(false) }; +} + +const SYS_SECCOMP: libc::c_int = 1; + +fn with_svc_call(call: F) -> R +where + F: FnOnce() -> R, +{ + SVC_IN_FLIGHT.with(|in_flight| in_flight.set(true)); + let result = call(); + SVC_IN_FLIGHT.with(|in_flight| in_flight.set(false)); + result +} + +fn take_sigsys_occurred() -> bool { + SIGSYS_OCCURRED.with(|occurred| occurred.replace(false)) +} + +extern "C" fn sigsys_handler( + _sig: libc::c_int, + info: *mut libc::siginfo_t, + ctx: *mut libc::c_void, +) { + unsafe { + if info.is_null() || ctx.is_null() || (*info).si_code != SYS_SECCOMP { + return; + } + if SVC_IN_FLIGHT.with(Cell::get) { + SIGSYS_OCCURRED.with(|occurred| occurred.set(true)); + } + + let ucontext = ctx.cast::(); + #[cfg(target_arch = "aarch64")] + { + (*ucontext).uc_mcontext.regs[0] = (-libc::EPERM) as u64; + } + #[cfg(target_arch = "arm")] + { + (*ucontext).uc_mcontext.arm_r0 = (-libc::EPERM) as u32; + } + #[cfg(target_arch = "x86_64")] + { + let rax = libc::REG_RAX as usize; + (*ucontext).uc_mcontext.gregs[rax] = i64::from(-libc::EPERM); + } + } +} + +pub fn setup_sigsys_handler() { + unsafe { + let mut sa: libc::sigaction = std::mem::zeroed(); + sa.sa_flags = libc::SA_SIGINFO; + sa.sa_sigaction = sigsys_handler as *const () as usize; + libc::sigemptyset(std::ptr::addr_of_mut!(sa.sa_mask)); + if libc::sigaction(libc::SIGSYS, std::ptr::addr_of!(sa), std::ptr::null_mut()) != 0 { + let error = std::io::Error::last_os_error(); + log::warn!("Failed to set SIGSYS handler: {error}"); + } + } +} + +const DRIVER_FD_NAME: &str = "anon_inode:[ksu_driver]"; +const SU_DRIVER_FD_NAME: &str = "anon_inode:[ksu_driver_su]"; + // Global driver fd cache static DRIVER_FD: OnceLock = OnceLock::new(); static INFO_CACHE: OnceLock = OnceLock::new(); -fn scan_driver_fd() -> Option { - let fd_dir = fs::read_dir("/proc/self/fd").ok()?; +fn scan_driver_fd() -> io::Result> { + let fd_dir = fs::read_dir("/proc/self/fd")?; + let mut driver_fd = None; for entry in fd_dir.flatten() { if let Ok(fd_num) = entry.file_name().to_string_lossy().parse::() { let link_path = format!("/proc/self/fd/{fd_num}"); if let Ok(target) = fs::read_link(&link_path) { let target_str = target.to_string_lossy(); - if target_str.contains("[ksu_driver]") { - return Some(fd_num); + if target_str == SU_DRIVER_FD_NAME { + return Ok(Some(fd_num)); + } + if target_str == DRIVER_FD_NAME { + driver_fd = Some(fd_num); } } } } - None + Ok(driver_fd) +} + +pub fn claim_inherited_driver_fd() -> io::Result<()> { + if DRIVER_FD.get().is_none() + && let Some(fd) = scan_driver_fd()? + { + let _ = DRIVER_FD.set(fd); + } + Ok(()) } // Get cached driver fd fn init_driver_fd() -> Option { - let fd = scan_driver_fd(); + let fd = scan_driver_fd().ok().flatten(); if fd.is_none() { let mut fd = -1; - unsafe { + with_svc_call(|| unsafe { libc::syscall( libc::SYS_reboot, uapi::KSU_INSTALL_MAGIC1_RUST, uapi::KSU_INSTALL_MAGIC2_RUST, 0, &mut fd, - ); - }; + ) + }); + if take_sigsys_occurred() { + eprintln!("KernelSU driver install syscall was blocked by seccomp"); + log::error!("KernelSU driver install syscall was blocked by seccomp"); + } if fd >= 0 { Some(fd) } else { None } } else { fd @@ -48,17 +135,19 @@ fn init_driver_fd() -> Option { } // ioctl wrapper using libc -pub fn ksuctl(request: u32, arg: *mut T) -> std::io::Result { +pub fn ksuctl(request: u32, arg: *mut T) -> Result { use std::io; let fd = *DRIVER_FD.get_or_init(|| init_driver_fd().unwrap_or(-1)); + if fd < 0 { + bail!("could not retrieve kernelsu driver fd") + } unsafe { let ret = libc::ioctl(fd as libc::c_int, request as i32, arg); if ret < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(ret) + bail!("ioctl failed: {}", io::Error::last_os_error()); } + Ok(ret) } } @@ -140,7 +229,7 @@ pub fn get_full_version() -> String { } } -pub fn grant_root() -> std::io::Result<()> { +pub fn grant_root() -> Result<()> { ksuctl(uapi::KSU_IOCTL_GRANT_ROOT_RUST, std::ptr::null_mut::())?; Ok(()) } @@ -168,7 +257,7 @@ pub fn check_kernel_safemode() -> bool { cmd.in_safe_mode != 0 } -pub fn set_sepolicy(payload: *const u8, payload_len: u64) -> std::io::Result { +pub fn set_sepolicy(payload: *const u8, payload_len: u64) -> Result { let mut ioctl_cmd = uapi::ksu_set_sepolicy_cmd { data_len: payload_len, data: payload as u64, @@ -179,7 +268,7 @@ pub fn set_sepolicy(payload: *const u8, payload_len: u64) -> std::io::Result std::io::Result<(u64, bool)> { +pub fn get_feature(feature_id: u32) -> Result<(u64, bool)> { let mut cmd = uapi::ksu_get_feature_cmd { feature_id, value: 0, @@ -190,13 +279,13 @@ pub fn get_feature(feature_id: u32) -> std::io::Result<(u64, bool)> { } /// Set feature value in kernel -pub fn set_feature(feature_id: u32, value: u64) -> std::io::Result<()> { +pub fn set_feature(feature_id: u32, value: u64) -> Result<()> { let mut cmd = uapi::ksu_set_feature_cmd { feature_id, value }; ksuctl(uapi::KSU_IOCTL_SET_FEATURE_RUST, &raw mut cmd)?; Ok(()) } -pub fn get_wrapped_fd(fd: RawFd) -> std::io::Result { +pub fn get_wrapped_fd(fd: RawFd) -> Result { let mut cmd = uapi::ksu_get_wrapper_fd_cmd { fd: fd as u32, flags: 0, @@ -205,14 +294,14 @@ pub fn get_wrapped_fd(fd: RawFd) -> std::io::Result { Ok(result) } -pub fn get_sulog_fd() -> std::io::Result { +pub fn get_sulog_fd() -> Result { let mut cmd = uapi::ksu_get_sulog_fd_cmd { flags: 0 }; let result = ksuctl(uapi::KSU_IOCTL_GET_SULOG_FD, &raw mut cmd)?; Ok(result) } /// Get mark status for a process (pid=0 returns total marked count) -pub fn mark_get(pid: i32) -> std::io::Result { +pub fn mark_get(pid: i32) -> Result { let mut cmd = uapi::ksu_manage_mark_cmd { operation: uapi::KSU_MARK_GET_RUST, pid, @@ -223,7 +312,7 @@ pub fn mark_get(pid: i32) -> std::io::Result { } /// Mark a process (pid=0 marks all processes) -pub fn mark_set(pid: i32) -> std::io::Result<()> { +pub fn mark_set(pid: i32) -> Result<()> { let mut cmd = uapi::ksu_manage_mark_cmd { operation: uapi::KSU_MARK_MARK_RUST, pid, @@ -234,7 +323,7 @@ pub fn mark_set(pid: i32) -> std::io::Result<()> { } /// Unmark a process (pid=0 unmarks all processes) -pub fn mark_unset(pid: i32) -> std::io::Result<()> { +pub fn mark_unset(pid: i32) -> Result<()> { let mut cmd = uapi::ksu_manage_mark_cmd { operation: uapi::KSU_MARK_UNMARK_RUST, pid, @@ -245,7 +334,7 @@ pub fn mark_unset(pid: i32) -> std::io::Result<()> { } /// Refresh mark for all running processes -pub fn mark_refresh() -> std::io::Result<()> { +pub fn mark_refresh() -> Result<()> { let mut cmd = uapi::ksu_manage_mark_cmd { operation: uapi::KSU_MARK_REFRESH_RUST, pid: 0, @@ -265,7 +354,7 @@ pub fn nuke_ext4_sysfs(mnt: &str) -> anyhow::Result<()> { } /// Wipe all entries from umount list -pub fn umount_list_wipe() -> std::io::Result<()> { +pub fn umount_list_wipe() -> Result<()> { let mut cmd = uapi::ksu_manage_try_umount_cmd { arg: 0, flags: 0, @@ -300,7 +389,7 @@ pub fn umount_list_del(path: &str) -> anyhow::Result<()> { } /// Set current process's process group to init_group (pgid = 0) -pub fn set_init_pgrp() -> std::io::Result<()> { +pub fn set_init_pgrp() -> Result<()> { ksuctl( uapi::KSU_IOCTL_SET_INIT_PGRP_RUST, std::ptr::null_mut::(), diff --git a/userspace/ksud/src/android/late_load/mod.rs b/userspace/ksud/src/android/late_load/mod.rs index 040eb5506..aa90136df 100644 --- a/userspace/ksud/src/android/late_load/mod.rs +++ b/userspace/ksud/src/android/late_load/mod.rs @@ -67,6 +67,7 @@ pub fn run(package_name: &String, kmi: Option, allow_shell: bool) -> Res // 4. Load kernelsu.ko from memory with manual relocation info!("Loading kernelsu.ko for KMI {kmi}..."); + // bundled flag is meaningless in jailbreak mode since we can't flash boot to update it. let params = if allow_shell { cstr!("allow_shell=1") } else { diff --git a/userspace/ksud/src/android/resetprop.rs b/userspace/ksud/src/android/resetprop.rs index 3f50e9121..dc67486ea 100644 --- a/userspace/ksud/src/android/resetprop.rs +++ b/userspace/ksud/src/android/resetprop.rs @@ -187,8 +187,12 @@ fn execute(cli: &Args) -> Result<()> { if let Some(path) = &cli.file { let file = File::open(path).with_context(|| format!("Failed to open {path}"))?; let reader = BufReader::new(file); - rp.load_props(reader.lines()) - .context("Failed to load properties from file")?; + if rp + .load_props(reader.lines()) + .context("Failed to load properties from file")? + { + eprintln!("resetprop: warning: rebuild is needed!"); + } return Ok(()); } @@ -225,8 +229,12 @@ fn execute(cli: &Args) -> Result<()> { match (name, value) { // resetprop name value (set) (Some(name), Some(value)) => { - rp.set(name, value) - .with_context(|| format!("Failed to set {name}"))?; + if rp + .set(name, value) + .with_context(|| format!("Failed to set {name}"))? + { + eprintln!("resetprop: warning: rebuild is needed!"); + } } // resetprop name (get) @@ -272,7 +280,8 @@ pub(crate) fn set_property_direct(name: &str, value: &str) -> Result<()> { sys_prop::init().context("Failed to initialize system property API")?; direct_resetprop() .set(name, value) - .with_context(|| format!("Failed to set {name}")) + .with_context(|| format!("Failed to set {name}"))?; + Ok(()) } /// Load system.prop file using internal resetprop API. @@ -292,8 +301,15 @@ pub fn load_system_prop_file(path: &Path) -> Result<()> { let file = File::open(path).with_context(|| format!("Failed to open {}", path.display()))?; let reader = BufReader::new(file); - rp.load_props(reader.lines()) - .with_context(|| format!("Failed to load properties from {}", path.display()))?; + if rp + .load_props(reader.lines()) + .with_context(|| format!("Failed to load properties from {}", path.display()))? + { + log::warn!( + "warning: after loaded prop file from {}, rebuild is needed!", + path.display() + ); + } info!("Loaded system.prop from {}", path.display()); Ok(()) diff --git a/userspace/ksud/src/android/su.rs b/userspace/ksud/src/android/su.rs index f21d0c52f..f504974c7 100644 --- a/userspace/ksud/src/android/su.rs +++ b/userspace/ksud/src/android/su.rs @@ -4,11 +4,12 @@ use std::{ cmp::Ordering, env, ffi::{CStr, CString}, + io, path::PathBuf, process::Command, }; -use anyhow::{Context, Ok, Result, bail}; +use anyhow::{Context, Ok, Result, anyhow, bail}; use getopts::Options; use libc::c_int; use log::error; @@ -19,6 +20,7 @@ use rustix::{ use crate::{ android::{ + ksucalls, ksucalls::{get_wrapped_fd, set_ksu_no_new_privs}, utils::{self, umask}, }, @@ -82,16 +84,27 @@ fn set_selinux_context(context: &str) -> Result<()> { fn wrap_tty(fd: c_int) { let inner_fn = move || -> Result<()> { - if unsafe { libc::isatty(fd) != 1 } { + if unsafe { libc::isatty(fd) != 1 } + && io::Error::last_os_error().raw_os_error() != Some(libc::EACCES) + { return Ok(()); } + + // The root profile is already active here, so its SELinux domain may + // return EACCES while querying the original terminal. In that case, + // check the wrapped fd instead, since that descriptor is intended to + // bypass this restriction. let new_fd = get_wrapped_fd(fd).context("get_wrapped_fd")?; - if unsafe { libc::dup2(new_fd, fd) } == -1 { - bail!("dup {new_fd} -> {fd} errno: {}", unsafe { - *libc::__errno() - }); + if unsafe { libc::isatty(new_fd) != 1 } { + unsafe { libc::close(new_fd) }; + return Ok(()); } + let dup_result = unsafe { libc::dup2(new_fd, fd) }; + let dup_errno = unsafe { *libc::__errno() }; unsafe { libc::close(new_fd) }; + if dup_result == -1 { + bail!("dup {new_fd} -> {fd} errno: {dup_errno}"); + } Ok(()) }; @@ -102,9 +115,13 @@ fn wrap_tty(fd: c_int) { #[allow(clippy::similar_names)] pub fn root_shell() -> Result<()> { - // we are root now, this was set in kernel! + // The kernel has already applied the selected root profile. + + // A su-session driver fd deliberately survives the exec into ksud. Claim + // it before handling any arguments and restore FD_CLOEXEC so it cannot + // leak into the target shell, including when fd wrapping is disabled. + ksucalls::claim_inherited_driver_fd().context("claim inherited KernelSU driver fd")?; - use anyhow::anyhow; let env_args: Vec = env::args().collect(); let program = env_args[0].clone(); let mut executable: Option = None; diff --git a/userspace/ksud/src/android/sulog.rs b/userspace/ksud/src/android/sulog.rs index a7ca03937..3b959416b 100644 --- a/userspace/ksud/src/android/sulog.rs +++ b/userspace/ksud/src/android/sulog.rs @@ -641,22 +641,22 @@ fn handle_readable(fd: RawFd, writer: &mut DailyLogWriter) -> Result } } -pub fn open_sulog_fd() -> io::Result { +pub fn open_sulog_fd() -> Result { let fd = ksucalls::get_sulog_fd()?; - let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) }; + let fd = unsafe { OwnedFd::from_raw_fd(fd) }; + let flags = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETFL) }; if flags < 0 { - let err = io::Error::last_os_error(); - let _ = unsafe { libc::close(fd) }; - return Err(err); + bail!("open_sulog_fd: get flags: {}", io::Error::last_os_error()); } - if unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 { - let err = io::Error::last_os_error(); - let _ = unsafe { libc::close(fd) }; - return Err(err); + if unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_SETFL, flags | libc::O_NONBLOCK) } < 0 { + bail!( + "open_sulog_fd: set cloexec flags: {}", + io::Error::last_os_error() + ) } - Ok(unsafe { OwnedFd::from_raw_fd(fd) }) + Ok(fd) } fn write_session_marker( diff --git a/userspace/ksud/src/android/susfs/cli.rs b/userspace/ksud/src/android/susfs/cli.rs index d0748e904..2ad039c7e 100644 --- a/userspace/ksud/src/android/susfs/cli.rs +++ b/userspace/ksud/src/android/susfs/cli.rs @@ -78,7 +78,7 @@ pub enum SuSFSSubCommands { /// This command must be completed with later after the added path is bind mounted or overlayed. /// /// * Important Notes * - /// - Only effective for umounted process with uid >= 10000. + /// - Effective for all processes with uid >= 10000 #[command(name = "add_sus_kstat")] AddSusKstat { /// Path of file or directory @@ -90,7 +90,7 @@ pub enum SuSFSSubCommands { /// This updates the target ino, but size and blocks are remained the same as current stat. /// /// * Important Notes * - /// - Only effective for umounted process with uid >= 10000. + /// - Effective for all processes with uid >= 10000 #[command(name = "update_sus_kstat")] UpdateSusKstat { /// Path of file or directory @@ -102,7 +102,7 @@ pub enum SuSFSSubCommands { /// This updates the target ino only, other stat members are remained the same as the original stat. /// /// * Important Notes * - /// - Only effective for umounted process with uid >= 10000. + /// - Effective for all processes with uid >= 10000 #[command(name = "update_sus_kstat_full_clone")] UpdateSusKstatFullClone { /// Path of file or directory @@ -112,7 +112,7 @@ pub enum SuSFSSubCommands { /// Spoof the kstat of a file or directory by static fields. /// /// * Important Notes * - /// - Only effective for umounted process with uid >= 10000. + /// - Effective for all processes with uid >= 10000. #[command(name = "add_sus_kstat_statically")] AddSusKstatStatically { /// Path of file or directory diff --git a/userspace/ksud/src/android/unload.rs b/userspace/ksud/src/android/unload.rs index a4bff5120..1f405fae4 100644 --- a/userspace/ksud/src/android/unload.rs +++ b/userspace/ksud/src/android/unload.rs @@ -64,7 +64,7 @@ fn find_ksu_fd_holders() -> Vec { let link_path = fd_entry.path(); if let Ok(target) = fs::read_link(&link_path) { let target_str = target.to_string_lossy(); - if target_str.contains("[ksu_driver]") || target_str.contains("[ksu_fdwrapper]") { + if target_str.contains("[ksu_driver") || target_str.contains("[ksu_fdwrapper]") { pids.push(pid); break; } @@ -95,7 +95,7 @@ fn close_ksu_fds() { }; if let Ok(target) = fs::read_link(entry.path()) { let target_str = target.to_string_lossy(); - if target_str.contains("[ksu_driver]") || target_str.contains("[ksu_fdwrapper]") { + if target_str.contains("[ksu_driver") || target_str.contains("[ksu_fdwrapper]") { info!("unload: closing fd {fd} -> {target_str}"); unsafe { libc::close(fd); diff --git a/userspace/ksud/src/boot_patch.rs b/userspace/ksud/src/boot_patch.rs index 7813e6801..f3b4265f6 100644 --- a/userspace/ksud/src/boot_patch.rs +++ b/userspace/ksud/src/boot_patch.rs @@ -554,6 +554,9 @@ pub fn patch(args: BootPatchArgs) -> Result<()> { ); } + // None means --no-install: preserve the marker for the existing LKM. + let bundled_lkm = (!no_install).then_some(kmod.is_none()); + let kmi = kmi.map_or_else( || -> Result<_> { if kmod.is_some() { @@ -738,6 +741,9 @@ pub fn patch(args: BootPatchArgs) -> Result<()> { apply_config("no custom rc", "norc=1", no_custom_rc); apply_config("allow shell", "allow_shell=1", allow_shell); + if let Some(bundled) = bundled_lkm { + apply_config("bundled LKM", "bundled=1", bundled); + } if ksu_config.is_empty() { cpio.rm("ksu_config", false); diff --git a/userspace/ksuinit/Cargo.lock b/userspace/ksuinit/Cargo.lock index 7f576677a..d84cacac9 100644 --- a/userspace/ksuinit/Cargo.lock +++ b/userspace/ksuinit/Cargo.lock @@ -10,9 +10,9 @@ checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "bitflags" -version = "2.13.1" +version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" [[package]] name = "errno" @@ -38,7 +38,7 @@ dependencies = [ [[package]] name = "kernlog" version = "0.3.1" -source = "git+https://github.com/kstep/kernlog.rs#68caa7bf1e27baea35b00ebba786cafae0bca90f" +source = "git+https://github.com/ReSukiSU/kernlog.rs#68caa7bf1e27baea35b00ebba786cafae0bca90f" dependencies = [ "libc", "log", @@ -102,7 +102,7 @@ dependencies = [ [[package]] name = "rustix" version = "0.38.34" -source = "git+https://github.com/Kernel-SU/rustix.git?rev=4a53fbc#4a53fbc7cb7a07cabe87125cc21dbc27db316259" +source = "git+https://github.com/ReSukiSU/rustix.git?rev=4a53fbc#4a53fbc7cb7a07cabe87125cc21dbc27db316259" dependencies = [ "bitflags", "errno", @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "scroll_derive" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d" +checksum = "e1a36a382ed65dbcc0ab47fd5e9a94112417ccd34560a392ef3b7b0f0ec39148" dependencies = [ "proc-macro2", "quote", @@ -133,9 +133,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.119" +version = "3.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" dependencies = [ "proc-macro2", "quote", diff --git a/userspace/ksuinit/Cargo.toml b/userspace/ksuinit/Cargo.toml index bded45dbe..70dd3aa51 100644 --- a/userspace/ksuinit/Cargo.toml +++ b/userspace/ksuinit/Cargo.toml @@ -12,7 +12,7 @@ goblin = "0.10" scroll = "0.13" anyhow = "1" -rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc", features = ["mount", "fs", "runtime", "system", "process"] } +rustix = { git = "https://github.com/ReSukiSU/rustix.git", rev = "4a53fbc", features = ["mount", "fs", "runtime", "system", "process"] } syscalls = { version = "0.8", default-features = false, features = [ "aarch64", @@ -21,7 +21,7 @@ syscalls = { version = "0.8", default-features = false, features = [ # for kmsg logging log = "0.4" -kernlog = { git = "https://github.com/kstep/kernlog.rs" } +kernlog = { git = "https://github.com/ReSukiSU/kernlog.rs" } [profile.release] strip = true