diff --git a/README.md b/README.md deleted file mode 100644 index e5a86f0..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# sukisu - diff --git a/docs/guide/tracepoint-hook.md b/docs/guide/tracepoint-hook.md index 8333561..af5fa72 100644 --- a/docs/guide/tracepoint-hook.md +++ b/docs/guide/tracepoint-hook.md @@ -44,7 +44,7 @@ Generally need to modify the `do_execve` and `compat_do_execve` methods in `fs/e .ptr.compat = __envp, }; +#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+ trace_ksu_trace_execveat_sucompat_hook((int *)AT_FDCWD, &filename, NULL, NULL, NULL); /* 32-bit su */ ++ trace_ksu_trace_execveat_hook((int *)AT_FDCWD, &filename, &argv, &envp, 0); // 32-bit su and 32-on-64 support +#endif return do_execveat_common(AT_FDCWD, filename, argv, envp, 0); } @@ -237,34 +237,3 @@ Need to modify the `input_event` method in `drivers/input/input.c`, not `input_h spin_lock_irqsave(&dev->event_lock, flags); ``` - -### devpts Hook (`pty.c`) - -Need to modify the `pts_unix98_lookup` method in `drivers/tty/pty.c` - -```patch ---- a/drivers/tty/pty.c -+++ b/drivers/tty/pty.c -@@ -31,6 +31,10 @@ - #include - #include "tty.h" - -+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+#include <../../drivers/kernelsu/ksu_trace.h> -+#endif -+ - #undef TTY_DEBUG_HANGUP - #ifdef TTY_DEBUG_HANGUP - # define tty_debug_hangup(tty, f, args...) tty_debug(tty, f, ##args) -@@ -707,6 +711,10 @@ static struct tty_struct *pts_unix98_lookup(struct tty_driver *driver, - { - struct tty_struct *tty; - -+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+ trace_ksu_trace_devpts_hook((struct inode *)file->f_path.dentry->d_inode); -+#endif -+ - mutex_lock(&devpts_mutex); - tty = devpts_get_priv(file->f_path.dentry); - mutex_unlock(&devpts_mutex); -``` diff --git a/docs/zh/guide/tracepoint-hook.md b/docs/zh/guide/tracepoint-hook.md index 3b98fbd..7dde784 100644 --- a/docs/zh/guide/tracepoint-hook.md +++ b/docs/zh/guide/tracepoint-hook.md @@ -44,7 +44,7 @@ .ptr.compat = __envp, }; +#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+ trace_ksu_trace_execveat_sucompat_hook((int *)AT_FDCWD, &filename, NULL, NULL, NULL); /* 32-bit su */ ++ trace_ksu_trace_execveat_hook((int *)AT_FDCWD, &filename, &argv, &envp, 0)); // 32-bit su and 32-on-64 support +#endif return do_execveat_common(AT_FDCWD, filename, argv, envp, 0); } @@ -236,35 +236,4 @@ if (is_event_supported(type, dev->evbit, EV_MAX)) { spin_lock_irqsave(&dev->event_lock, flags); -``` - -### devpts 钩子 (`pty.c`) - -需要修改 `drivers/tty/pty.c` 的 `pts_unix98_lookup` 方法 - -```patch ---- a/drivers/tty/pty.c -+++ b/drivers/tty/pty.c -@@ -31,6 +31,10 @@ - #include - #include "tty.h" - -+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+#include <../../drivers/kernelsu/ksu_trace.h> -+#endif -+ - #undef TTY_DEBUG_HANGUP - #ifdef TTY_DEBUG_HANGUP - # define tty_debug_hangup(tty, f, args...) tty_debug(tty, f, ##args) -@@ -707,6 +711,10 @@ static struct tty_struct *pts_unix98_lookup(struct tty_driver *driver, - { - struct tty_struct *tty; - -+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+ trace_ksu_trace_devpts_hook((struct inode *)file->f_path.dentry->d_inode); -+#endif -+ - mutex_lock(&devpts_mutex); - tty = devpts_get_priv(file->f_path.dentry); - mutex_unlock(&devpts_mutex); -``` +``` \ No newline at end of file diff --git a/icon/logo.png b/icon/logo.png deleted file mode 100644 index cddff92..0000000 Binary files a/icon/logo.png and /dev/null differ diff --git a/kernel/.clang-format b/kernel/.clang-format index 10dc5a9..6453cf9 100644 --- a/kernel/.clang-format +++ b/kernel/.clang-format @@ -56,8 +56,8 @@ ColumnLimit: 80 CommentPragmas: '^ IWYU pragma:' #CompactNamespaces: false # Unknown to clang-format-4.0 ConstructorInitializerAllOnOneLineOrOnePerLine: false -ConstructorInitializerIndentWidth: 8 -ContinuationIndentWidth: 8 +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 Cpp11BracedListStyle: false DerivePointerAlignment: false DisableFormat: false @@ -501,7 +501,7 @@ IncludeCategories: IncludeIsMainRegex: '(Test)?$' IndentCaseLabels: false #IndentPPDirectives: None # Unknown to clang-format-5.0 -IndentWidth: 8 +IndentWidth: 4 IndentWrappedFunctionNames: false JavaScriptQuotes: Leave JavaScriptWrapImports: true @@ -511,7 +511,7 @@ MacroBlockEnd: '' MaxEmptyLinesToKeep: 1 NamespaceIndentation: None #ObjCBinPackProtocolList: Auto # Unknown to clang-format-5.0 -ObjCBlockIndentWidth: 8 +ObjCBlockIndentWidth: 4 ObjCSpaceAfterProperty: true ObjCSpaceBeforeProtocolList: true @@ -543,6 +543,6 @@ SpacesInCStyleCastParentheses: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Cpp03 -TabWidth: 8 -UseTab: Always +TabWidth: 4 +UseTab: Never ... diff --git a/kernel/.gitignore b/kernel/.gitignore new file mode 100644 index 0000000..20d68ae --- /dev/null +++ b/kernel/.gitignore @@ -0,0 +1,22 @@ +.cache/ +.thinlto-cache/ +compile_commands.json +*.ko +*.o +*.mod +*.lds +*.mod.o +.*.o* +.*.mod* +*.ko* +*.mod.c +*.symvers* +*.order +.*.ko.cmd +.tmp_versions/ +libs/ +obj/ + +CLAUDE.md +.ddk-version +.vscode/settings.json \ No newline at end of file diff --git a/kernel/.vscode/c_cpp_properties.json b/kernel/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..f661370 --- /dev/null +++ b/kernel/.vscode/c_cpp_properties.json @@ -0,0 +1,11 @@ +{ + "configurations": [ + { + "name": "Linux", + "cStandard": "c11", + "intelliSenseMode": "gcc-arm64", + "compileCommands": "${workspaceFolder}/compile_commands.json" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/kernel/.vscode/generate_compdb.py b/kernel/.vscode/generate_compdb.py new file mode 100644 index 0000000..8866913 --- /dev/null +++ b/kernel/.vscode/generate_compdb.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +from __future__ import print_function, division + +import argparse +import fnmatch +import functools +import json +import math +import multiprocessing +import os +import re +import sys + + +CMD_VAR_RE = re.compile(r'^\s*(?:saved)?cmd_(\S+)\s*:=\s*(.+)\s*$', re.MULTILINE) +SOURCE_VAR_RE = re.compile(r'^\s*source_(\S+)\s*:=\s*(.+)\s*$', re.MULTILINE) + + +def print_progress_bar(progress): + progress_bar = '[' + '|' * int(50 * progress) + '-' * int(50 * (1.0 - progress)) + ']' + print('\r', progress_bar, "{0:.1%}".format(progress), end='\r', file=sys.stderr) + + +def parse_cmd_file(out_dir, cmdfile_path): + with open(cmdfile_path, 'r') as cmdfile: + cmdfile_content = cmdfile.read() + + commands = { match.group(1): match.group(2) for match in CMD_VAR_RE.finditer(cmdfile_content) } + sources = { match.group(1): match.group(2) for match in SOURCE_VAR_RE.finditer(cmdfile_content) } + + return [{ + 'directory': out_dir, + 'command': commands[o_file_name], + 'file': source, + 'output': o_file_name + } for o_file_name, source in sources.items()] + + +def gen_compile_commands(cmd_file_search_path, out_dir): + print("Building *.o.cmd file list...", file=sys.stderr) + + out_dir = os.path.abspath(out_dir) + + if not cmd_file_search_path: + cmd_file_search_path = [out_dir] + + cmd_files = [] + for search_path in cmd_file_search_path: + if (os.path.isdir(search_path)): + for cur_dir, subdir, files in os.walk(search_path): + cmd_files.extend(os.path.join(cur_dir, cmdfile_name) for cmdfile_name in fnmatch.filter(files, '*.o.cmd')) + else: + cmd_files.extend(search_path) + + if not cmd_files: + print("No *.o.cmd files found in", ", ".join(cmd_file_search_path), file=sys.stderr) + return + + print("Parsing *.o.cmd files...", file=sys.stderr) + + n_processed = 0 + print_progress_bar(0) + + compdb = [] + pool = multiprocessing.Pool() + try: + for compdb_chunk in pool.imap_unordered(functools.partial(parse_cmd_file, out_dir), cmd_files, chunksize=int(math.sqrt(len(cmd_files)))): + compdb.extend(compdb_chunk) + n_processed += 1 + print_progress_bar(n_processed / len(cmd_files)) + + finally: + pool.terminate() + pool.join() + + print(file=sys.stderr) + print("Writing compile_commands.json...", file=sys.stderr) + + with open('compile_commands.json', 'w') as compdb_file: + json.dump(compdb, compdb_file, indent=1) + + +def main(): + cmd_parser = argparse.ArgumentParser() + cmd_parser.add_argument('-O', '--out-dir', type=str, default=os.getcwd(), help="Build output directory") + cmd_parser.add_argument('cmd_file_search_path', nargs='*', help="*.cmd file search path") + gen_compile_commands(**vars(cmd_parser.parse_args())) + + +if __name__ == '__main__': + main() diff --git a/kernel/.vscode/tasks.json b/kernel/.vscode/tasks.json new file mode 100644 index 0000000..4ed9adb --- /dev/null +++ b/kernel/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Generate compile_commands.json", + "type": "process", + "command": "python", + "args": [ + "${workspaceRoot}/.vscode/generate_compdb.py" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/kernel/Kconfig b/kernel/Kconfig index a24a5b9..fd5e0c1 100644 --- a/kernel/Kconfig +++ b/kernel/Kconfig @@ -1,57 +1,42 @@ menu "KernelSU" config KSU - tristate "KernelSU function support" - depends on OVERLAY_FS - default y - help - Enable kernel-level root privileges on Android System. - To compile as a module, choose M here: the - module will be called kernelsu. + tristate "KernelSU function support" + default y + help + Enable kernel-level root privileges on Android System. + To compile as a module, choose M here: the + module will be called kernelsu. config KSU_DEBUG - bool "KernelSU debug mode" - depends on KSU - default n - help - Enable KernelSU debug mode. + bool "KernelSU debug mode" + depends on KSU + default n + help + Enable KernelSU debug mode. + +config KSU_MANUAL_SU + bool "Use manual su" + depends on KSU + default y + help + Use manual su and authorize the corresponding command line and application via prctl config KPM - bool "Enable SukiSU KPM" - depends on KSU && 64BIT - default n - help - Enabling this option will activate the KPM feature of SukiSU. - This option is suitable for scenarios where you need to force KPM to be enabled. - but it may affect system stability. - select KALLSYMS - select KALLSYMS_ALL - -choice - prompt "KernelSU hook type" - depends on KSU - default KSU_KPROBES_HOOK + bool "Enable SukiSU KPM" + depends on KSU && 64BIT + default n help - Hook type for KernelSU - -config KSU_KPROBES_HOOK - bool "Hook KernelSU with Kprobes" - depends on KPROBES - help - If enabled, Hook required KernelSU syscalls with Kernel-probe. - -config KSU_TRACEPOINT_HOOK - bool "Hook KernelSU with Tracepoint" - depends on TRACEPOINTS - help - If enabled, Hook required KernelSU syscalls with Tracepoint. + Enabling this option will activate the KPM feature of SukiSU. + This option is suitable for scenarios where you need to force KPM to be enabled. + but it may affect system stability. + select KALLSYMS + select KALLSYMS_ALL config KSU_MANUAL_HOOK bool "Hook KernelSU manually" - depends on KSU != m - help - If enabled, Hook required KernelSU syscalls with manually-patched function. - -endchoice + depends on KSU != m + help + If enabled, Hook required KernelSU syscalls with manually-patched function. endmenu diff --git a/kernel/Makefile b/kernel/Makefile index 0c2ba39..8b246bd 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -1,17 +1,28 @@ kernelsu-objs := ksu.o kernelsu-objs += allowlist.o +kernelsu-objs += app_profile.o kernelsu-objs += dynamic_manager.o kernelsu-objs += apk_sign.o kernelsu-objs += sucompat.o +kernelsu-objs += syscall_hook_manager.o kernelsu-objs += throne_tracker.o -kernelsu-objs += core_hook.o +kernelsu-objs += pkg_observer.o +kernelsu-objs += throne_tracker.o +kernelsu-objs += umount_manager.o +kernelsu-objs += setuid_hook.o +kernelsu-objs += kernel_umount.o +kernelsu-objs += supercalls.o +kernelsu-objs += feature.o kernelsu-objs += ksud.o kernelsu-objs += embed_ksud.o -kernelsu-objs += kernel_compat.o +kernelsu-objs += seccomp_cache.o +kernelsu-objs += file_wrapper.o kernelsu-objs += throne_comm.o +kernelsu-objs += sulog.o -ifeq ($(CONFIG_KSU_TRACEPOINT_HOOK), y) -kernelsu-objs += ksu_trace.o +ifeq ($(CONFIG_KSU_MANUAL_SU), y) +ccflags-y += -DCONFIG_KSU_MANUAL_SU +kernelsu-objs += manual_su.o endif kernelsu-objs += selinux/selinux.o @@ -21,46 +32,62 @@ ccflags-y += -I$(srctree)/security/selinux -I$(srctree)/security/selinux/include ccflags-y += -I$(objtree)/security/selinux -include $(srctree)/include/uapi/asm-generic/errno.h obj-$(CONFIG_KSU) += kernelsu.o -obj-$(CONFIG_KSU_TRACEPOINT_HOOK) += ksu_trace_export.o obj-$(CONFIG_KPM) += kpm/ - REPO_OWNER := SukiSU-Ultra REPO_NAME := SukiSU-Ultra REPO_BRANCH := main -KSU_VERSION_API := 3.2.0 +KSU_VERSION_API := 4.0.0 GIT_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git CURL_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin curl +KDIR := $(KDIR) +MDIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))) + +ifneq ($(KDIR),) +$(info -- KDIR: $(KDIR)) +$(info -- MDIR: $(MDIR)) +endif + KSU_GITHUB_VERSION := $(shell $(CURL_BIN) -s "https://api.github.com/repos/$(REPO_OWNER)/$(REPO_NAME)/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') KSU_GITHUB_VERSION_COMMIT := $(shell $(CURL_BIN) -sI "https://api.github.com/repos/$(REPO_OWNER)/$(REPO_NAME)/commits?sha=$(REPO_BRANCH)&per_page=1" | grep -i "link:" | sed -n 's/.*page=\([0-9]*\)>; rel="last".*/\1/p') -LOCAL_GIT_EXISTS := $(shell test -e $(srctree)/$(src)/../.git && echo 1 || echo 0) +ifeq ($(findstring $(srctree),$(src)),$(srctree)) + KSU_SRC := $(src) +else + KSU_SRC := $(srctree)/$(src) +endif + +ifneq ($(shell test -e $(KSU_SRC)/../.git && echo "in-tree"),in-tree) + KSU_SRC := $(MDIR) +endif + +LOCAL_GIT_EXISTS := $(shell test -e $(KSU_SRC)/../.git && echo 1 || echo 0) define get_ksu_version_full -v$1-$(shell cd $(srctree)/$(src); $(GIT_BIN) rev-parse --short=8 HEAD)@$(shell cd $(srctree)/$(src); $(GIT_BIN) rev-parse --abbrev-ref HEAD) +v$1-$(shell cd $(KSU_SRC); $(GIT_BIN) rev-parse --short=8 HEAD)@$(shell cd $(KSU_SRC); $(GIT_BIN) rev-parse --abbrev-ref HEAD) endef ifeq ($(KSU_GITHUB_VERSION_COMMIT),) ifeq ($(LOCAL_GIT_EXISTS),1) - $(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) - KSU_LOCAL_VERSION := $(shell cd $(srctree)/$(src); $(GIT_BIN) rev-list --count $(REPO_BRANCH)) - KSU_VERSION := $(shell expr 10000 + $(KSU_LOCAL_VERSION) + 700) + $(shell cd $(KSU_SRC); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) + KSU_LOCAL_VERSION := $(shell cd $(KSU_SRC); $(GIT_BIN) rev-list --count $(REPO_BRANCH)) + KSU_VERSION := $(shell expr 40000 + $(KSU_LOCAL_VERSION) - 2815) $(info -- $(REPO_NAME) version (local .git): $(KSU_VERSION)) else KSU_VERSION := 13000 $(warning -- Could not fetch version online or via local .git! Using fallback version: $(KSU_VERSION)) endif else - KSU_VERSION := $(shell expr 10000 + $(KSU_GITHUB_VERSION_COMMIT) + 700) + KSU_VERSION := $(shell expr 40000 + $(KSU_GITHUB_VERSION_COMMIT) - 2815) $(info -- $(REPO_NAME) version (GitHub): $(KSU_VERSION)) endif ifeq ($(KSU_GITHUB_VERSION),) ifeq ($(LOCAL_GIT_EXISTS),1) - $(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) + $(shell cd $(KSU_SRC); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) KSU_VERSION_FULL := $(call get_ksu_version_full,$(KSU_VERSION_API)) $(info -- $(REPO_NAME) version (local .git): $(KSU_VERSION_FULL)) $(info -- $(REPO_NAME) Formatted version (local .git): $(KSU_VERSION)) @@ -69,7 +96,7 @@ ifeq ($(KSU_GITHUB_VERSION),) $(warning -- $(REPO_NAME) version: $(KSU_VERSION_FULL)) endif else - $(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) + $(shell cd $(KSU_SRC); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) KSU_VERSION_FULL := $(call get_ksu_version_full,$(KSU_GITHUB_VERSION)) $(info -- $(REPO_NAME) version (Github): $(KSU_VERSION_FULL)) endif @@ -93,14 +120,13 @@ ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\" $(info -- SukiSU Manager package name: $(KSU_MANAGER_PACKAGE)) endif -$(info -- Supported Unofficial Manager: 5ec1cff (GKI) ShirkNeko udochina (GKI and KPM)) - -ifeq ($(CONFIG_KSU_KPROBES_HOOK), y) -$(info -- SukiSU: CONFIG_KSU_KPROBES_HOOK) -else ifeq ($(CONFIG_KSU_TRACEPOINT_HOOK), y) -$(info -- SukiSU: CONFIG_KSU_TRACEPOINT_HOOK) -else ifeq ($(CONFIG_KSU_MANUAL_HOOK), y) -$(info -- SukiSU: CONFIG_KSU_MANUAL_HOOK) +ifeq ($(CONFIG_KSU_MANUAL_HOOK), y) +ccflags-y += -DKSU_MANUAL_HOOK +$(info -- SukiSU: KSU_MANUAL_HOOK Temporarily discontinued)) +else +ccflags-y += -DKSU_KPROBES_HOOK +ccflags-y += -DKSU_TP_HOOK +$(info -- SukiSU: KSU_TRACEPOINT_HOOK) endif KERNEL_VERSION := $(VERSION).$(PATCHLEVEL) @@ -117,14 +143,30 @@ endif $(info -- KERNEL_VERSION: $(KERNEL_VERSION)) $(info -- KERNEL_TYPE: $(KERNEL_TYPE)) -$(info -- KERNEL_VERSION: $(KERNEL_VERSION)) ifeq ($(CONFIG_KPM), y) $(info -- KPM is enabled) else $(info -- KPM is disabled) endif -ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat -ccflags-y += -Wno-declaration-after-statement -Wno-unused-function +# Check new vfs_getattr() +ifeq ($(shell grep -A1 "^int vfs_getattr" $(srctree)/fs/stat.c | grep -q "query_flags" ; echo $$?),0) +ccflags-y += -DKSU_HAS_NEW_VFS_GETATTR +endif + +# Function proc_ops check +ifeq ($(shell grep -q "struct proc_ops " $(srctree)/include/linux/proc_fs.h; echo $$?),0) +ccflags-y += -DKSU_COMPAT_HAS_PROC_OPS +endif + +ccflags-y += -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat -Wno-missing-prototypes +ccflags-y += -Wno-declaration-after-statement -Wno-unused-function -Wno-unused-variable + +all: + make -C $(KDIR) M=$(MDIR) modules +compdb: + python3 $(MDIR)/.vscode/generate_compdb.py -O $(KDIR) $(MDIR) +clean: + make -C $(KDIR) M=$(MDIR) clean # Keep a new line here!! Because someone may append config diff --git a/kernel/allowlist.c b/kernel/allowlist.c index 9745567..c3f7a5b 100644 --- a/kernel/allowlist.c +++ b/kernel/allowlist.c @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -8,14 +10,16 @@ #include #include #include +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0) #include +#endif -#include "ksu.h" #include "klog.h" // IWYU pragma: keep +#include "ksud.h" #include "selinux/selinux.h" -#include "kernel_compat.h" #include "allowlist.h" #include "manager.h" +#include "syscall_hook_manager.h" #define FILE_MAGIC 0x7f4b5355 // ' KSU', u32 #define FILE_FORMAT_VERSION 3 // u32 @@ -29,58 +33,61 @@ static DEFINE_MUTEX(allowlist_mutex); static struct root_profile default_root_profile; static struct non_root_profile default_non_root_profile; -static int allow_list_arr[PAGE_SIZE / sizeof(int)] __read_mostly __aligned(PAGE_SIZE); +void persistent_allow_list(void); + +static int allow_list_arr[PAGE_SIZE / sizeof(int)] __read_mostly + __aligned(PAGE_SIZE); static int allow_list_pointer __read_mostly = 0; static void remove_uid_from_arr(uid_t uid) { - int *temp_arr; - int i, j; + int *temp_arr; + int i, j; - if (allow_list_pointer == 0) - return; + if (allow_list_pointer == 0) + return; - temp_arr = kmalloc(sizeof(allow_list_arr), GFP_KERNEL); - if (temp_arr == NULL) { - pr_err("%s: unable to allocate memory\n", __func__); - return; - } + temp_arr = kzalloc(sizeof(allow_list_arr), GFP_KERNEL); + if (temp_arr == NULL) { + pr_err("%s: unable to allocate memory\n", __func__); + return; + } - for (i = j = 0; i < allow_list_pointer; i++) { - if (allow_list_arr[i] == uid) - continue; - temp_arr[j++] = allow_list_arr[i]; - } + for (i = j = 0; i < allow_list_pointer; i++) { + if (allow_list_arr[i] == uid) + continue; + temp_arr[j++] = allow_list_arr[i]; + } - allow_list_pointer = j; + allow_list_pointer = j; - for (; j < ARRAY_SIZE(allow_list_arr); j++) - temp_arr[j] = -1; + for (; j < ARRAY_SIZE(allow_list_arr); j++) + temp_arr[j] = -1; - memcpy(&allow_list_arr, temp_arr, PAGE_SIZE); - kfree(temp_arr); + memcpy(&allow_list_arr, temp_arr, PAGE_SIZE); + kfree(temp_arr); } -static void init_default_profiles() +static void init_default_profiles(void) { - kernel_cap_t full_cap = CAP_FULL_SET; + kernel_cap_t full_cap = CAP_FULL_SET; - default_root_profile.uid = 0; - default_root_profile.gid = 0; - default_root_profile.groups_count = 1; - default_root_profile.groups[0] = 0; - memcpy(&default_root_profile.capabilities.effective, &full_cap, - sizeof(default_root_profile.capabilities.effective)); - default_root_profile.namespaces = 0; - strcpy(default_root_profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN); + default_root_profile.uid = 0; + default_root_profile.gid = 0; + default_root_profile.groups_count = 1; + default_root_profile.groups[0] = 0; + memcpy(&default_root_profile.capabilities.effective, &full_cap, + sizeof(default_root_profile.capabilities.effective)); + default_root_profile.namespaces = 0; + strcpy(default_root_profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN); - // This means that we will umount modules by default! - default_non_root_profile.umount_modules = true; + // This means that we will umount modules by default! + default_non_root_profile.umount_modules = true; } struct perm_data { - struct list_head list; - struct app_profile profile; + struct list_head list; + struct app_profile profile; }; static struct list_head allow_list; @@ -90,437 +97,535 @@ static uint8_t allow_list_bitmap[PAGE_SIZE] __read_mostly __aligned(PAGE_SIZE); #define KERNEL_SU_ALLOWLIST "/data/adb/ksu/.allowlist" -static struct work_struct ksu_save_work; -static struct work_struct ksu_load_work; - -bool persistent_allow_list(void); - void ksu_show_allow_list(void) { - struct perm_data *p = NULL; - struct list_head *pos = NULL; - pr_info("ksu_show_allow_list\n"); - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - pr_info("uid :%d, allow: %d\n", p->profile.current_uid, - p->profile.allow_su); - } + struct perm_data *p = NULL; + struct list_head *pos = NULL; + pr_info("ksu_show_allow_list\n"); + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + pr_info("uid :%d, allow: %d\n", p->profile.current_uid, + p->profile.allow_su); + } } #ifdef CONFIG_KSU_DEBUG -static void ksu_grant_root_to_shell() +static void ksu_grant_root_to_shell(void) { - struct app_profile profile = { - .version = KSU_APP_PROFILE_VER, - .allow_su = true, - .current_uid = 2000, - }; - strcpy(profile.key, "com.android.shell"); - strcpy(profile.rp_config.profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN); - ksu_set_app_profile(&profile, false); + struct app_profile profile = { + .version = KSU_APP_PROFILE_VER, + .allow_su = true, + .current_uid = 2000, + }; + strcpy(profile.key, "com.android.shell"); + strcpy(profile.rp_config.profile.selinux_domain, + KSU_DEFAULT_SELINUX_DOMAIN); + ksu_set_app_profile(&profile, false); } #endif bool ksu_get_app_profile(struct app_profile *profile) { - struct perm_data *p = NULL; - struct list_head *pos = NULL; - bool found = false; + struct perm_data *p = NULL; + struct list_head *pos = NULL; + bool found = false; - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - bool uid_match = profile->current_uid == p->profile.current_uid; - if (uid_match) { - // found it, override it with ours - memcpy(profile, &p->profile, sizeof(*profile)); - found = true; - goto exit; - } - } + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + bool uid_match = profile->current_uid == p->profile.current_uid; + if (uid_match) { + // found it, override it with ours + memcpy(profile, &p->profile, sizeof(*profile)); + found = true; + goto exit; + } + } exit: - return found; + return found; } -static inline bool forbid_system_uid(uid_t uid) { - #define SHELL_UID 2000 - #define SYSTEM_UID 1000 - return uid < SHELL_UID && uid != SYSTEM_UID; +static inline bool forbid_system_uid(uid_t uid) +{ +#define SHELL_UID 2000 +#define SYSTEM_UID 1000 + return uid < SHELL_UID && uid != SYSTEM_UID; } static bool profile_valid(struct app_profile *profile) { - if (!profile) { - return false; - } + if (!profile) { + return false; + } - if (profile->version < KSU_APP_PROFILE_VER) { - pr_info("Unsupported profile version: %d\n", profile->version); - return false; - } + if (profile->version < KSU_APP_PROFILE_VER) { + pr_info("Unsupported profile version: %d\n", profile->version); + return false; + } - if (profile->allow_su) { - if (profile->rp_config.profile.groups_count > KSU_MAX_GROUPS) { - return false; - } + if (profile->allow_su) { + if (profile->rp_config.profile.groups_count > KSU_MAX_GROUPS) { + return false; + } - if (strlen(profile->rp_config.profile.selinux_domain) == 0) { - return false; - } - } + if (strlen(profile->rp_config.profile.selinux_domain) == 0) { + return false; + } + } - return true; + return true; } bool ksu_set_app_profile(struct app_profile *profile, bool persist) { - struct perm_data *p = NULL; - struct list_head *pos = NULL; - bool result = false; + struct perm_data *p = NULL; + struct list_head *pos = NULL; + bool result = false; - if (!profile_valid(profile)) { - pr_err("Failed to set app profile: invalid profile!\n"); - return false; - } + if (!profile_valid(profile)) { + pr_err("Failed to set app profile: invalid profile!\n"); + return false; + } - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - // both uid and package must match, otherwise it will break multiple package with different user id - if (profile->current_uid == p->profile.current_uid && - !strcmp(profile->key, p->profile.key)) { - // found it, just override it all! - memcpy(&p->profile, profile, sizeof(*profile)); - result = true; - goto out; - } - } + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + // both uid and package must match, otherwise it will break multiple package with different user id + if (profile->current_uid == p->profile.current_uid && + !strcmp(profile->key, p->profile.key)) { + // found it, just override it all! + memcpy(&p->profile, profile, sizeof(*profile)); + result = true; + goto out; + } + } - // not found, alloc a new node! - p = (struct perm_data *)kmalloc(sizeof(struct perm_data), GFP_KERNEL); - if (!p) { - pr_err("ksu_set_app_profile alloc failed\n"); - return false; - } + // not found, alloc a new node! + p = (struct perm_data *)kzalloc(sizeof(struct perm_data), GFP_KERNEL); + if (!p) { + pr_err("ksu_set_app_profile alloc failed\n"); + return false; + } - memcpy(&p->profile, profile, sizeof(*profile)); - if (profile->allow_su) { - pr_info("set root profile, key: %s, uid: %d, gid: %d, context: %s\n", - profile->key, profile->current_uid, - profile->rp_config.profile.gid, - profile->rp_config.profile.selinux_domain); - } else { - pr_info("set app profile, key: %s, uid: %d, umount modules: %d\n", - profile->key, profile->current_uid, - profile->nrp_config.profile.umount_modules); - } - list_add_tail(&p->list, &allow_list); + memcpy(&p->profile, profile, sizeof(*profile)); + if (profile->allow_su) { + pr_info("set root profile, key: %s, uid: %d, gid: %d, context: %s\n", + profile->key, profile->current_uid, + profile->rp_config.profile.gid, + profile->rp_config.profile.selinux_domain); + } else { + pr_info("set app profile, key: %s, uid: %d, umount modules: %d\n", + profile->key, profile->current_uid, + profile->nrp_config.profile.umount_modules); + } + list_add_tail(&p->list, &allow_list); out: - if (profile->current_uid <= BITMAP_UID_MAX) { - if (profile->allow_su) - allow_list_bitmap[profile->current_uid / BITS_PER_BYTE] |= 1 << (profile->current_uid % BITS_PER_BYTE); - else - allow_list_bitmap[profile->current_uid / BITS_PER_BYTE] &= ~(1 << (profile->current_uid % BITS_PER_BYTE)); - } else { - if (profile->allow_su) { - /* - * 1024 apps with uid higher than BITMAP_UID_MAX - * registered to request superuser? - */ - if (allow_list_pointer >= ARRAY_SIZE(allow_list_arr)) { - pr_err("too many apps registered\n"); - WARN_ON(1); - return false; - } - allow_list_arr[allow_list_pointer++] = profile->current_uid; - } else { - remove_uid_from_arr(profile->current_uid); - } - } - result = true; + if (profile->current_uid <= BITMAP_UID_MAX) { + if (profile->allow_su) + allow_list_bitmap[profile->current_uid / BITS_PER_BYTE] |= + 1 << (profile->current_uid % BITS_PER_BYTE); + else + allow_list_bitmap[profile->current_uid / BITS_PER_BYTE] &= + ~(1 << (profile->current_uid % BITS_PER_BYTE)); + } else { + if (profile->allow_su) { + /* + * 1024 apps with uid higher than BITMAP_UID_MAX + * registered to request superuser? + */ + if (allow_list_pointer >= ARRAY_SIZE(allow_list_arr)) { + pr_err("too many apps registered\n"); + WARN_ON(1); + return false; + } + allow_list_arr[allow_list_pointer++] = profile->current_uid; + } else { + remove_uid_from_arr(profile->current_uid); + } + } + result = true; - // check if the default profiles is changed, cache it to a single struct to accelerate access. - if (unlikely(!strcmp(profile->key, "$"))) { - // set default non root profile - memcpy(&default_non_root_profile, &profile->nrp_config.profile, - sizeof(default_non_root_profile)); - } + // check if the default profiles is changed, cache it to a single struct to accelerate access. + if (unlikely(!strcmp(profile->key, "$"))) { + // set default non root profile + memcpy(&default_non_root_profile, &profile->nrp_config.profile, + sizeof(default_non_root_profile)); + } - if (unlikely(!strcmp(profile->key, "#"))) { - // set default root profile - memcpy(&default_root_profile, &profile->rp_config.profile, - sizeof(default_root_profile)); - } + if (unlikely(!strcmp(profile->key, "#"))) { + // set default root profile + memcpy(&default_root_profile, &profile->rp_config.profile, + sizeof(default_root_profile)); + } - if (persist) - persistent_allow_list(); + if (persist) { + persistent_allow_list(); + // FIXME: use a new flag + ksu_mark_running_process(); + } - return result; + return result; } bool __ksu_is_allow_uid(uid_t uid) { - int i; + int i; - if (unlikely(uid == 0)) { - // already root, but only allow our domain. - return is_ksu_domain(); - } + if (forbid_system_uid(uid)) { + // do not bother going through the list if it's system + return false; + } - if (forbid_system_uid(uid)) { - // do not bother going through the list if it's system - return false; - } + if (likely(ksu_is_manager_uid_valid()) && + unlikely(ksu_get_manager_uid() == uid)) { + // manager is always allowed! + return true; + } - if (likely(ksu_is_manager_uid_valid()) && unlikely(ksu_get_manager_uid() == uid)) { - // manager is always allowed! - return true; - } + if (likely(uid <= BITMAP_UID_MAX)) { + return !!(allow_list_bitmap[uid / BITS_PER_BYTE] & + (1 << (uid % BITS_PER_BYTE))); + } else { + for (i = 0; i < allow_list_pointer; i++) { + if (allow_list_arr[i] == uid) + return true; + } + } - if (likely(uid <= BITMAP_UID_MAX)) { - return !!(allow_list_bitmap[uid / BITS_PER_BYTE] & (1 << (uid % BITS_PER_BYTE))); - } else { - for (i = 0; i < allow_list_pointer; i++) { - if (allow_list_arr[i] == uid) - return true; - } - } + return false; +} - return false; +bool __ksu_is_allow_uid_for_current(uid_t uid) +{ + if (unlikely(uid == 0)) { + // already root, but only allow our domain. + return is_ksu_domain(); + } + return __ksu_is_allow_uid(uid); } bool ksu_uid_should_umount(uid_t uid) { - struct app_profile profile = { .current_uid = uid }; - if (likely(ksu_is_manager_uid_valid()) && unlikely(ksu_get_manager_uid() == uid)) { - // we should not umount on manager! - return false; - } - bool found = ksu_get_app_profile(&profile); - if (!found) { - // no app profile found, it must be non root app - return default_non_root_profile.umount_modules; - } - if (profile.allow_su) { - // if found and it is granted to su, we shouldn't umount for it - return false; - } else { - // found an app profile - if (profile.nrp_config.use_default) { - return default_non_root_profile.umount_modules; - } else { - return profile.nrp_config.profile.umount_modules; - } - } + struct app_profile profile = { .current_uid = uid }; + if (likely(ksu_is_manager_uid_valid()) && + unlikely(ksu_get_manager_uid() == uid)) { + // we should not umount on manager! + return false; + } + bool found = ksu_get_app_profile(&profile); + if (!found) { + // no app profile found, it must be non root app + return default_non_root_profile.umount_modules; + } + if (profile.allow_su) { + // if found and it is granted to su, we shouldn't umount for it + return false; + } else { + // found an app profile + if (profile.nrp_config.use_default) { + return default_non_root_profile.umount_modules; + } else { + return profile.nrp_config.profile.umount_modules; + } + } } struct root_profile *ksu_get_root_profile(uid_t uid) { - struct perm_data *p = NULL; - struct list_head *pos = NULL; + struct perm_data *p = NULL; + struct list_head *pos = NULL; - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - if (uid == p->profile.current_uid && p->profile.allow_su) { - if (!p->profile.rp_config.use_default) { - return &p->profile.rp_config.profile; - } - } - } + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + if (uid == p->profile.current_uid && p->profile.allow_su) { + if (!p->profile.rp_config.use_default) { + return &p->profile.rp_config.profile; + } + } + } - // use default profile - return &default_root_profile; + // use default profile + return &default_root_profile; } bool ksu_get_allow_list(int *array, int *length, bool allow) { - struct perm_data *p = NULL; - struct list_head *pos = NULL; - int i = 0; - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - // pr_info("get_allow_list uid: %d allow: %d\n", p->uid, p->allow); - if (p->profile.allow_su == allow) { - array[i++] = p->profile.current_uid; - } - } - *length = i; + struct perm_data *p = NULL; + struct list_head *pos = NULL; + int i = 0; + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + // pr_info("get_allow_list uid: %d allow: %d\n", p->uid, p->allow); + if (p->profile.allow_su == allow) { + array[i++] = p->profile.current_uid; + } + } + *length = i; - return true; + return true; } -void do_save_allow_list(struct work_struct *work) +static void do_persistent_allow_list(struct callback_head *_cb) { - u32 magic = FILE_MAGIC; - u32 version = FILE_FORMAT_VERSION; - struct perm_data *p = NULL; - struct list_head *pos = NULL; - loff_t off = 0; + u32 magic = FILE_MAGIC; + u32 version = FILE_FORMAT_VERSION; + struct perm_data *p = NULL; + struct list_head *pos = NULL; + loff_t off = 0; - struct file *fp = - ksu_filp_open_compat(KERNEL_SU_ALLOWLIST, O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (IS_ERR(fp)) { - pr_err("save_allow_list create file failed: %ld\n", PTR_ERR(fp)); - return; - } + mutex_lock(&allowlist_mutex); + struct file *fp = + filp_open(KERNEL_SU_ALLOWLIST, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (IS_ERR(fp)) { + pr_err("save_allow_list create file failed: %ld\n", PTR_ERR(fp)); + goto unlock; + } - // store magic and version - if (ksu_kernel_write_compat(fp, &magic, sizeof(magic), &off) != - sizeof(magic)) { - pr_err("save_allow_list write magic failed.\n"); - goto exit; - } + // store magic and version + if (kernel_write(fp, &magic, sizeof(magic), &off) != sizeof(magic)) { + pr_err("save_allow_list write magic failed.\n"); + goto close_file; + } - if (ksu_kernel_write_compat(fp, &version, sizeof(version), &off) != - sizeof(version)) { - pr_err("save_allow_list write version failed.\n"); - goto exit; - } + if (kernel_write(fp, &version, sizeof(version), &off) != sizeof(version)) { + pr_err("save_allow_list write version failed.\n"); + goto close_file; + } - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - pr_info("save allow list, name: %s uid :%d, allow: %d\n", - p->profile.key, p->profile.current_uid, - p->profile.allow_su); + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + pr_info("save allow list, name: %s uid :%d, allow: %d\n", + p->profile.key, p->profile.current_uid, p->profile.allow_su); - ksu_kernel_write_compat(fp, &p->profile, sizeof(p->profile), - &off); - } + kernel_write(fp, &p->profile, sizeof(p->profile), &off); + } -exit: - filp_close(fp, 0); +close_file: + filp_close(fp, 0); +unlock: + mutex_unlock(&allowlist_mutex); + kfree(_cb); } -void do_load_allow_list(struct work_struct *work) +void persistent_allow_list() { - loff_t off = 0; - ssize_t ret = 0; - struct file *fp = NULL; - u32 magic; - u32 version; + struct task_struct *tsk; + + tsk = get_pid_task(find_vpid(1), PIDTYPE_PID); + if (!tsk) { + pr_err("save_allow_list find init task err\n"); + return; + } + + struct callback_head *cb = + kzalloc(sizeof(struct callback_head), GFP_KERNEL); + if (!cb) { + pr_err("save_allow_list alloc cb err\b"); + goto put_task; + } + cb->func = do_persistent_allow_list; + task_work_add(tsk, cb, TWA_RESUME); + +put_task: + put_task_struct(tsk); +} + +void ksu_load_allow_list() +{ + loff_t off = 0; + ssize_t ret = 0; + struct file *fp = NULL; + u32 magic; + u32 version; #ifdef CONFIG_KSU_DEBUG - // always allow adb shell by default - ksu_grant_root_to_shell(); + // always allow adb shell by default + ksu_grant_root_to_shell(); #endif - // load allowlist now! - fp = ksu_filp_open_compat(KERNEL_SU_ALLOWLIST, O_RDONLY, 0); - if (IS_ERR(fp)) { - pr_err("load_allow_list open file failed: %ld\n", PTR_ERR(fp)); - return; - } + // load allowlist now! + fp = filp_open(KERNEL_SU_ALLOWLIST, O_RDONLY, 0); + if (IS_ERR(fp)) { + pr_err("load_allow_list open file failed: %ld\n", PTR_ERR(fp)); + return; + } - // verify magic - if (ksu_kernel_read_compat(fp, &magic, sizeof(magic), &off) != - sizeof(magic) || - magic != FILE_MAGIC) { - pr_err("allowlist file invalid: %d!\n", magic); - goto exit; - } + // verify magic + if (kernel_read(fp, &magic, sizeof(magic), &off) != sizeof(magic) || + magic != FILE_MAGIC) { + pr_err("allowlist file invalid: %d!\n", magic); + goto exit; + } - if (ksu_kernel_read_compat(fp, &version, sizeof(version), &off) != - sizeof(version)) { - pr_err("allowlist read version: %d failed\n", version); - goto exit; - } + if (kernel_read(fp, &version, sizeof(version), &off) != sizeof(version)) { + pr_err("allowlist read version: %d failed\n", version); + goto exit; + } - pr_info("allowlist version: %d\n", version); + pr_info("allowlist version: %d\n", version); - while (true) { - struct app_profile profile; + while (true) { + struct app_profile profile; - ret = ksu_kernel_read_compat(fp, &profile, sizeof(profile), - &off); + ret = kernel_read(fp, &profile, sizeof(profile), &off); - if (ret <= 0) { - pr_info("load_allow_list read err: %zd\n", ret); - break; - } + if (ret <= 0) { + pr_info("load_allow_list read err: %zd\n", ret); + break; + } - pr_info("load_allow_uid, name: %s, uid: %d, allow: %d\n", - profile.key, profile.current_uid, profile.allow_su); - ksu_set_app_profile(&profile, false); - } + pr_info("load_allow_uid, name: %s, uid: %d, allow: %d\n", profile.key, + profile.current_uid, profile.allow_su); + ksu_set_app_profile(&profile, false); + } exit: - ksu_show_allow_list(); - filp_close(fp, 0); + ksu_show_allow_list(); + filp_close(fp, 0); } -void ksu_prune_allowlist(bool (*is_uid_valid)(uid_t, char *, void *), void *data) +void ksu_prune_allowlist(bool (*is_uid_valid)(uid_t, char *, void *), + void *data) { - struct perm_data *np = NULL; - struct perm_data *n = NULL; + struct perm_data *np = NULL; + struct perm_data *n = NULL; - bool modified = false; - // TODO: use RCU! - mutex_lock(&allowlist_mutex); - list_for_each_entry_safe (np, n, &allow_list, list) { - uid_t uid = np->profile.current_uid; - char *package = np->profile.key; - // we use this uid for special cases, don't prune it! - bool is_preserved_uid = uid == KSU_APP_PROFILE_PRESERVE_UID; - if (!is_preserved_uid && !is_uid_valid(uid, package, data)) { - modified = true; - pr_info("prune uid: %d, package: %s\n", uid, package); - list_del(&np->list); - if (likely(uid <= BITMAP_UID_MAX)) { - allow_list_bitmap[uid / BITS_PER_BYTE] &= ~(1 << (uid % BITS_PER_BYTE)); - } - remove_uid_from_arr(uid); - smp_mb(); - kfree(np); - } - } - mutex_unlock(&allowlist_mutex); + if (!ksu_boot_completed) { + pr_info("boot not completed, skip prune\n"); + return; + } - if (modified) { - persistent_allow_list(); - } -} + bool modified = false; + // TODO: use RCU! + mutex_lock(&allowlist_mutex); + list_for_each_entry_safe (np, n, &allow_list, list) { + uid_t uid = np->profile.current_uid; + char *package = np->profile.key; + // we use this uid for special cases, don't prune it! + bool is_preserved_uid = uid == KSU_APP_PROFILE_PRESERVE_UID; + if (!is_preserved_uid && !is_uid_valid(uid, package, data)) { + modified = true; + pr_info("prune uid: %d, package: %s\n", uid, package); + list_del(&np->list); + if (likely(uid <= BITMAP_UID_MAX)) { + allow_list_bitmap[uid / BITS_PER_BYTE] &= + ~(1 << (uid % BITS_PER_BYTE)); + } + remove_uid_from_arr(uid); + smp_mb(); + kfree(np); + } + } + mutex_unlock(&allowlist_mutex); -// make sure allow list works cross boot -bool persistent_allow_list(void) -{ - return ksu_queue_work(&ksu_save_work); -} - -bool ksu_load_allow_list(void) -{ - return ksu_queue_work(&ksu_load_work); + if (modified) { + persistent_allow_list(); + } } void ksu_allowlist_init(void) { - int i; + int i; - BUILD_BUG_ON(sizeof(allow_list_bitmap) != PAGE_SIZE); - BUILD_BUG_ON(sizeof(allow_list_arr) != PAGE_SIZE); + BUILD_BUG_ON(sizeof(allow_list_bitmap) != PAGE_SIZE); + BUILD_BUG_ON(sizeof(allow_list_arr) != PAGE_SIZE); - for (i = 0; i < ARRAY_SIZE(allow_list_arr); i++) - allow_list_arr[i] = -1; + for (i = 0; i < ARRAY_SIZE(allow_list_arr); i++) + allow_list_arr[i] = -1; - INIT_LIST_HEAD(&allow_list); + INIT_LIST_HEAD(&allow_list); - INIT_WORK(&ksu_save_work, do_save_allow_list); - INIT_WORK(&ksu_load_work, do_load_allow_list); - - init_default_profiles(); + init_default_profiles(); } void ksu_allowlist_exit(void) { - struct perm_data *np = NULL; - struct perm_data *n = NULL; + struct perm_data *np = NULL; + struct perm_data *n = NULL; - do_save_allow_list(NULL); - - // free allowlist - mutex_lock(&allowlist_mutex); - list_for_each_entry_safe (np, n, &allow_list, list) { - list_del(&np->list); - kfree(np); - } - mutex_unlock(&allowlist_mutex); + // free allowlist + mutex_lock(&allowlist_mutex); + list_for_each_entry_safe (np, n, &allow_list, list) { + list_del(&np->list); + kfree(np); + } + mutex_unlock(&allowlist_mutex); } + +#ifdef CONFIG_KSU_MANUAL_SU +bool ksu_temp_grant_root_once(uid_t uid) +{ + struct app_profile profile = { + .version = KSU_APP_PROFILE_VER, + .allow_su = true, + .current_uid = uid, + }; + + const char *default_key = "com.temp.once"; + + struct perm_data *p = NULL; + struct list_head *pos = NULL; + bool found = false; + + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + if (p->profile.current_uid == uid) { + strcpy(profile.key, p->profile.key); + found = true; + break; + } + } + + if (!found) { + strcpy(profile.key, default_key); + } + + profile.rp_config.profile.uid = default_root_profile.uid; + profile.rp_config.profile.gid = default_root_profile.gid; + profile.rp_config.profile.groups_count = default_root_profile.groups_count; + memcpy(profile.rp_config.profile.groups, default_root_profile.groups, sizeof(default_root_profile.groups)); + memcpy(&profile.rp_config.profile.capabilities, &default_root_profile.capabilities, sizeof(default_root_profile.capabilities)); + profile.rp_config.profile.namespaces = default_root_profile.namespaces; + strcpy(profile.rp_config.profile.selinux_domain, default_root_profile.selinux_domain); + + bool ok = ksu_set_app_profile(&profile, false); + if (ok) + pr_info("pending_root: UID=%d granted and persisted\n", uid); + return ok; +} + +void ksu_temp_revoke_root_once(uid_t uid) +{ + struct app_profile profile = { + .version = KSU_APP_PROFILE_VER, + .allow_su = false, + .current_uid = uid, + }; + + const char *default_key = "com.temp.once"; + + struct perm_data *p = NULL; + struct list_head *pos = NULL; + bool found = false; + + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + if (p->profile.current_uid == uid) { + strcpy(profile.key, p->profile.key); + found = true; + break; + } + } + + if (!found) { + strcpy(profile.key, default_key); + } + + profile.nrp_config.profile.umount_modules = default_non_root_profile.umount_modules; + strcpy(profile.rp_config.profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN); + + ksu_set_app_profile(&profile, false); + persistent_allow_list(); + pr_info("pending_root: UID=%d removed and persist updated\n", uid); +} +#endif \ No newline at end of file diff --git a/kernel/allowlist.h b/kernel/allowlist.h index e89bf71..4bac8c3 100644 --- a/kernel/allowlist.h +++ b/kernel/allowlist.h @@ -2,19 +2,29 @@ #define __KSU_H_ALLOWLIST #include -#include "ksu.h" +#include +#include "app_profile.h" + +#define PER_USER_RANGE 100000 +#define FIRST_APPLICATION_UID 10000 +#define LAST_APPLICATION_UID 19999 void ksu_allowlist_init(void); void ksu_allowlist_exit(void); -bool ksu_load_allow_list(void); +void ksu_load_allow_list(void); void ksu_show_allow_list(void); +// Check if the uid is in allow list bool __ksu_is_allow_uid(uid_t uid); #define ksu_is_allow_uid(uid) unlikely(__ksu_is_allow_uid(uid)) +// Check if the uid is in allow list, or current is ksu domain root +bool __ksu_is_allow_uid_for_current(uid_t uid); +#define ksu_is_allow_uid_for_current(uid) unlikely(__ksu_is_allow_uid_for_current(uid)) + bool ksu_get_allow_list(int *array, int *length, bool allow); void ksu_prune_allowlist(bool (*is_uid_exist)(uid_t, char *, void *), void *data); @@ -24,4 +34,16 @@ bool ksu_set_app_profile(struct app_profile *, bool persist); bool ksu_uid_should_umount(uid_t uid); struct root_profile *ksu_get_root_profile(uid_t uid); + +static inline bool is_appuid(uid_t uid) +{ + uid_t appid = uid % PER_USER_RANGE; + return appid >= FIRST_APPLICATION_UID && appid <= LAST_APPLICATION_UID; +} + +#ifdef CONFIG_KSU_MANUAL_SU +bool ksu_temp_grant_root_once(uid_t uid); +void ksu_temp_revoke_root_once(uid_t uid); +#endif + #endif diff --git a/kernel/apk_sign.c b/kernel/apk_sign.c index 3c22ca0..271e802 100644 --- a/kernel/apk_sign.c +++ b/kernel/apk_sign.c @@ -17,69 +17,65 @@ #include "apk_sign.h" #include "dynamic_manager.h" #include "klog.h" // IWYU pragma: keep -#include "kernel_compat.h" #include "manager_sign.h" struct sdesc { - struct shash_desc shash; - char ctx[]; + struct shash_desc shash; + char ctx[]; }; -static struct apk_sign_key { - unsigned size; - const char *sha256; -} apk_sign_keys[] = { - {EXPECTED_SIZE_SHIRKNEKO, EXPECTED_HASH_SHIRKNEKO}, // ShirkNeko/SukiSU +static apk_sign_key_t apk_sign_keys[] = { + {EXPECTED_SIZE_SHIRKNEKO, EXPECTED_HASH_SHIRKNEKO}, // ShirkNeko/SukiSU #ifdef EXPECTED_SIZE - {EXPECTED_SIZE, EXPECTED_HASH}, // Custom + {EXPECTED_SIZE, EXPECTED_HASH}, // Custom #endif }; static struct sdesc *init_sdesc(struct crypto_shash *alg) { - struct sdesc *sdesc; - int size; + struct sdesc *sdesc; + int size; - size = sizeof(struct shash_desc) + crypto_shash_descsize(alg); - sdesc = kmalloc(size, GFP_KERNEL); - if (!sdesc) - return ERR_PTR(-ENOMEM); - sdesc->shash.tfm = alg; - return sdesc; + size = sizeof(struct shash_desc) + crypto_shash_descsize(alg); + sdesc = kzalloc(size, GFP_KERNEL); + if (!sdesc) + return ERR_PTR(-ENOMEM); + sdesc->shash.tfm = alg; + return sdesc; } static int calc_hash(struct crypto_shash *alg, const unsigned char *data, - unsigned int datalen, unsigned char *digest) + unsigned int datalen, unsigned char *digest) { - struct sdesc *sdesc; - int ret; + struct sdesc *sdesc; + int ret; - sdesc = init_sdesc(alg); - if (IS_ERR(sdesc)) { - pr_info("can't alloc sdesc\n"); - return PTR_ERR(sdesc); - } + sdesc = init_sdesc(alg); + if (IS_ERR(sdesc)) { + pr_info("can't alloc sdesc\n"); + return PTR_ERR(sdesc); + } - ret = crypto_shash_digest(&sdesc->shash, data, datalen, digest); - kfree(sdesc); - return ret; + ret = crypto_shash_digest(&sdesc->shash, data, datalen, digest); + kfree(sdesc); + return ret; } static int ksu_sha256(const unsigned char *data, unsigned int datalen, - unsigned char *digest) + unsigned char *digest) { - struct crypto_shash *alg; - char *hash_alg_name = "sha256"; - int ret; + struct crypto_shash *alg; + char *hash_alg_name = "sha256"; + int ret; - alg = crypto_alloc_shash(hash_alg_name, 0, 0); - if (IS_ERR(alg)) { - pr_info("can't alloc alg %s\n", hash_alg_name); - return PTR_ERR(alg); - } - ret = calc_hash(alg, data, datalen, digest); - crypto_free_shash(alg); - return ret; + alg = crypto_alloc_shash(hash_alg_name, 0, 0); + if (IS_ERR(alg)) { + pr_info("can't alloc alg %s\n", hash_alg_name); + return PTR_ERR(alg); + } + ret = calc_hash(alg, data, datalen, digest); + crypto_free_shash(alg); + return ret; } @@ -87,304 +83,307 @@ static struct dynamic_sign_key dynamic_sign = DYNAMIC_SIGN_DEFAULT_CONFIG; static bool check_dynamic_sign(struct file *fp, u32 size4, loff_t *pos, int *matched_index) { - struct dynamic_sign_key current_dynamic_key = dynamic_sign; - - if (ksu_get_dynamic_manager_config(¤t_dynamic_key.size, ¤t_dynamic_key.hash)) { - pr_debug("Using dynamic manager config: size=0x%x, hash=%.16s...\n", - current_dynamic_key.size, current_dynamic_key.hash); - } - - if (size4 != current_dynamic_key.size) { - return false; - } + struct dynamic_sign_key current_dynamic_key = dynamic_sign; + + if (ksu_get_dynamic_manager_config(¤t_dynamic_key.size, ¤t_dynamic_key.hash)) { + pr_debug("Using dynamic manager config: size=0x%x, hash=%.16s...\n", + current_dynamic_key.size, current_dynamic_key.hash); + } + + if (size4 != current_dynamic_key.size) { + return false; + } #define CERT_MAX_LENGTH 1024 - char cert[CERT_MAX_LENGTH]; - if (size4 > CERT_MAX_LENGTH) { - pr_info("cert length overlimit\n"); - return false; - } - - ksu_kernel_read_compat(fp, cert, size4, pos); - - unsigned char digest[SHA256_DIGEST_SIZE]; - if (ksu_sha256(cert, size4, digest) < 0) { - pr_info("sha256 error\n"); - return false; - } + char cert[CERT_MAX_LENGTH]; + if (size4 > CERT_MAX_LENGTH) { + pr_info("cert length overlimit\n"); + return false; + } + + kernel_read(fp, cert, size4, pos); + + unsigned char digest[SHA256_DIGEST_SIZE]; + if (ksu_sha256(cert, size4, digest) < 0) { + pr_info("sha256 error\n"); + return false; + } - char hash_str[SHA256_DIGEST_SIZE * 2 + 1]; - hash_str[SHA256_DIGEST_SIZE * 2] = '\0'; - bin2hex(hash_str, digest, SHA256_DIGEST_SIZE); - - pr_info("sha256: %s, expected: %s, index: dynamic\n", hash_str, current_dynamic_key.hash); - - if (strcmp(current_dynamic_key.hash, hash_str) == 0) { - if (matched_index) { - *matched_index = DYNAMIC_SIGN_INDEX; - } - return true; - } - - return false; + char hash_str[SHA256_DIGEST_SIZE * 2 + 1]; + hash_str[SHA256_DIGEST_SIZE * 2] = '\0'; + bin2hex(hash_str, digest, SHA256_DIGEST_SIZE); + + pr_info("sha256: %s, expected: %s, index: dynamic\n", hash_str, current_dynamic_key.hash); + + if (strcmp(current_dynamic_key.hash, hash_str) == 0) { + if (matched_index) { + *matched_index = DYNAMIC_SIGN_INDEX; + } + return true; + } + + return false; } static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, int *matched_index) { - int i; - struct apk_sign_key sign_key; - bool signature_valid = false; + int i; + apk_sign_key_t sign_key; + bool signature_valid = false; - ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer-sequence length - ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer length - ksu_kernel_read_compat(fp, size4, 0x4, pos); // signed data length + kernel_read(fp, size4, 0x4, pos); // signer-sequence length + kernel_read(fp, size4, 0x4, pos); // signer length + kernel_read(fp, size4, 0x4, pos); // signed data length - *offset += 0x4 * 3; + *offset += 0x4 * 3; - ksu_kernel_read_compat(fp, size4, 0x4, pos); // digests-sequence length + kernel_read(fp, size4, 0x4, pos); // digests-sequence length - *pos += *size4; - *offset += 0x4 + *size4; + *pos += *size4; + *offset += 0x4 + *size4; - ksu_kernel_read_compat(fp, size4, 0x4, pos); // certificates length - ksu_kernel_read_compat(fp, size4, 0x4, pos); // certificate length - *offset += 0x4 * 2; + kernel_read(fp, size4, 0x4, pos); // certificates length + kernel_read(fp, size4, 0x4, pos); // certificate length + *offset += 0x4 * 2; - if (ksu_is_dynamic_manager_enabled()) { - loff_t temp_pos = *pos; - if (check_dynamic_sign(fp, *size4, &temp_pos, matched_index)) { - *pos = temp_pos; - *offset += *size4; - return true; - } - } + if (ksu_is_dynamic_manager_enabled()) { + loff_t temp_pos = *pos; + if (check_dynamic_sign(fp, *size4, &temp_pos, matched_index)) { + *pos = temp_pos; + *offset += *size4; + return true; + } + } - for (i = 0; i < ARRAY_SIZE(apk_sign_keys); i++) { - sign_key = apk_sign_keys[i]; + for (i = 0; i < ARRAY_SIZE(apk_sign_keys); i++) { + sign_key = apk_sign_keys[i]; - if (*size4 != sign_key.size) - continue; - *offset += *size4; + if (*size4 != sign_key.size) + continue; + *offset += *size4; #define CERT_MAX_LENGTH 1024 - char cert[CERT_MAX_LENGTH]; - if (*size4 > CERT_MAX_LENGTH) { - pr_info("cert length overlimit\n"); - return false; - } - ksu_kernel_read_compat(fp, cert, *size4, pos); - unsigned char digest[SHA256_DIGEST_SIZE]; - if (IS_ERR(ksu_sha256(cert, *size4, digest))) { - pr_info("sha256 error\n"); - return false; - } + char cert[CERT_MAX_LENGTH]; + if (*size4 > CERT_MAX_LENGTH) { + pr_info("cert length overlimit\n"); + return false; + } + kernel_read(fp, cert, *size4, pos); + unsigned char digest[SHA256_DIGEST_SIZE]; + if (ksu_sha256(cert, *size4, digest) < 0 ) { + pr_info("sha256 error\n"); + return false; + } - char hash_str[SHA256_DIGEST_SIZE * 2 + 1]; - hash_str[SHA256_DIGEST_SIZE * 2] = '\0'; + char hash_str[SHA256_DIGEST_SIZE * 2 + 1]; + hash_str[SHA256_DIGEST_SIZE * 2] = '\0'; - bin2hex(hash_str, digest, SHA256_DIGEST_SIZE); - pr_info("sha256: %s, expected: %s, index: %d\n", hash_str, sign_key.sha256, i); - - if (strcmp(sign_key.sha256, hash_str) == 0) { - signature_valid = true; - if (matched_index) { - *matched_index = i; - } - break; - } - } - return signature_valid; + bin2hex(hash_str, digest, SHA256_DIGEST_SIZE); + pr_info("sha256: %s, expected: %s, index: %d\n", hash_str, sign_key.sha256, i); + + if (strcmp(sign_key.sha256, hash_str) == 0) { + signature_valid = true; + if (matched_index) { + *matched_index = i; + } + break; + } + } + return signature_valid; } struct zip_entry_header { - uint32_t signature; - uint16_t version; - uint16_t flags; - uint16_t compression; - uint16_t mod_time; - uint16_t mod_date; - uint32_t crc32; - uint32_t compressed_size; - uint32_t uncompressed_size; - uint16_t file_name_length; - uint16_t extra_field_length; + uint32_t signature; + uint16_t version; + uint16_t flags; + uint16_t compression; + uint16_t mod_time; + uint16_t mod_date; + uint32_t crc32; + uint32_t compressed_size; + uint32_t uncompressed_size; + uint16_t file_name_length; + uint16_t extra_field_length; } __attribute__((packed)); // This is a necessary but not sufficient condition, but it is enough for us static bool has_v1_signature_file(struct file *fp) { - struct zip_entry_header header; - const char MANIFEST[] = "META-INF/MANIFEST.MF"; + struct zip_entry_header header; + const char MANIFEST[] = "META-INF/MANIFEST.MF"; - loff_t pos = 0; + loff_t pos = 0; - while (ksu_kernel_read_compat(fp, &header, - sizeof(struct zip_entry_header), &pos) == - sizeof(struct zip_entry_header)) { - if (header.signature != 0x04034b50) { - // ZIP magic: 'PK' - return false; - } - // Read the entry file name - if (header.file_name_length == sizeof(MANIFEST) - 1) { - char fileName[sizeof(MANIFEST)]; - ksu_kernel_read_compat(fp, fileName, - header.file_name_length, &pos); - fileName[header.file_name_length] = '\0'; + while (kernel_read(fp, &header, + sizeof(struct zip_entry_header), &pos) == + sizeof(struct zip_entry_header)) { + if (header.signature != 0x04034b50) { + // ZIP magic: 'PK' + return false; + } + // Read the entry file name + if (header.file_name_length == sizeof(MANIFEST) - 1) { + char fileName[sizeof(MANIFEST)]; + kernel_read(fp, fileName, + header.file_name_length, &pos); + fileName[header.file_name_length] = '\0'; - // Check if the entry matches META-INF/MANIFEST.MF - if (strncmp(MANIFEST, fileName, sizeof(MANIFEST) - 1) == 0) { - return true; - } - } else { - // Skip the entry file name - pos += header.file_name_length; - } + // Check if the entry matches META-INF/MANIFEST.MF + if (strncmp(MANIFEST, fileName, sizeof(MANIFEST) - 1) == + 0) { + return true; + } + } else { + // Skip the entry file name + pos += header.file_name_length; + } - // Skip to the next entry - pos += header.extra_field_length + header.compressed_size; - } + // Skip to the next entry + pos += header.extra_field_length + header.compressed_size; + } - return false; + return false; } static __always_inline bool check_v2_signature(char *path, bool check_multi_manager, int *signature_index) { - unsigned char buffer[0x11] = { 0 }; - u32 size4; - u64 size8, size_of_block; - loff_t pos; - bool v2_signing_valid = false; - int v2_signing_blocks = 0; - bool v3_signing_exist = false; - bool v3_1_signing_exist = false; - int matched_index = -1; - int i; - struct file *fp = ksu_filp_open_compat(path, O_RDONLY, 0); - if (IS_ERR(fp)) { - pr_err("open %s error.\n", path); - return false; - } + unsigned char buffer[0x11] = { 0 }; + u32 size4; + u64 size8, size_of_block; - // If you want to check for multi-manager APK signing, but dynamic managering is not enabled, skip - if (check_multi_manager && !ksu_is_dynamic_manager_enabled()) { - filp_close(fp, 0); - return 0; - } + loff_t pos; - // disable inotify for this file - fp->f_mode |= FMODE_NONOTIFY; + bool v2_signing_valid = false; + int v2_signing_blocks = 0; + bool v3_signing_exist = false; + bool v3_1_signing_exist = false; + int matched_index = -1; + int i; + struct file *fp = filp_open(path, O_RDONLY, 0); + if (IS_ERR(fp)) { + pr_err("open %s error.\n", path); + return false; + } - // https://en.wikipedia.org/wiki/Zip_(file_format)#End_of_central_directory_record_(EOCD) - for (i = 0;; ++i) { - unsigned short n; - pos = generic_file_llseek(fp, -i - 2, SEEK_END); - ksu_kernel_read_compat(fp, &n, 2, &pos); - if (n == i) { - pos -= 22; - ksu_kernel_read_compat(fp, &size4, 4, &pos); - if ((size4 ^ 0xcafebabeu) == 0xccfbf1eeu) { - break; - } - } - if (i == 0xffff) { - pr_info("error: cannot find eocd\n"); - goto clean; - } - } + // If you want to check for multi-manager APK signing, but dynamic managering is not enabled, skip + if (check_multi_manager && !ksu_is_dynamic_manager_enabled()) { + filp_close(fp, 0); + return 0; + } - pos += 12; - // offset - ksu_kernel_read_compat(fp, &size4, 0x4, &pos); - pos = size4 - 0x18; + // disable inotify for this file + fp->f_mode |= FMODE_NONOTIFY; - ksu_kernel_read_compat(fp, &size8, 0x8, &pos); - ksu_kernel_read_compat(fp, buffer, 0x10, &pos); - if (strcmp((char *)buffer, "APK Sig Block 42")) { - goto clean; - } + // https://en.wikipedia.org/wiki/Zip_(file_format)#End_of_central_directory_record_(EOCD) + for (i = 0;; ++i) { + unsigned short n; + pos = generic_file_llseek(fp, -i - 2, SEEK_END); + kernel_read(fp, &n, 2, &pos); + if (n == i) { + pos -= 22; + kernel_read(fp, &size4, 4, &pos); + if ((size4 ^ 0xcafebabeu) == 0xccfbf1eeu) { + break; + } + } + if (i == 0xffff) { + pr_info("error: cannot find eocd\n"); + goto clean; + } + } - pos = size4 - (size8 + 0x8); - ksu_kernel_read_compat(fp, &size_of_block, 0x8, &pos); - if (size_of_block != size8) { - goto clean; - } + pos += 12; + // offset + kernel_read(fp, &size4, 0x4, &pos); + pos = size4 - 0x18; - int loop_count = 0; - while (loop_count++ < 10) { - uint32_t id; - uint32_t offset; - ksu_kernel_read_compat(fp, &size8, 0x8, - &pos); // sequence length - if (size8 == size_of_block) { - break; - } - ksu_kernel_read_compat(fp, &id, 0x4, &pos); // id - offset = 4; - if (id == 0x7109871au) { - v2_signing_blocks++; - bool result = check_block(fp, &size4, &pos, &offset, &matched_index); - if (result) { - v2_signing_valid = true; - } - } else if (id == 0xf05368c0u) { - // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73 - v3_signing_exist = true; - } else if (id == 0x1b93ad61u) { - // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#74 - v3_1_signing_exist = true; - } else { + kernel_read(fp, &size8, 0x8, &pos); + kernel_read(fp, buffer, 0x10, &pos); + if (strcmp((char *)buffer, "APK Sig Block 42")) { + goto clean; + } + + pos = size4 - (size8 + 0x8); + kernel_read(fp, &size_of_block, 0x8, &pos); + if (size_of_block != size8) { + goto clean; + } + + int loop_count = 0; + while (loop_count++ < 10) { + uint32_t id; + uint32_t offset; + kernel_read(fp, &size8, 0x8, + &pos); // sequence length + if (size8 == size_of_block) { + break; + } + kernel_read(fp, &id, 0x4, &pos); // id + offset = 4; + if (id == 0x7109871au) { + v2_signing_blocks++; + bool result = check_block(fp, &size4, &pos, &offset, &matched_index); + if (result) { + v2_signing_valid = true; + } + } else if (id == 0xf05368c0u) { + // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73 + v3_signing_exist = true; + } else if (id == 0x1b93ad61u) { + // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#74 + v3_1_signing_exist = true; + } else { #ifdef CONFIG_KSU_DEBUG - pr_info("Unknown id: 0x%08x\n", id); + pr_info("Unknown id: 0x%08x\n", id); #endif - } - pos += (size8 - offset); - } + } + pos += (size8 - offset); + } - if (v2_signing_blocks != 1) { + if (v2_signing_blocks != 1) { #ifdef CONFIG_KSU_DEBUG - pr_err("Unexpected v2 signature count: %d\n", - v2_signing_blocks); + pr_err("Unexpected v2 signature count: %d\n", + v2_signing_blocks); #endif - v2_signing_valid = false; - } + v2_signing_valid = false; + } - if (v2_signing_valid) { - int has_v1_signing = has_v1_signature_file(fp); - if (has_v1_signing) { - pr_err("Unexpected v1 signature scheme found!\n"); - filp_close(fp, 0); - return false; - } - } + if (v2_signing_valid) { + int has_v1_signing = has_v1_signature_file(fp); + if (has_v1_signing) { + pr_err("Unexpected v1 signature scheme found!\n"); + filp_close(fp, 0); + return false; + } + } clean: - filp_close(fp, 0); + filp_close(fp, 0); - if (v3_signing_exist || v3_1_signing_exist) { + if (v3_signing_exist || v3_1_signing_exist) { #ifdef CONFIG_KSU_DEBUG - pr_err("Unexpected v3 signature scheme found!\n"); + pr_err("Unexpected v3 signature scheme found!\n"); #endif - return false; - } + return false; + } - if (v2_signing_valid) { - if (signature_index) { - *signature_index = matched_index; - } - - if (check_multi_manager) { - // 0: ShirkNeko/SukiSU, DYNAMIC_SIGN_INDEX : Dynamic Sign - if (matched_index == 0 || matched_index == DYNAMIC_SIGN_INDEX) { - pr_info("Multi-manager APK detected (dynamic_manager enabled): signature_index=%d\n", matched_index); - return true; - } - return false; - } else { - // Common manager check: any valid signature will do - return true; - } - } - return false; + if (v2_signing_valid) { + if (signature_index) { + *signature_index = matched_index; + } + + if (check_multi_manager) { + // 0: ShirkNeko/SukiSU, DYNAMIC_SIGN_INDEX : Dynamic Sign + if (matched_index == 0 || matched_index == DYNAMIC_SIGN_INDEX) { + pr_info("Multi-manager APK detected (dynamic_manager enabled): signature_index=%d\n", matched_index); + return true; + } + return false; + } else { + // Common manager check: any valid signature will do + return true; + } + } + return false; } #ifdef CONFIG_KSU_DEBUG @@ -395,19 +394,19 @@ int ksu_debug_manager_uid = -1; static int set_expected_size(const char *val, const struct kernel_param *kp) { - int rv = param_set_uint(val, kp); - ksu_set_manager_uid(ksu_debug_manager_uid); - pr_info("ksu_manager_uid set to %d\n", ksu_debug_manager_uid); - return rv; + int rv = param_set_uint(val, kp); + ksu_set_manager_uid(ksu_debug_manager_uid); + pr_info("ksu_manager_uid set to %d\n", ksu_debug_manager_uid); + return rv; } static struct kernel_param_ops expected_size_ops = { - .set = set_expected_size, - .get = param_get_uint, + .set = set_expected_size, + .get = param_get_uint, }; module_param_cb(ksu_debug_manager_uid, &expected_size_ops, - &ksu_debug_manager_uid, S_IRUSR | S_IWUSR); + &ksu_debug_manager_uid, S_IRUSR | S_IWUSR); #endif diff --git a/kernel/app_profile.c b/kernel/app_profile.c new file mode 100644 index 0000000..00cd9c5 --- /dev/null +++ b/kernel/app_profile.c @@ -0,0 +1,303 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "objsec.h" + +#include "allowlist.h" +#include "app_profile.h" +#include "klog.h" // IWYU pragma: keep +#include "selinux/selinux.h" +#include "syscall_hook_manager.h" +#include "sucompat.h" + +#include "sulog.h" + +#if LINUX_VERSION_CODE >= KERNEL_VERSION (6, 7, 0) + static struct group_info root_groups = { .usage = REFCOUNT_INIT(2), }; +#else + static struct group_info root_groups = { .usage = ATOMIC_INIT(2) }; +#endif + +static void setup_groups(struct root_profile *profile, struct cred *cred) +{ + if (profile->groups_count > KSU_MAX_GROUPS) { + pr_warn("Failed to setgroups, too large group: %d!\n", + profile->uid); + return; + } + + if (profile->groups_count == 1 && profile->groups[0] == 0) { + // setgroup to root and return early. + if (cred->group_info) + put_group_info(cred->group_info); + cred->group_info = get_group_info(&root_groups); + return; + } + + u32 ngroups = profile->groups_count; + struct group_info *group_info = groups_alloc(ngroups); + if (!group_info) { + pr_warn("Failed to setgroups, ENOMEM for: %d\n", profile->uid); + return; + } + + int i; + for (i = 0; i < ngroups; i++) { + gid_t gid = profile->groups[i]; + kgid_t kgid = make_kgid(current_user_ns(), gid); + if (!gid_valid(kgid)) { + pr_warn("Failed to setgroups, invalid gid: %d\n", gid); + put_group_info(group_info); + return; + } + group_info->gid[i] = kgid; + } + + groups_sort(group_info); + set_groups(cred, group_info); + put_group_info(group_info); +} + +void disable_seccomp(void) +{ + assert_spin_locked(¤t->sighand->siglock); + // disable seccomp +#if defined(CONFIG_GENERIC_ENTRY) && \ + LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) + clear_syscall_work(SECCOMP); +#else + clear_thread_flag(TIF_SECCOMP); +#endif + +#ifdef CONFIG_SECCOMP + current->seccomp.mode = 0; + current->seccomp.filter = NULL; + atomic_set(¤t->seccomp.filter_count, 0); +#else +#endif +} + +void escape_with_root_profile(void) +{ + struct cred *cred; + struct task_struct *p = current; + struct task_struct *t; + + cred = prepare_creds(); + if (!cred) { + pr_warn("prepare_creds failed!\n"); + return; + } + + if (cred->euid.val == 0) { + pr_warn("Already root, don't escape!\n"); +#if __SULOG_GATE + ksu_sulog_report_su_grant(current_euid().val, NULL, "escape_to_root_failed"); +#endif + abort_creds(cred); + return; + } + + struct root_profile *profile = ksu_get_root_profile(cred->uid.val); + + cred->uid.val = profile->uid; + cred->suid.val = profile->uid; + cred->euid.val = profile->uid; + cred->fsuid.val = profile->uid; + + cred->gid.val = profile->gid; + cred->fsgid.val = profile->gid; + cred->sgid.val = profile->gid; + cred->egid.val = profile->gid; + cred->securebits = 0; + + BUILD_BUG_ON(sizeof(profile->capabilities.effective) != + sizeof(kernel_cap_t)); + + // setup capabilities + // we need CAP_DAC_READ_SEARCH becuase `/data/adb/ksud` is not accessible for non root process + // we add it here but don't add it to cap_inhertiable, it would be dropped automaticly after exec! + u64 cap_for_ksud = + profile->capabilities.effective | CAP_DAC_READ_SEARCH; + memcpy(&cred->cap_effective, &cap_for_ksud, + sizeof(cred->cap_effective)); + memcpy(&cred->cap_permitted, &profile->capabilities.effective, + sizeof(cred->cap_permitted)); + memcpy(&cred->cap_bset, &profile->capabilities.effective, + sizeof(cred->cap_bset)); + + setup_groups(profile, cred); + + commit_creds(cred); + + // Refer to kernel/seccomp.c: seccomp_set_mode_strict + // When disabling Seccomp, ensure that current->sighand->siglock is held during the operation. + spin_lock_irq(¤t->sighand->siglock); + disable_seccomp(); + spin_unlock_irq(¤t->sighand->siglock); + + setup_selinux(profile->selinux_domain); +#if __SULOG_GATE + ksu_sulog_report_su_grant(current_euid().val, NULL, "escape_to_root"); +#endif + + for_each_thread (p, t) { + ksu_set_task_tracepoint_flag(t); + } +} + +#ifdef CONFIG_KSU_MANUAL_SU + +#include "ksud.h" + +#ifndef DEVPTS_SUPER_MAGIC +#define DEVPTS_SUPER_MAGIC 0x1cd1 +#endif + +static int __manual_su_handle_devpts(struct inode *inode) +{ + if (!current->mm) { + return 0; + } + + uid_t uid = current_uid().val; + if (uid % 100000 < 10000) { + // not untrusted_app, ignore it + return 0; + } + + if (likely(!ksu_is_allow_uid_for_current(uid))) + return 0; + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 1, 0) || defined(KSU_OPTIONAL_SELINUX_INODE) + struct inode_security_struct *sec = selinux_inode(inode); +#else + struct inode_security_struct *sec = + (struct inode_security_struct *)inode->i_security; +#endif + if (ksu_file_sid && sec) + sec->sid = ksu_file_sid; + + return 0; +} + +static void disable_seccomp_for_task(struct task_struct *tsk) +{ + assert_spin_locked(&tsk->sighand->siglock); +#ifdef CONFIG_SECCOMP + if (tsk->seccomp.mode == SECCOMP_MODE_DISABLED && !tsk->seccomp.filter) + return; +#endif + clear_tsk_thread_flag(tsk, TIF_SECCOMP); +#ifdef CONFIG_SECCOMP + tsk->seccomp.mode = SECCOMP_MODE_DISABLED; + if (tsk->seccomp.filter) { +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0) + seccomp_filter_release(tsk); +#else + put_seccomp_filter(tsk); + tsk->seccomp.filter = NULL; +#endif + } +#endif +} + +void escape_to_root_for_cmd_su(uid_t target_uid, pid_t target_pid) +{ + struct cred *newcreds; + struct task_struct *target_task; + unsigned long flags; + struct task_struct *p = current; + struct task_struct *t; + + pr_info("cmd_su: escape_to_root_for_cmd_su called for UID: %d, PID: %d\n", target_uid, target_pid); + + // Find target task by PID + rcu_read_lock(); + target_task = pid_task(find_vpid(target_pid), PIDTYPE_PID); + if (!target_task) { + rcu_read_unlock(); + pr_err("cmd_su: target task not found for PID: %d\n", target_pid); +#if __SULOG_GATE + ksu_sulog_report_su_grant(target_uid, "cmd_su", "target_not_found"); +#endif + return; + } + get_task_struct(target_task); + rcu_read_unlock(); + + if (task_uid(target_task).val == 0) { + pr_warn("cmd_su: target task is already root, PID: %d\n", target_pid); + put_task_struct(target_task); + return; + } + + newcreds = prepare_kernel_cred(target_task); + if (newcreds == NULL) { + pr_err("cmd_su: failed to allocate new cred for PID: %d\n", target_pid); +#if __SULOG_GATE + ksu_sulog_report_su_grant(target_uid, "cmd_su", "cred_alloc_failed"); +#endif + put_task_struct(target_task); + return; + } + + struct root_profile *profile = ksu_get_root_profile(target_uid); + + newcreds->uid.val = profile->uid; + newcreds->suid.val = profile->uid; + newcreds->euid.val = profile->uid; + newcreds->fsuid.val = profile->uid; + + newcreds->gid.val = profile->gid; + newcreds->fsgid.val = profile->gid; + newcreds->sgid.val = profile->gid; + newcreds->egid.val = profile->gid; + newcreds->securebits = 0; + + u64 cap_for_cmd_su = profile->capabilities.effective | CAP_DAC_READ_SEARCH | CAP_SETUID | CAP_SETGID; + memcpy(&newcreds->cap_effective, &cap_for_cmd_su, sizeof(newcreds->cap_effective)); + memcpy(&newcreds->cap_permitted, &profile->capabilities.effective, sizeof(newcreds->cap_permitted)); + memcpy(&newcreds->cap_bset, &profile->capabilities.effective, sizeof(newcreds->cap_bset)); + + setup_groups(profile, newcreds); + task_lock(target_task); + + const struct cred *old_creds = get_task_cred(target_task); + + rcu_assign_pointer(target_task->real_cred, newcreds); + rcu_assign_pointer(target_task->cred, get_cred(newcreds)); + task_unlock(target_task); + + if (target_task->sighand) { + spin_lock_irqsave(&target_task->sighand->siglock, flags); + disable_seccomp_for_task(target_task); + spin_unlock_irqrestore(&target_task->sighand->siglock, flags); + } + + setup_selinux(profile->selinux_domain); + put_cred(old_creds); + wake_up_process(target_task); + + if (target_task->signal->tty) { + struct inode *inode = target_task->signal->tty->driver_data; + if (inode && inode->i_sb->s_magic == DEVPTS_SUPER_MAGIC) { + __manual_su_handle_devpts(inode); + } + } + + put_task_struct(target_task); +#if __SULOG_GATE + ksu_sulog_report_su_grant(target_uid, "cmd_su", "manual_escalation"); +#endif + for_each_thread (p, t) { + ksu_set_task_tracepoint_flag(t); + } + pr_info("cmd_su: privilege escalation completed for UID: %d, PID: %d\n", target_uid, target_pid); +} +#endif diff --git a/kernel/app_profile.h b/kernel/app_profile.h new file mode 100644 index 0000000..871abb6 --- /dev/null +++ b/kernel/app_profile.h @@ -0,0 +1,70 @@ +#ifndef __KSU_H_APP_PROFILE +#define __KSU_H_APP_PROFILE + +#include + +// Forward declarations +struct cred; + +#define KSU_APP_PROFILE_VER 2 +#define KSU_MAX_PACKAGE_NAME 256 +// NGROUPS_MAX for Linux is 65535 generally, but we only supports 32 groups. +#define KSU_MAX_GROUPS 32 +#define KSU_SELINUX_DOMAIN 64 + +struct root_profile { + int32_t uid; + int32_t gid; + + int32_t groups_count; + int32_t groups[KSU_MAX_GROUPS]; + + // kernel_cap_t is u32[2] for capabilities v3 + struct { + u64 effective; + u64 permitted; + u64 inheritable; + } capabilities; + + char selinux_domain[KSU_SELINUX_DOMAIN]; + + int32_t namespaces; +}; + +struct non_root_profile { + bool umount_modules; +}; + +struct app_profile { + // It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this. + u32 version; + + // this is usually the package of the app, but can be other value for special apps + char key[KSU_MAX_PACKAGE_NAME]; + int32_t current_uid; + bool allow_su; + + union { + struct { + bool use_default; + char template_name[KSU_MAX_PACKAGE_NAME]; + + struct root_profile profile; + } rp_config; + + struct { + bool use_default; + + struct non_root_profile profile; + } nrp_config; + }; +}; + +// Escalate current process to root with the appropriate profile +void escape_with_root_profile(void); + +void escape_to_root_for_cmd_su(uid_t target_uid, pid_t target_pid); + +void disable_seccomp(void); + +#endif diff --git a/kernel/arch.h b/kernel/arch.h index eec38c2..ee2b16c 100644 --- a/kernel/arch.h +++ b/kernel/arch.h @@ -18,10 +18,8 @@ #define __PT_SP_REG sp #define __PT_IP_REG pc -#define PRCTL_SYMBOL "__arm64_sys_prctl" +#define REBOOT_SYMBOL "__arm64_sys_reboot" #define SYS_READ_SYMBOL "__arm64_sys_read" -#define SYS_NEWFSTATAT_SYMBOL "__arm64_sys_newfstatat" -#define SYS_FACCESSAT_SYMBOL "__arm64_sys_faccessat" #define SYS_EXECVE_SYMBOL "__arm64_sys_execve" #elif defined(__x86_64__) @@ -39,10 +37,8 @@ #define __PT_RC_REG ax #define __PT_SP_REG sp #define __PT_IP_REG ip -#define PRCTL_SYMBOL "__x64_sys_prctl" +#define REBOOT_SYMBOL "__x64_sys_reboot" #define SYS_READ_SYMBOL "__x64_sys_read" -#define SYS_NEWFSTATAT_SYMBOL "__x64_sys_newfstatat" -#define SYS_FACCESSAT_SYMBOL "__x64_sys_faccessat" #define SYS_EXECVE_SYMBOL "__x64_sys_execve" #else diff --git a/kernel/core_hook.c b/kernel/core_hook.c deleted file mode 100644 index 63c066f..0000000 --- a/kernel/core_hook.c +++ /dev/null @@ -1,1092 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#ifdef MODULE -#include -#include -#include -#include -#include -#endif - -#include "allowlist.h" -#include "arch.h" -#include "core_hook.h" -#include "klog.h" // IWYU pragma: keep -#include "ksu.h" -#include "ksud.h" -#include "manager.h" -#include "selinux/selinux.h" -#include "throne_tracker.h" -#include "throne_comm.h" -#include "kernel_compat.h" - -#include "kpm/kpm.h" -#include "dynamic_manager.h" - -static bool ksu_module_mounted = false; - -extern int handle_sepolicy(unsigned long arg3, void __user *arg4); - -static bool ksu_su_compat_enabled = true; -extern void ksu_sucompat_init(); -extern void ksu_sucompat_exit(); - -static inline bool is_allow_su() -{ - if (is_manager()) { - // we are manager, allow! - return true; - } - return ksu_is_allow_uid(current_uid().val); -} - -static inline bool is_unsupported_app_uid(uid_t uid) -{ -#define LAST_APPLICATION_UID 19999 - uid_t appid = uid % 100000; - return appid > LAST_APPLICATION_UID; -} - -static struct group_info root_groups = { .usage = ATOMIC_INIT(2) }; - -static void setup_groups(struct root_profile *profile, struct cred *cred) -{ - if (profile->groups_count > KSU_MAX_GROUPS) { - pr_warn("Failed to setgroups, too large group: %d!\n", - profile->uid); - return; - } - - if (profile->groups_count == 1 && profile->groups[0] == 0) { - // setgroup to root and return early. - if (cred->group_info) - put_group_info(cred->group_info); - cred->group_info = get_group_info(&root_groups); - return; - } - - u32 ngroups = profile->groups_count; - struct group_info *group_info = groups_alloc(ngroups); - if (!group_info) { - pr_warn("Failed to setgroups, ENOMEM for: %d\n", profile->uid); - return; - } - - int i; - for (i = 0; i < ngroups; i++) { - gid_t gid = profile->groups[i]; - kgid_t kgid = make_kgid(current_user_ns(), gid); - if (!gid_valid(kgid)) { - pr_warn("Failed to setgroups, invalid gid: %d\n", gid); - put_group_info(group_info); - return; - } - group_info->gid[i] = kgid; - } - - groups_sort(group_info); - set_groups(cred, group_info); - put_group_info(group_info); -} - -static void disable_seccomp() -{ - assert_spin_locked(¤t->sighand->siglock); - // disable seccomp -#if defined(CONFIG_GENERIC_ENTRY) && \ - LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) - current_thread_info()->syscall_work &= ~SYSCALL_WORK_SECCOMP; -#else - current_thread_info()->flags &= ~(TIF_SECCOMP | _TIF_SECCOMP); -#endif - -#ifdef CONFIG_SECCOMP - current->seccomp.mode = 0; - current->seccomp.filter = NULL; -#else -#endif -} - -void escape_to_root(void) -{ - struct cred *cred; - - cred = prepare_creds(); - if (!cred) { - pr_warn("prepare_creds failed!\n"); - return; - } - - if (cred->euid.val == 0) { - pr_warn("Already root, don't escape!\n"); - abort_creds(cred); - return; - } - - struct root_profile *profile = ksu_get_root_profile(cred->uid.val); - - cred->uid.val = profile->uid; - cred->suid.val = profile->uid; - cred->euid.val = profile->uid; - cred->fsuid.val = profile->uid; - - cred->gid.val = profile->gid; - cred->fsgid.val = profile->gid; - cred->sgid.val = profile->gid; - cred->egid.val = profile->gid; - cred->securebits = 0; - - BUILD_BUG_ON(sizeof(profile->capabilities.effective) != - sizeof(kernel_cap_t)); - - // setup capabilities - // we need CAP_DAC_READ_SEARCH becuase `/data/adb/ksud` is not accessible for non root process - // we add it here but don't add it to cap_inhertiable, it would be dropped automaticly after exec! - u64 cap_for_ksud = - profile->capabilities.effective | CAP_DAC_READ_SEARCH; - memcpy(&cred->cap_effective, &cap_for_ksud, - sizeof(cred->cap_effective)); - memcpy(&cred->cap_permitted, &profile->capabilities.effective, - sizeof(cred->cap_permitted)); - memcpy(&cred->cap_bset, &profile->capabilities.effective, - sizeof(cred->cap_bset)); - - setup_groups(profile, cred); - - commit_creds(cred); - - // Refer to kernel/seccomp.c: seccomp_set_mode_strict - // When disabling Seccomp, ensure that current->sighand->siglock is held during the operation. - spin_lock_irq(¤t->sighand->siglock); - disable_seccomp(); - spin_unlock_irq(¤t->sighand->siglock); - - setup_selinux(profile->selinux_domain); -} - -int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry) -{ - if (!current->mm) { - // skip kernel threads - return 0; - } - - if (current_uid().val != 1000) { - // skip non system uid - return 0; - } - - if (!old_dentry || !new_dentry) { - return 0; - } - - // /data/system/packages.list.tmp -> /data/system/packages.list - if (strcmp(new_dentry->d_iname, "packages.list")) { - return 0; - } - - char path[128]; - char *buf = dentry_path_raw(new_dentry, path, sizeof(path)); - if (IS_ERR(buf)) { - pr_err("dentry_path_raw failed.\n"); - return 0; - } - - if (!strstr(buf, "/system/packages.list")) { - return 0; - } - pr_info("renameat: %s -> %s, new path: %s\n", old_dentry->d_iname, - new_dentry->d_iname, buf); - - track_throne(); - - // Also request userspace scan for next time - ksu_request_userspace_scan(); - - return 0; -} - -#ifdef CONFIG_EXT4_FS -static void nuke_ext4_sysfs() { - struct path path; - int err = kern_path("/data/adb/modules", 0, &path); - if (err) { - pr_err("nuke path err: %d\n", err); - return; - } - - struct super_block* sb = path.dentry->d_inode->i_sb; - const char* name = sb->s_type->name; - if (strcmp(name, "ext4") != 0) { - pr_info("nuke but module aren't mounted\n"); - return; - } - - ext4_unregister_sysfs(sb); - path_put(&path); -} -#else -static inline void nuke_ext4_sysfs() { } -#endif - -static bool is_system_bin_su() -{ - // YES in_execve becomes 0 when it succeeds. - if (!current->mm || current->in_execve) - return false; - - // quick af check - return (current->mm->exe_file && !strcmp(current->mm->exe_file->f_path.dentry->d_name.name, "su")); -} - -int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, - unsigned long arg4, unsigned long arg5) -{ - // if success, we modify the arg5 as result! - u32 *result = (u32 *)arg5; - u32 reply_ok = KERNEL_SU_OPTION; - - if (KERNEL_SU_OPTION != option) { - return 0; - } - - // TODO: find it in throne tracker! - uid_t current_uid_val = current_uid().val; - uid_t manager_uid = ksu_get_manager_uid(); - if (current_uid_val != manager_uid && - current_uid_val % 100000 == manager_uid) { - ksu_set_manager_uid(current_uid_val); - } - - bool from_root = 0 == current_uid().val; - bool from_manager = is_manager(); - - if (!from_root && !from_manager - && !(is_allow_su() && is_system_bin_su())) { - // only root or manager can access this interface - return 0; - } - -#ifdef CONFIG_KSU_DEBUG - pr_info("option: 0x%x, cmd: %ld\n", option, arg2); -#endif - - if (arg2 == CMD_BECOME_MANAGER) { - if (from_manager) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("become_manager: prctl reply error\n"); - } - return 0; - } - return 0; - } - - if (arg2 == CMD_GRANT_ROOT) { - if (is_allow_su()) { - pr_info("allow root for: %d\n", current_uid().val); - escape_to_root(); - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("grant_root: prctl reply error\n"); - } - } - return 0; - } - - // Both root manager and root processes should be allowed to get version - if (arg2 == CMD_GET_VERSION) { - u32 version = KERNEL_SU_VERSION; - if (copy_to_user(arg3, &version, sizeof(version))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - u32 version_flags = 2; -#ifdef MODULE - version_flags |= 0x1; -#endif - if (arg4 && - copy_to_user(arg4, &version_flags, sizeof(version_flags))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - return 0; - } - - // Allow root manager to get full version strings - if (arg2 == CMD_GET_FULL_VERSION) { - char ksu_version_full[KSU_FULL_VERSION_STRING] = {0}; -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) - strscpy(ksu_version_full, KSU_VERSION_FULL, KSU_FULL_VERSION_STRING); -#else - strlcpy(ksu_version_full, KSU_VERSION_FULL, KSU_FULL_VERSION_STRING); -#endif - if (copy_to_user((void __user *)arg3, ksu_version_full, KSU_FULL_VERSION_STRING)) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - return -EFAULT; - } - return 0; - } - - // Allow the root manager to configure dynamic manageratures - if (arg2 == CMD_DYNAMIC_MANAGER) { - if (!from_root && !from_manager) { - return 0; - } - - struct dynamic_manager_user_config config; - - if (copy_from_user(&config, (void __user *)arg3, sizeof(config))) { - pr_err("copy dynamic manager config failed\n"); - return 0; - } - - int ret = ksu_handle_dynamic_manager(&config); - - if (ret == 0 && config.operation == DYNAMIC_MANAGER_OP_GET) { - if (copy_to_user((void __user *)arg3, &config, sizeof(config))) { - pr_err("copy dynamic manager config back failed\n"); - return 0; - } - } - - if (ret == 0) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("dynamic_manager: prctl reply error\n"); - } - } - return 0; - } - - // Allow root manager to get active managers - if (arg2 == CMD_GET_MANAGERS) { - if (!from_root && !from_manager) { - return 0; - } - - struct manager_list_info manager_info; - int ret = ksu_get_active_managers(&manager_info); - - if (ret == 0) { - if (copy_to_user((void __user *)arg3, &manager_info, sizeof(manager_info))) { - pr_err("copy manager list failed\n"); - return 0; - } - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("get_managers: prctl reply error\n"); - } - } - return 0; - } - - if (arg2 == CMD_REPORT_EVENT) { - if (!from_root) { - return 0; - } - switch (arg3) { - case EVENT_POST_FS_DATA: { - static bool post_fs_data_lock = false; - if (!post_fs_data_lock) { - post_fs_data_lock = true; - pr_info("post-fs-data triggered\n"); - on_post_fs_data(); - // Initialize throne communication - ksu_throne_comm_init(); - // Initializing Dynamic Signatures - ksu_dynamic_manager_init(); - pr_info("Dynamic sign config loaded during post-fs-data\n"); - } - break; - } - case EVENT_BOOT_COMPLETED: { - static bool boot_complete_lock = false; - if (!boot_complete_lock) { - boot_complete_lock = true; - pr_info("boot_complete triggered\n"); - } - break; - } - case EVENT_MODULE_MOUNTED: { - ksu_module_mounted = true; - pr_info("module mounted!\n"); - nuke_ext4_sysfs(); - break; - } - default: - break; - } - return 0; - } - - if (arg2 == CMD_SET_SEPOLICY) { - if (!from_root) { - return 0; - } - if (!handle_sepolicy(arg3, arg4)) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("sepolicy: prctl reply error\n"); - } - } - - return 0; - } - - if (arg2 == CMD_CHECK_SAFEMODE) { - if (ksu_is_safe_mode()) { - pr_warn("safemode enabled!\n"); - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("safemode: prctl reply error\n"); - } - } - return 0; - } - - if (arg2 == CMD_GET_ALLOW_LIST || arg2 == CMD_GET_DENY_LIST) { - u32 array[128]; - u32 array_length; - bool success = ksu_get_allow_list(array, &array_length, - arg2 == CMD_GET_ALLOW_LIST); - if (success) { - if (!copy_to_user(arg4, &array_length, - sizeof(array_length)) && - !copy_to_user(arg3, array, - sizeof(u32) * array_length)) { - if (copy_to_user(result, &reply_ok, - sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", - arg2); - } - } else { - pr_err("prctl copy allowlist error\n"); - } - } - return 0; - } - - if (arg2 == CMD_UID_GRANTED_ROOT || arg2 == CMD_UID_SHOULD_UMOUNT) { - uid_t target_uid = (uid_t)arg3; - bool allow = false; - if (arg2 == CMD_UID_GRANTED_ROOT) { - allow = ksu_is_allow_uid(target_uid); - } else if (arg2 == CMD_UID_SHOULD_UMOUNT) { - allow = ksu_uid_should_umount(target_uid); - } else { - pr_err("unknown cmd: %lu\n", arg2); - } - if (!copy_to_user(arg4, &allow, sizeof(allow))) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - } else { - pr_err("prctl copy err, cmd: %lu\n", arg2); - } - return 0; - } - - if (arg2 == CMD_ENABLE_SU) { - bool enabled = (arg3 != 0); - if (enabled == ksu_su_compat_enabled) { - pr_info("cmd enable su but no need to change.\n"); - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {// return the reply_ok directly - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - return 0; - } - - if (enabled) { - ksu_sucompat_init(); - } else { - ksu_sucompat_exit(); - } - ksu_su_compat_enabled = enabled; - - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - - return 0; - } - - #ifdef CONFIG_KPM - // ADD: 添加KPM模块控制 - if(sukisu_is_kpm_control_code(arg2)) { - int res; - - pr_info("KPM: calling before arg2=%d\n", (int) arg2); - - res = sukisu_handle_kpm(arg2, arg3, arg4, arg5); - - return 0; - } - #endif - - if (arg2 == CMD_ENABLE_KPM) { - bool KPM_Enabled = IS_ENABLED(CONFIG_KPM); - if (copy_to_user((void __user *)arg3, &KPM_Enabled, sizeof(KPM_Enabled))) - pr_info("KPM: copy_to_user() failed\n"); - return 0; - } - - // Checking hook usage - if (arg2 == CMD_HOOK_TYPE) { - const char *hook_type = "Kprobes"; -#if defined(CONFIG_KSU_TRACEPOINT_HOOK) - hook_type = "Tracepoint"; -#elif defined(CONFIG_KSU_MANUAL_HOOK) - hook_type = "Manual"; -#endif - - size_t len = strlen(hook_type) + 1; - if (copy_to_user((void __user *)arg3, hook_type, len)) { - pr_err("hook_type: copy_to_user failed\n"); - return 0; - } - - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("hook_type: prctl reply error\n"); - } - return 0; - } - - - // all other cmds are for 'root manager' - if (!from_manager) { - return 0; - } - - // we are already manager - if (arg2 == CMD_GET_APP_PROFILE) { - struct app_profile profile; - if (copy_from_user(&profile, arg3, sizeof(profile))) { - pr_err("copy profile failed\n"); - return 0; - } - - bool success = ksu_get_app_profile(&profile); - if (success) { - if (copy_to_user(arg3, &profile, sizeof(profile))) { - pr_err("copy profile failed\n"); - return 0; - } - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - } - return 0; - } - - if (arg2 == CMD_SET_APP_PROFILE) { - struct app_profile profile; - if (copy_from_user(&profile, arg3, sizeof(profile))) { - pr_err("copy profile failed\n"); - return 0; - } - - // todo: validate the params - if (ksu_set_app_profile(&profile, true)) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - } - return 0; - } - - if (arg2 == CMD_IS_SU_ENABLED) { - if (copy_to_user(arg3, &ksu_su_compat_enabled, - sizeof(ksu_su_compat_enabled))) { - pr_err("copy su compat failed\n"); - return 0; - } - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - return 0; - } - - return 0; -} - -static bool is_non_appuid(kuid_t uid) -{ -#define PER_USER_RANGE 100000 -#define FIRST_APPLICATION_UID 10000 - - uid_t appid = uid.val % PER_USER_RANGE; - return appid < FIRST_APPLICATION_UID; -} - -static bool should_umount(struct path *path) -{ - if (!path) { - return false; - } - - if (current->nsproxy->mnt_ns == init_nsproxy.mnt_ns) { - pr_info("ignore global mnt namespace process: %d\n", - current_uid().val); - return false; - } - - if (path->mnt && path->mnt->mnt_sb && path->mnt->mnt_sb->s_type) { - const char *fstype = path->mnt->mnt_sb->s_type->name; - return strcmp(fstype, "overlay") == 0; - } - return false; -} - -static void ksu_umount_mnt(struct path *path, int flags) -{ - int err = path_umount(path, flags); - if (err) { - pr_info("umount %s failed: %d\n", path->dentry->d_iname, err); - } -} - -static void try_umount(const char *mnt, bool check_mnt, int flags) -{ - struct path path; - int err = kern_path(mnt, 0, &path); - if (err) { - return; - } - - if (path.dentry != path.mnt->mnt_root) { - // it is not root mountpoint, maybe umounted by others already. - path_put(&path); - return; - } - - // we are only interest in some specific mounts - if (check_mnt && !should_umount(&path)) { - path_put(&path); - return; - } - - ksu_umount_mnt(&path, flags); -} - -int ksu_handle_setuid(struct cred *new, const struct cred *old) -{ - // this hook is used for umounting overlayfs for some uid, if there isn't any module mounted, just ignore it! - if (!ksu_module_mounted) { - return 0; - } - - if (!new || !old) { - return 0; - } - - kuid_t new_uid = new->uid; - kuid_t old_uid = old->uid; - - if (0 != old_uid.val) { - // old process is not root, ignore it. - return 0; - } - - if (is_non_appuid(new_uid)) { -#ifdef CONFIG_KSU_DEBUG - pr_info("handle setuid ignore non application uid: %d\n", new_uid.val); -#endif - return 0; - } - - // isolated process may be directly forked from zygote, always unmount - if (is_unsupported_app_uid(new_uid.val)) { -#ifdef CONFIG_KSU_DEBUG - pr_info("handle umount for unsupported application uid: %d\n", new_uid.val); -#endif - goto do_umount; - } - - if (ksu_is_allow_uid(new_uid.val)) { -#ifdef CONFIG_KSU_DEBUG - pr_info("handle setuid ignore allowed application: %d\n", new_uid.val); -#endif - return 0; - } - - if (!ksu_uid_should_umount(new_uid.val)) { - return 0; - } else { -#ifdef CONFIG_KSU_DEBUG - pr_info("uid: %d should not umount!\n", current_uid().val); -#endif - } - -do_umount: - // check old process's selinux context, if it is not zygote, ignore it! - // because some su apps may setuid to untrusted_app but they are in global mount namespace - // when we umount for such process, that is a disaster! - if (!is_zygote(old->security)) { - pr_info("handle umount ignore non zygote child: %d\n", - current->pid); - return 0; - } -#ifdef CONFIG_KSU_DEBUG - // umount the target mnt - pr_info("handle umount for uid: %d, pid: %d\n", new_uid.val, - current->pid); -#endif - - // fixme: use `collect_mounts` and `iterate_mount` to iterate all mountpoint and - // filter the mountpoint whose target is `/data/adb` - try_umount("/system", true, 0); - try_umount("/vendor", true, 0); - try_umount("/product", true, 0); - try_umount("/system_ext", true, 0); - try_umount("/data/adb/modules", false, MNT_DETACH); - - // try umount ksu temp path - try_umount("/debug_ramdisk", false, MNT_DETACH); - - return 0; -} - -// Init functons - -static int handler_pre(struct kprobe *p, struct pt_regs *regs) -{ - struct pt_regs *real_regs = PT_REAL_REGS(regs); - int option = (int)PT_REGS_PARM1(real_regs); - unsigned long arg2 = (unsigned long)PT_REGS_PARM2(real_regs); - unsigned long arg3 = (unsigned long)PT_REGS_PARM3(real_regs); - // PRCTL_SYMBOL is the arch-specificed one, which receive raw pt_regs from syscall - unsigned long arg4 = (unsigned long)PT_REGS_SYSCALL_PARM4(real_regs); - unsigned long arg5 = (unsigned long)PT_REGS_PARM5(real_regs); - - return ksu_handle_prctl(option, arg2, arg3, arg4, arg5); -} - -static struct kprobe prctl_kp = { - .symbol_name = PRCTL_SYMBOL, - .pre_handler = handler_pre, -}; - -static int renameat_handler_pre(struct kprobe *p, struct pt_regs *regs) -{ -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 12, 0) - // https://elixir.bootlin.com/linux/v5.12-rc1/source/include/linux/fs.h - struct renamedata *rd = PT_REGS_PARM1(regs); - struct dentry *old_entry = rd->old_dentry; - struct dentry *new_entry = rd->new_dentry; -#else - struct dentry *old_entry = (struct dentry *)PT_REGS_PARM2(regs); - struct dentry *new_entry = (struct dentry *)PT_REGS_CCALL_PARM4(regs); -#endif - - return ksu_handle_rename(old_entry, new_entry); -} - -static struct kprobe renameat_kp = { - .symbol_name = "vfs_rename", - .pre_handler = renameat_handler_pre, -}; - -__maybe_unused int ksu_kprobe_init(void) -{ - int rc = 0; - rc = register_kprobe(&prctl_kp); - - if (rc) { - pr_info("prctl kprobe failed: %d.\n", rc); - return rc; - } - - rc = register_kprobe(&renameat_kp); - pr_info("renameat kp: %d\n", rc); - - return rc; -} - -__maybe_unused int ksu_kprobe_exit(void) -{ - unregister_kprobe(&prctl_kp); - unregister_kprobe(&renameat_kp); - return 0; -} - -#ifndef DEVPTS_SUPER_MAGIC -#define DEVPTS_SUPER_MAGIC 0x1cd1 -#endif - -extern int __ksu_handle_devpts(struct inode *inode); // sucompat.c - -int ksu_inode_permission(struct inode *inode, int mask) -{ - if (inode && inode->i_sb - && unlikely(inode->i_sb->s_magic == DEVPTS_SUPER_MAGIC)) { - //pr_info("%s: handling devpts for: %s \n", __func__, current->comm); - __ksu_handle_devpts(inode); - } - return 0; -} - -#ifdef CONFIG_COMPAT -bool ksu_is_compat __read_mostly = false; -#endif - -int ksu_bprm_check(struct linux_binprm *bprm) -{ - char *filename = (char *)bprm->filename; - - if (likely(!ksu_execveat_hook)) - return 0; - -#ifdef CONFIG_COMPAT - static bool compat_check_done __read_mostly = false; - if ( unlikely(!compat_check_done) && unlikely(!strcmp(filename, "/data/adb/ksud")) - && !memcmp(bprm->buf, "\x7f\x45\x4c\x46", 4) ) { - if (bprm->buf[4] == 0x01 ) - ksu_is_compat = true; - - pr_info("%s: %s ELF magic found! ksu_is_compat: %d \n", __func__, filename, ksu_is_compat); - compat_check_done = true; - } -#endif - - ksu_handle_pre_ksud(filename); - - return 0; - -} - -static int ksu_task_prctl(int option, unsigned long arg2, unsigned long arg3, - unsigned long arg4, unsigned long arg5) -{ - ksu_handle_prctl(option, arg2, arg3, arg4, arg5); - return -ENOSYS; -} - -static int ksu_inode_rename(struct inode *old_inode, struct dentry *old_dentry, - struct inode *new_inode, struct dentry *new_dentry) -{ - return ksu_handle_rename(old_dentry, new_dentry); -} - -static int ksu_task_fix_setuid(struct cred *new, const struct cred *old, - int flags) -{ - return ksu_handle_setuid(new, old); -} - -#ifndef MODULE -static struct security_hook_list ksu_hooks[] = { - LSM_HOOK_INIT(task_prctl, ksu_task_prctl), - LSM_HOOK_INIT(inode_rename, ksu_inode_rename), - LSM_HOOK_INIT(task_fix_setuid, ksu_task_fix_setuid), - LSM_HOOK_INIT(inode_permission, ksu_inode_permission), -#ifndef CONFIG_KSU_KPROBES_HOOK - LSM_HOOK_INIT(bprm_check_security, ksu_bprm_check), -#endif -}; - -void __init ksu_lsm_hook_init(void) -{ - security_add_hooks(ksu_hooks, ARRAY_SIZE(ksu_hooks), "ksu"); -} - -#else -static int override_security_head(void *head, const void *new_head, size_t len) -{ - unsigned long base = (unsigned long)head & PAGE_MASK; - unsigned long offset = offset_in_page(head); - - // this is impossible for our case because the page alignment - // but be careful for other cases! - BUG_ON(offset + len > PAGE_SIZE); - struct page *page = phys_to_page(__pa(base)); - if (!page) { - return -EFAULT; - } - - void *addr = vmap(&page, 1, VM_MAP, PAGE_KERNEL); - if (!addr) { - return -ENOMEM; - } - local_irq_disable(); - memcpy(addr + offset, new_head, len); - local_irq_enable(); - vunmap(addr); - return 0; -} - -static void free_security_hook_list(struct hlist_head *head) -{ - struct hlist_node *temp; - struct security_hook_list *entry; - - if (!head) - return; - - hlist_for_each_entry_safe (entry, temp, head, list) { - hlist_del(&entry->list); - kfree(entry); - } - - kfree(head); -} - -struct hlist_head *copy_security_hlist(struct hlist_head *orig) -{ - struct hlist_head *new_head = kmalloc(sizeof(*new_head), GFP_KERNEL); - if (!new_head) - return NULL; - - INIT_HLIST_HEAD(new_head); - - struct security_hook_list *entry; - struct security_hook_list *new_entry; - - hlist_for_each_entry (entry, orig, list) { - new_entry = kmalloc(sizeof(*new_entry), GFP_KERNEL); - if (!new_entry) { - free_security_hook_list(new_head); - return NULL; - } - - *new_entry = *entry; - - hlist_add_tail_rcu(&new_entry->list, new_head); - } - - return new_head; -} - -#define LSM_SEARCH_MAX 180 // This should be enough to iterate -static void *find_head_addr(void *security_ptr, int *index) -{ - if (!security_ptr) { - return NULL; - } - struct hlist_head *head_start = - (struct hlist_head *)&security_hook_heads; - - for (int i = 0; i < LSM_SEARCH_MAX; i++) { - struct hlist_head *head = head_start + i; - struct security_hook_list *pos; - hlist_for_each_entry (pos, head, list) { - if (pos->hook.capget == security_ptr) { - if (index) { - *index = i; - } - return head; - } - } - } - - return NULL; -} - -#define GET_SYMBOL_ADDR(sym) \ - ({ \ - void *addr = kallsyms_lookup_name(#sym ".cfi_jt"); \ - if (!addr) { \ - addr = kallsyms_lookup_name(#sym); \ - } \ - addr; \ - }) - -#define KSU_LSM_HOOK_HACK_INIT(head_ptr, name, func) \ - do { \ - static struct security_hook_list hook = { \ - .hook = { .name = func } \ - }; \ - hook.head = head_ptr; \ - hook.lsm = "ksu"; \ - struct hlist_head *new_head = copy_security_hlist(hook.head); \ - if (!new_head) { \ - pr_err("Failed to copy security list: %s\n", #name); \ - break; \ - } \ - hlist_add_tail_rcu(&hook.list, new_head); \ - if (override_security_head(hook.head, new_head, \ - sizeof(*new_head))) { \ - free_security_hook_list(new_head); \ - pr_err("Failed to hack lsm for: %s\n", #name); \ - } \ - } while (0) - -void __init ksu_lsm_hook_init(void) -{ - void *cap_prctl = GET_SYMBOL_ADDR(cap_task_prctl); - void *prctl_head = find_head_addr(cap_prctl, NULL); - if (prctl_head) { - if (prctl_head != &security_hook_heads.task_prctl) { - pr_warn("prctl's address has shifted!\n"); - } - KSU_LSM_HOOK_HACK_INIT(prctl_head, task_prctl, ksu_task_prctl); - } else { - pr_warn("Failed to find task_prctl!\n"); - } - - int inode_killpriv_index = -1; - void *cap_killpriv = GET_SYMBOL_ADDR(cap_inode_killpriv); - find_head_addr(cap_killpriv, &inode_killpriv_index); - if (inode_killpriv_index < 0) { - pr_warn("Failed to find inode_rename, use kprobe instead!\n"); - register_kprobe(&renameat_kp); - } else { - int inode_rename_index = inode_killpriv_index + - &security_hook_heads.inode_rename - - &security_hook_heads.inode_killpriv; - struct hlist_head *head_start = - (struct hlist_head *)&security_hook_heads; - void *inode_rename_head = head_start + inode_rename_index; - if (inode_rename_head != &security_hook_heads.inode_rename) { - pr_warn("inode_rename's address has shifted!\n"); - } - KSU_LSM_HOOK_HACK_INIT(inode_rename_head, inode_rename, - ksu_inode_rename); - } - void *cap_setuid = GET_SYMBOL_ADDR(cap_task_fix_setuid); - void *setuid_head = find_head_addr(cap_setuid, NULL); - if (setuid_head) { - if (setuid_head != &security_hook_heads.task_fix_setuid) { - pr_warn("setuid's address has shifted!\n"); - } - KSU_LSM_HOOK_HACK_INIT(setuid_head, task_fix_setuid, - ksu_task_fix_setuid); - } else { - pr_warn("Failed to find task_fix_setuid!\n"); - } - smp_mb(); -} -#endif - -void __init ksu_core_init(void) -{ - ksu_lsm_hook_init(); -} - -void ksu_core_exit(void) -{ - ksu_throne_comm_exit(); -#ifdef CONFIG_KPROBE - pr_info("ksu_core_kprobe_exit\n"); - // we dont use this now - // ksu_kprobe_exit(); -#endif -} diff --git a/kernel/core_hook.h b/kernel/core_hook.h deleted file mode 100644 index 6ed328a..0000000 --- a/kernel/core_hook.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef __KSU_H_KSU_CORE -#define __KSU_H_KSU_CORE - -#include -#include "apk_sign.h" - -void __init ksu_core_init(void); -void ksu_core_exit(void); - -#endif diff --git a/kernel/dynamic_manager.c b/kernel/dynamic_manager.c index 6f34d19..96bc710 100644 --- a/kernel/dynamic_manager.c +++ b/kernel/dynamic_manager.c @@ -17,7 +17,6 @@ #include "dynamic_manager.h" #include "klog.h" // IWYU pragma: keep -#include "kernel_compat.h" #include "manager.h" #define MAX_MANAGERS 2 @@ -233,23 +232,23 @@ static void do_save_dynamic_manager(struct work_struct *work) return; } - fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_MANAGER, O_WRONLY | O_CREAT | O_TRUNC, 0644); + fp = filp_open(KERNEL_SU_DYNAMIC_MANAGER, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (IS_ERR(fp)) { pr_err("save_dynamic_manager create file failed: %ld\n", PTR_ERR(fp)); return; } - if (ksu_kernel_write_compat(fp, &magic, sizeof(magic), &off) != sizeof(magic)) { + if (kernel_write(fp, &magic, sizeof(magic), &off) != sizeof(magic)) { pr_err("save_dynamic_manager write magic failed.\n"); goto exit; } - if (ksu_kernel_write_compat(fp, &version, sizeof(version), &off) != sizeof(version)) { + if (kernel_write(fp, &version, sizeof(version), &off) != sizeof(version)) { pr_err("save_dynamic_manager write version failed.\n"); goto exit; } - if (ksu_kernel_write_compat(fp, &config_to_save, sizeof(config_to_save), &off) != sizeof(config_to_save)) { + if (kernel_write(fp, &config_to_save, sizeof(config_to_save), &off) != sizeof(config_to_save)) { pr_err("save_dynamic_manager write config failed.\n"); goto exit; } @@ -271,7 +270,7 @@ static void do_load_dynamic_manager(struct work_struct *work) unsigned long flags; int i; - fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_MANAGER, O_RDONLY, 0); + fp = filp_open(KERNEL_SU_DYNAMIC_MANAGER, O_RDONLY, 0); if (IS_ERR(fp)) { if (PTR_ERR(fp) == -ENOENT) { pr_info("No saved dynamic manager config found\n"); @@ -281,20 +280,20 @@ static void do_load_dynamic_manager(struct work_struct *work) return; } - if (ksu_kernel_read_compat(fp, &magic, sizeof(magic), &off) != sizeof(magic) || + if (kernel_read(fp, &magic, sizeof(magic), &off) != sizeof(magic) || magic != DYNAMIC_MANAGER_FILE_MAGIC) { pr_err("dynamic manager file invalid magic: %x!\n", magic); goto exit; } - if (ksu_kernel_read_compat(fp, &version, sizeof(version), &off) != sizeof(version)) { + if (kernel_read(fp, &version, sizeof(version), &off) != sizeof(version)) { pr_err("dynamic manager read version failed\n"); goto exit; } pr_info("dynamic manager file version: %d\n", version); - ret = ksu_kernel_read_compat(fp, &loaded_config, sizeof(loaded_config), &off); + ret = kernel_read(fp, &loaded_config, sizeof(loaded_config), &off); if (ret <= 0) { pr_info("load_dynamic_manager read err: %zd\n", ret); goto exit; @@ -348,14 +347,14 @@ static void do_clear_dynamic_manager(struct work_struct *work) memset(zero_buffer, 0, sizeof(zero_buffer)); - fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_MANAGER, O_WRONLY | O_CREAT | O_TRUNC, 0644); + fp = filp_open(KERNEL_SU_DYNAMIC_MANAGER, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (IS_ERR(fp)) { pr_err("clear_dynamic_manager create file failed: %ld\n", PTR_ERR(fp)); return; } // Write null bytes to overwrite the file content - if (ksu_kernel_write_compat(fp, zero_buffer, sizeof(zero_buffer), &off) != sizeof(zero_buffer)) { + if (kernel_write(fp, zero_buffer, sizeof(zero_buffer), &off) != sizeof(zero_buffer)) { pr_err("clear_dynamic_manager write null bytes failed.\n"); } else { pr_info("Dynamic sign config file cleared successfully\n"); diff --git a/kernel/feature.c b/kernel/feature.c new file mode 100644 index 0000000..99277b5 --- /dev/null +++ b/kernel/feature.c @@ -0,0 +1,173 @@ +#include "feature.h" +#include "klog.h" // IWYU pragma: keep + +#include + +static const struct ksu_feature_handler *feature_handlers[KSU_FEATURE_MAX]; + +static DEFINE_MUTEX(feature_mutex); + +int ksu_register_feature_handler(const struct ksu_feature_handler *handler) +{ + if (!handler) { + pr_err("feature: register handler is NULL\n"); + return -EINVAL; + } + + if (handler->feature_id >= KSU_FEATURE_MAX) { + pr_err("feature: invalid feature_id %u\n", handler->feature_id); + return -EINVAL; + } + + if (!handler->get_handler && !handler->set_handler) { + pr_err("feature: no handler provided for feature %u\n", handler->feature_id); + return -EINVAL; + } + + mutex_lock(&feature_mutex); + + if (feature_handlers[handler->feature_id]) { + pr_warn("feature: handler for %u already registered, overwriting\n", + handler->feature_id); + } + + feature_handlers[handler->feature_id] = handler; + + pr_info("feature: registered handler for %s (id=%u)\n", + handler->name ? handler->name : "unknown", handler->feature_id); + + mutex_unlock(&feature_mutex); + return 0; +} + +int ksu_unregister_feature_handler(u32 feature_id) +{ + int ret = 0; + + if (feature_id >= KSU_FEATURE_MAX) { + pr_err("feature: invalid feature_id %u\n", feature_id); + return -EINVAL; + } + + mutex_lock(&feature_mutex); + + if (!feature_handlers[feature_id]) { + pr_warn("feature: no handler registered for %u\n", feature_id); + ret = -ENOENT; + goto out; + } + + feature_handlers[feature_id] = NULL; + + pr_info("feature: unregistered handler for id=%u\n", feature_id); + +out: + mutex_unlock(&feature_mutex); + return ret; +} + +int ksu_get_feature(u32 feature_id, u64 *value, bool *supported) +{ + int ret = 0; + const struct ksu_feature_handler *handler; + + if (feature_id >= KSU_FEATURE_MAX) { + pr_err("feature: invalid feature_id %u\n", feature_id); + return -EINVAL; + } + + if (!value || !supported) { + pr_err("feature: invalid parameters\n"); + return -EINVAL; + } + + mutex_lock(&feature_mutex); + + handler = feature_handlers[feature_id]; + + if (!handler) { + *supported = false; + *value = 0; + pr_debug("feature: feature %u not supported\n", feature_id); + goto out; + } + + *supported = true; + + if (!handler->get_handler) { + pr_warn("feature: no get_handler for feature %u\n", feature_id); + ret = -EOPNOTSUPP; + goto out; + } + + ret = handler->get_handler(value); + if (ret) { + pr_err("feature: get_handler for %u failed: %d\n", feature_id, ret); + } + +out: + mutex_unlock(&feature_mutex); + return ret; +} + +int ksu_set_feature(u32 feature_id, u64 value) +{ + int ret = 0; + const struct ksu_feature_handler *handler; + + if (feature_id >= KSU_FEATURE_MAX) { + pr_err("feature: invalid feature_id %u\n", feature_id); + return -EINVAL; + } + + mutex_lock(&feature_mutex); + + handler = feature_handlers[feature_id]; + + if (!handler) { + pr_err("feature: feature %u not registered\n", feature_id); + ret = -EOPNOTSUPP; + goto out; + } + + if (!handler->set_handler) { + pr_warn("feature: no set_handler for feature %u\n", feature_id); + ret = -EOPNOTSUPP; + goto out; + } + + ret = handler->set_handler(value); + if (ret) { + pr_err("feature: set_handler for %u failed: %d\n", feature_id, ret); + } + +out: + mutex_unlock(&feature_mutex); + return ret; +} + +void ksu_feature_init(void) +{ + int i; + + for (i = 0; i < KSU_FEATURE_MAX; i++) { + feature_handlers[i] = NULL; + } + + pr_info("feature: feature management initialized\n"); +} + +void ksu_feature_exit(void) +{ + int i; + + mutex_lock(&feature_mutex); + + for (i = 0; i < KSU_FEATURE_MAX; i++) { + feature_handlers[i] = NULL; + } + + mutex_unlock(&feature_mutex); + + pr_info("feature: feature management cleaned up\n"); +} diff --git a/kernel/feature.h b/kernel/feature.h new file mode 100644 index 0000000..9b63a0b --- /dev/null +++ b/kernel/feature.h @@ -0,0 +1,37 @@ +#ifndef __KSU_H_FEATURE +#define __KSU_H_FEATURE + +#include + +enum ksu_feature_id { + KSU_FEATURE_SU_COMPAT = 0, + KSU_FEATURE_KERNEL_UMOUNT = 1, + KSU_FEATURE_ENHANCED_SECURITY = 2, + KSU_FEATURE_SULOG = 3, + + KSU_FEATURE_MAX +}; + +typedef int (*ksu_feature_get_t)(u64 *value); +typedef int (*ksu_feature_set_t)(u64 value); + +struct ksu_feature_handler { + u32 feature_id; + const char *name; + ksu_feature_get_t get_handler; + ksu_feature_set_t set_handler; +}; + +int ksu_register_feature_handler(const struct ksu_feature_handler *handler); + +int ksu_unregister_feature_handler(u32 feature_id); + +int ksu_get_feature(u32 feature_id, u64 *value, bool *supported); + +int ksu_set_feature(u32 feature_id, u64 value); + +void ksu_feature_init(void); + +void ksu_feature_exit(void); + +#endif // __KSU_H_FEATURE diff --git a/kernel/file_wrapper.c b/kernel/file_wrapper.c new file mode 100644 index 0000000..d73cf5d --- /dev/null +++ b/kernel/file_wrapper.c @@ -0,0 +1,341 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "klog.h" // IWYU pragma: keep +#include "selinux/selinux.h" + +#include "file_wrapper.h" + +static loff_t ksu_wrapper_llseek(struct file *fp, loff_t off, int flags) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->llseek(data->orig, off, flags); +} + +static ssize_t ksu_wrapper_read(struct file *fp, char __user *ptr, size_t sz, loff_t *off) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->read(orig, ptr, sz, off); +} + +static ssize_t ksu_wrapper_write(struct file *fp, const char __user *ptr, size_t sz, loff_t *off) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->write(orig, ptr, sz, off); +} + +static ssize_t ksu_wrapper_read_iter(struct kiocb *iocb, struct iov_iter *iovi) { + struct ksu_file_wrapper* data = iocb->ki_filp->private_data; + struct file* orig = data->orig; + iocb->ki_filp = orig; + return orig->f_op->read_iter(iocb, iovi); +} + +static ssize_t ksu_wrapper_write_iter(struct kiocb *iocb, struct iov_iter *iovi) { + struct ksu_file_wrapper* data = iocb->ki_filp->private_data; + struct file* orig = data->orig; + iocb->ki_filp = orig; + return orig->f_op->write_iter(iocb, iovi); +} + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 1, 0) +static int ksu_wrapper_iopoll(struct kiocb *kiocb, struct io_comp_batch* icb, unsigned int v) { + struct ksu_file_wrapper* data = kiocb->ki_filp->private_data; + struct file* orig = data->orig; + kiocb->ki_filp = orig; + return orig->f_op->iopoll(kiocb, icb, v); +} +#else +static int ksu_wrapper_iopoll(struct kiocb *kiocb, bool spin) { + struct ksu_file_wrapper* data = kiocb->ki_filp->private_data; + struct file* orig = data->orig; + kiocb->ki_filp = orig; + return orig->f_op->iopoll(kiocb, spin); +} +#endif + +#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0) +static int ksu_wrapper_iterate (struct file *fp, struct dir_context *dc) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->iterate(orig, dc); +} +#endif + +static int ksu_wrapper_iterate_shared(struct file *fp, struct dir_context *dc) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->iterate_shared(orig, dc); +} + +static __poll_t ksu_wrapper_poll(struct file *fp, struct poll_table_struct *pts) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->poll(orig, pts); +} + +static long ksu_wrapper_unlocked_ioctl(struct file *fp, unsigned int cmd, unsigned long arg) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->unlocked_ioctl(orig, cmd, arg); +} + +static long ksu_wrapper_compat_ioctl(struct file *fp, unsigned int cmd, unsigned long arg) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->compat_ioctl(orig, cmd, arg); +} + +static int ksu_wrapper_mmap(struct file *fp, struct vm_area_struct * vma) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->mmap(orig, vma); +} + +// static unsigned long mmap_supported_flags {} + +static int ksu_wrapper_open(struct inode *ino, struct file *fp) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + struct inode *orig_ino = file_inode(orig); + return orig->f_op->open(orig_ino, orig); +} + +static int ksu_wrapper_flush(struct file *fp, fl_owner_t id) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->flush(orig, id); +} + + +static int ksu_wrapper_fsync(struct file *fp, loff_t off1, loff_t off2, int datasync) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->fsync(orig, off1, off2, datasync); +} + +static int ksu_wrapper_fasync(int arg, struct file *fp, int arg2) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->fasync(arg, orig, arg2); +} + +static int ksu_wrapper_lock(struct file *fp, int arg1, struct file_lock *fl) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + return orig->f_op->lock(orig, arg1, fl); +} + + +#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0) +static ssize_t ksu_wrapper_sendpage(struct file *fp, struct page *pg, int arg1, size_t sz, loff_t *off, int arg2) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->sendpage) { + return orig->f_op->sendpage(orig, pg, arg1, sz, off, arg2); + } + return -EINVAL; +} +#endif + +static unsigned long ksu_wrapper_get_unmapped_area(struct file *fp, unsigned long arg1, unsigned long arg2, unsigned long arg3, unsigned long arg4) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->get_unmapped_area) { + return orig->f_op->get_unmapped_area(orig, arg1, arg2, arg3, arg4); + } + return -EINVAL; +} + +// static int ksu_wrapper_check_flags(int arg) {} + +static int ksu_wrapper_flock(struct file *fp, int arg1, struct file_lock *fl) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->flock) { + return orig->f_op->flock(orig, arg1, fl); + } + return -EINVAL; +} + +static ssize_t ksu_wrapper_splice_write(struct pipe_inode_info * pii, struct file *fp, loff_t *off, size_t sz, unsigned int arg1) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->splice_write) { + return orig->f_op->splice_write(pii, orig, off, sz, arg1); + } + return -EINVAL; +} + +static ssize_t ksu_wrapper_splice_read(struct file *fp, loff_t *off, struct pipe_inode_info *pii, size_t sz, unsigned int arg1) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->splice_read) { + return orig->f_op->splice_read(orig, off, pii, sz, arg1); + } + return -EINVAL; +} + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0) +void ksu_wrapper_splice_eof(struct file *fp) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->splice_eof) { + return orig->f_op->splice_eof(orig); + } +} +#endif + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0) +static int ksu_wrapper_setlease(struct file *fp, int arg1, struct file_lease **fl, void **p) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->setlease) { + return orig->f_op->setlease(orig, arg1, fl, p); + } + return -EINVAL; +} +#elif LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0) +static int ksu_wrapper_setlease(struct file *fp, int arg1, struct file_lock **fl, void **p) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->setlease) { + return orig->f_op->setlease(orig, arg1, fl, p); + } + return -EINVAL; +} +#else +static int ksu_wrapper_setlease(struct file *fp, long arg1, struct file_lock **fl, void **p) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->setlease) { + return orig->f_op->setlease(orig, arg1, fl, p); + } + return -EINVAL; +} +#endif + +static long ksu_wrapper_fallocate(struct file *fp, int mode, loff_t offset, loff_t len) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->fallocate) { + return orig->f_op->fallocate(orig, mode, offset, len); + } + return -EINVAL; +} + +static void ksu_wrapper_show_fdinfo(struct seq_file *m, struct file *f) { + struct ksu_file_wrapper* data = f->private_data; + struct file* orig = data->orig; + if (orig->f_op->show_fdinfo) { + orig->f_op->show_fdinfo(m, orig); + } +} + +static ssize_t ksu_wrapper_copy_file_range(struct file *f1, loff_t off1, struct file *f2, + loff_t off2, size_t sz, unsigned int flags) { + // TODO: determine which file to use + struct ksu_file_wrapper* data = f1->private_data; + struct file* orig = data->orig; + if (orig->f_op->copy_file_range) { + return orig->f_op->copy_file_range(orig, off1, f2, off2, sz, flags); + } + return -EINVAL; +} + +static loff_t ksu_wrapper_remap_file_range(struct file *file_in, loff_t pos_in, + struct file *file_out, loff_t pos_out, + loff_t len, unsigned int remap_flags) { + // TODO: determine which file to use + struct ksu_file_wrapper* data = file_in->private_data; + struct file* orig = data->orig; + if (orig->f_op->remap_file_range) { + return orig->f_op->remap_file_range(orig, pos_in, file_out, pos_out, len, remap_flags); + } + return -EINVAL; +} + +static int ksu_wrapper_fadvise(struct file *fp, loff_t off1, loff_t off2, int flags) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + if (orig->f_op->fadvise) { + return orig->f_op->fadvise(orig, off1, off2, flags); + } + return -EINVAL; +} + +static int ksu_wrapper_release(struct inode *inode, struct file *filp) { + ksu_delete_file_wrapper(filp->private_data); + return 0; +} + +struct ksu_file_wrapper* ksu_create_file_wrapper(struct file* fp) { + struct ksu_file_wrapper* p = kcalloc(sizeof(struct ksu_file_wrapper), 1, GFP_KERNEL); + if (!p) { + return NULL; + } + + get_file(fp); + + p->orig = fp; + p->ops.owner = THIS_MODULE; + p->ops.llseek = fp->f_op->llseek ? ksu_wrapper_llseek : NULL; + p->ops.read = fp->f_op->read ? ksu_wrapper_read : NULL; + p->ops.write = fp->f_op->write ? ksu_wrapper_write : NULL; + p->ops.read_iter = fp->f_op->read_iter ? ksu_wrapper_read_iter : NULL; + p->ops.write_iter = fp->f_op->write_iter ? ksu_wrapper_write_iter : NULL; + p->ops.iopoll = fp->f_op->iopoll ? ksu_wrapper_iopoll : NULL; +#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0) + p->ops.iterate = fp->f_op->iterate ? ksu_wrapper_iterate : NULL; +#endif + p->ops.iterate_shared = fp->f_op->iterate_shared ? ksu_wrapper_iterate_shared : NULL; + p->ops.poll = fp->f_op->poll ? ksu_wrapper_poll : NULL; + p->ops.unlocked_ioctl = fp->f_op->unlocked_ioctl ? ksu_wrapper_unlocked_ioctl : NULL; + p->ops.compat_ioctl = fp->f_op->compat_ioctl ? ksu_wrapper_compat_ioctl : NULL; + p->ops.mmap = fp->f_op->mmap ? ksu_wrapper_mmap : NULL; +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0) + p->ops.fop_flags = fp->f_op->fop_flags; +#else + p->ops.mmap_supported_flags = fp->f_op->mmap_supported_flags; +#endif + p->ops.open = fp->f_op->open ? ksu_wrapper_open : NULL; + p->ops.flush = fp->f_op->flush ? ksu_wrapper_flush : NULL; + p->ops.release = ksu_wrapper_release; + p->ops.fsync = fp->f_op->fsync ? ksu_wrapper_fsync : NULL; + p->ops.fasync = fp->f_op->fasync ? ksu_wrapper_fasync : NULL; + p->ops.lock = fp->f_op->lock ? ksu_wrapper_lock : NULL; +#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0) + p->ops.sendpage = fp->f_op->sendpage ? ksu_wrapper_sendpage : NULL; +#endif + p->ops.get_unmapped_area = fp->f_op->get_unmapped_area ? ksu_wrapper_get_unmapped_area : NULL; + p->ops.check_flags = fp->f_op->check_flags; + p->ops.flock = fp->f_op->flock ? ksu_wrapper_flock : NULL; + p->ops.splice_write = fp->f_op->splice_write ? ksu_wrapper_splice_write : NULL; + p->ops.splice_read = fp->f_op->splice_read ? ksu_wrapper_splice_read : NULL; + p->ops.setlease = fp->f_op->setlease ? ksu_wrapper_setlease : NULL; + p->ops.fallocate = fp->f_op->fallocate ? ksu_wrapper_fallocate : NULL; + p->ops.show_fdinfo = fp->f_op->show_fdinfo ? ksu_wrapper_show_fdinfo : NULL; + p->ops.copy_file_range = fp->f_op->copy_file_range ? ksu_wrapper_copy_file_range : NULL; + p->ops.remap_file_range = fp->f_op->remap_file_range ? ksu_wrapper_remap_file_range : NULL; + p->ops.fadvise = fp->f_op->fadvise ? ksu_wrapper_fadvise : NULL; + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0) + p->ops.splice_eof = fp->f_op->splice_eof ? ksu_wrapper_splice_eof : NULL; +#endif + + return p; +} + +void ksu_delete_file_wrapper(struct ksu_file_wrapper* data) { + fput((struct file*) data->orig); + kfree(data); +} \ No newline at end of file diff --git a/kernel/file_wrapper.h b/kernel/file_wrapper.h new file mode 100644 index 0000000..421e20e --- /dev/null +++ b/kernel/file_wrapper.h @@ -0,0 +1,14 @@ +#ifndef KSU_FILE_WRAPPER_H +#define KSU_FILE_WRAPPER_H + +#include +#include + +struct ksu_file_wrapper { + struct file* orig; + struct file_operations ops; +}; + +struct ksu_file_wrapper* ksu_create_file_wrapper(struct file* fp); +void ksu_delete_file_wrapper(struct ksu_file_wrapper* data); +#endif // KSU_FILE_WRAPPER_H \ No newline at end of file diff --git a/kernel/include/ksu_hook.h b/kernel/include/ksu_hook.h deleted file mode 100644 index ea0b04d..0000000 --- a/kernel/include/ksu_hook.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef __KSU_H_KSHOOK -#define __KSU_H_KSHOOK - -#include -#include - -// For sucompat - -int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, - int *flags); - -int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags); - -// For ksud - -int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr, - size_t *count_ptr, loff_t **pos); - -// For ksud and sucompat - -int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv, - void *envp, int *flags); - -// For volume button -int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, - int *value); - -#endif diff --git a/kernel/kernel_compat.c b/kernel/kernel_compat.c deleted file mode 100644 index 3fb7ecd..0000000 --- a/kernel/kernel_compat.c +++ /dev/null @@ -1,94 +0,0 @@ -#include -#include -#include -#include -#include -#include "klog.h" // IWYU pragma: keep -#include "kernel_compat.h" - -extern struct task_struct init_task; - -// mnt_ns context switch for environment that android_init->nsproxy->mnt_ns != init_task.nsproxy->mnt_ns, such as WSA -struct ksu_ns_fs_saved { - struct nsproxy *ns; - struct fs_struct *fs; -}; - -static void ksu_save_ns_fs(struct ksu_ns_fs_saved *ns_fs_saved) -{ - ns_fs_saved->ns = current->nsproxy; - ns_fs_saved->fs = current->fs; -} - -static void ksu_load_ns_fs(struct ksu_ns_fs_saved *ns_fs_saved) -{ - current->nsproxy = ns_fs_saved->ns; - current->fs = ns_fs_saved->fs; -} - -static bool android_context_saved_checked = false; -static bool android_context_saved_enabled = false; -static struct ksu_ns_fs_saved android_context_saved; - -void ksu_android_ns_fs_check() -{ - if (android_context_saved_checked) - return; - android_context_saved_checked = true; - task_lock(current); - if (current->nsproxy && current->fs && - current->nsproxy->mnt_ns != init_task.nsproxy->mnt_ns) { - android_context_saved_enabled = true; -#ifdef CONFIG_KSU_DEBUG - pr_info("android context saved enabled due to init mnt_ns(%p) != android mnt_ns(%p)\n", - current->nsproxy->mnt_ns, init_task.nsproxy->mnt_ns); -#endif - ksu_save_ns_fs(&android_context_saved); - } else { - pr_info("android context saved disabled\n"); - } - task_unlock(current); -} - -struct file *ksu_filp_open_compat(const char *filename, int flags, umode_t mode) -{ - // switch mnt_ns even if current is not wq_worker, to ensure what we open is the correct file in android mnt_ns, rather than user created mnt_ns - struct ksu_ns_fs_saved saved; - if (android_context_saved_enabled) { -#ifdef CONFIG_KSU_DEBUG - pr_info("start switch current nsproxy and fs to android context\n"); -#endif - task_lock(current); - ksu_save_ns_fs(&saved); - ksu_load_ns_fs(&android_context_saved); - task_unlock(current); - } - struct file *fp = filp_open(filename, flags, mode); - if (android_context_saved_enabled) { - task_lock(current); - ksu_load_ns_fs(&saved); - task_unlock(current); -#ifdef CONFIG_KSU_DEBUG - pr_info("switch current nsproxy and fs back to saved successfully\n"); -#endif - } - return fp; -} - -ssize_t ksu_kernel_read_compat(struct file *p, void *buf, size_t count, - loff_t *pos) -{ - return kernel_read(p, buf, count, pos); -} - -ssize_t ksu_kernel_write_compat(struct file *p, const void *buf, size_t count, - loff_t *pos) -{ - return kernel_write(p, buf, count, pos); -} - -long ksu_strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr, - long count) -{ - return strncpy_from_user_nofault(dst, unsafe_addr, count); -} diff --git a/kernel/kernel_compat.h b/kernel/kernel_compat.h index ed02888..14e1cb2 100644 --- a/kernel/kernel_compat.h +++ b/kernel/kernel_compat.h @@ -3,63 +3,7 @@ #include #include -#include "ss/policydb.h" -#include "linux/key.h" -/** - * list_count_nodes - count the number of nodes in a list - * @head: the head of the list - * - * This function iterates over the list starting from @head and counts - * the number of nodes in the list. It does not modify the list. - * - * Context: Any context. The function is safe to call in any context, - * including interrupt context, as it does not sleep or allocate - * memory. - * - * Return: the number of nodes in the list (excluding the head) - */ -#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0) -static inline __maybe_unused size_t list_count_nodes(const struct list_head *head) -{ - const struct list_head *pos; - size_t count = 0; - - if (!head) - return 0; - - list_for_each(pos, head) - count++; - - return count; -} -#endif - -/* - * Adapt to Huawei HISI kernel without affecting other kernels , - * Huawei Hisi Kernel EBITMAP Enable or Disable Flag , - * From ss/ebitmap.h - */ -#if (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 9, 0)) && \ - (LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0)) || \ - (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0)) && \ - (LINUX_VERSION_CODE < KERNEL_VERSION(4, 15, 0)) -#ifdef HISI_SELINUX_EBITMAP_RO -#define CONFIG_IS_HW_HISI -#endif -#endif - -extern long ksu_strncpy_from_user_nofault(char *dst, - const void __user *unsafe_addr, - long count); - -extern void ksu_android_ns_fs_check(); -extern struct file *ksu_filp_open_compat(const char *filename, int flags, - umode_t mode); -extern ssize_t ksu_kernel_read_compat(struct file *p, void *buf, size_t count, - loff_t *pos); -extern ssize_t ksu_kernel_write_compat(struct file *p, const void *buf, - size_t count, loff_t *pos); /* * ksu_copy_from_user_retry * try nofault copy first, if it fails, try with plain @@ -67,14 +11,14 @@ extern ssize_t ksu_kernel_write_compat(struct file *p, const void *buf, * 0 = success */ static long ksu_copy_from_user_retry(void *to, - const void __user *from, unsigned long count) + const void __user *from, unsigned long count) { - long ret = copy_from_user_nofault(to, from, count); - if (likely(!ret)) - return ret; + long ret = copy_from_user_nofault(to, from, count); + if (likely(!ret)) + return ret; - // we faulted! fallback to slow path - return copy_from_user(to, from, count); + // we faulted! fallback to slow path + return copy_from_user(to, from, count); } #endif diff --git a/kernel/kernel_umount.c b/kernel/kernel_umount.c new file mode 100644 index 0000000..4086057 --- /dev/null +++ b/kernel/kernel_umount.c @@ -0,0 +1,175 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kernel_umount.h" +#include "klog.h" // IWYU pragma: keep +#include "allowlist.h" +#include "selinux/selinux.h" +#include "feature.h" +#include "ksud.h" + +#include "umount_manager.h" +#include "sulog.h" + +static bool ksu_kernel_umount_enabled = true; + +static int kernel_umount_feature_get(u64 *value) +{ + *value = ksu_kernel_umount_enabled ? 1 : 0; + return 0; +} + +static int kernel_umount_feature_set(u64 value) +{ + bool enable = value != 0; + ksu_kernel_umount_enabled = enable; + pr_info("kernel_umount: set to %d\n", enable); + return 0; +} + +static const struct ksu_feature_handler kernel_umount_handler = { + .feature_id = KSU_FEATURE_KERNEL_UMOUNT, + .name = "kernel_umount", + .get_handler = kernel_umount_feature_get, + .set_handler = kernel_umount_feature_set, +}; + +extern int path_umount(struct path *path, int flags); + +static void ksu_umount_mnt(struct path *path, int flags) +{ + int err = path_umount(path, flags); + if (err) { + pr_info("umount %s failed: %d\n", path->dentry->d_iname, err); + } +} + +void try_umount(const char *mnt, int flags) +{ + struct path path; + int err = kern_path(mnt, 0, &path); + if (err) { + return; + } + + if (path.dentry != path.mnt->mnt_root) { + // it is not root mountpoint, maybe umounted by others already. + path_put(&path); + return; + } + + ksu_umount_mnt(&path, flags); +} + +struct umount_tw { + struct callback_head cb; + const struct cred *old_cred; +}; + +static void umount_tw_func(struct callback_head *cb) +{ + struct umount_tw *tw = container_of(cb, struct umount_tw, cb); + const struct cred *saved = NULL; + if (tw->old_cred) { + saved = override_creds(tw->old_cred); + } + + struct mount_entry *entry; + down_read(&mount_list_lock); + list_for_each_entry(entry, &mount_list, list) { + pr_info("%s: unmounting: %s flags 0x%x\n", __func__, entry->umountable, entry->flags); + try_umount(entry->umountable, entry->flags); + } + up_read(&mount_list_lock); + + ksu_umount_manager_execute_all(tw->old_cred); + + if (saved) + revert_creds(saved); + + if (tw->old_cred) + put_cred(tw->old_cred); + + kfree(tw); +} + +int ksu_handle_umount(uid_t old_uid, uid_t new_uid) +{ + struct umount_tw *tw; + + // this hook is used for umounting overlayfs for some uid, if there isn't any module mounted, just ignore it! + if (!ksu_module_mounted) { + return 0; + } + + if (!ksu_kernel_umount_enabled) { + return 0; + } + + // FIXME: isolated process which directly forks from zygote is not handled + if (!is_appuid(new_uid)) { + return 0; + } + + if (!ksu_uid_should_umount(new_uid)) { + return 0; + } + + // check old process's selinux context, if it is not zygote, ignore it! + // because some su apps may setuid to untrusted_app but they are in global mount namespace + // when we umount for such process, that is a disaster! + bool is_zygote_child = is_zygote(get_current_cred()); + if (!is_zygote_child) { + pr_info("handle umount ignore non zygote child: %d\n", current->pid); + return 0; + } +#if __SULOG_GATE + ksu_sulog_report_syscall(new_uid, NULL, "setuid", NULL); +#endif + // umount the target mnt + pr_info("handle umount for uid: %d, pid: %d\n", new_uid, current->pid); + + tw = kzalloc(sizeof(*tw), GFP_ATOMIC); + if (!tw) + return 0; + + tw->old_cred = get_current_cred(); + tw->cb.func = umount_tw_func; + + int err = task_work_add(current, &tw->cb, TWA_RESUME); + if (err) { + if (tw->old_cred) { + put_cred(tw->old_cred); + } + kfree(tw); + pr_warn("unmount add task_work failed\n"); + } + + return 0; +} + +void ksu_kernel_umount_init(void) +{ + int rc = 0; + rc = ksu_umount_manager_init(); + if (rc) { + pr_err("Failed to initialize umount manager: %d\n", rc); + } + if (ksu_register_feature_handler(&kernel_umount_handler)) { + pr_err("Failed to register kernel_umount feature handler\n"); + } +} + +void ksu_kernel_umount_exit(void) +{ + ksu_unregister_feature_handler(KSU_FEATURE_KERNEL_UMOUNT); +} \ No newline at end of file diff --git a/kernel/kernel_umount.h b/kernel/kernel_umount.h new file mode 100644 index 0000000..65da620 --- /dev/null +++ b/kernel/kernel_umount.h @@ -0,0 +1,25 @@ +#ifndef __KSU_H_KERNEL_UMOUNT +#define __KSU_H_KERNEL_UMOUNT + +#include +#include +#include + +void ksu_kernel_umount_init(void); +void ksu_kernel_umount_exit(void); + +void try_umount(const char *mnt, int flags); + +// Handler function to be called from setresuid hook +int ksu_handle_umount(uid_t old_uid, uid_t new_uid); + +// for the umount list +struct mount_entry { + char *umountable; + unsigned int flags; + struct list_head list; +}; +extern struct list_head mount_list; +extern struct rw_semaphore mount_list_lock; + +#endif diff --git a/kernel/kpm/compact.c b/kernel/kpm/compact.c index 8af317c..5791db4 100644 --- a/kernel/kpm/compact.c +++ b/kernel/kpm/compact.c @@ -31,7 +31,7 @@ static int sukisu_is_su_allow_uid(uid_t uid) { - return ksu_is_allow_uid(uid) ? 1 : 0; + return ksu_is_allow_uid_for_current(uid) ? 1 : 0; } static int sukisu_get_ap_mod_exclude(uid_t uid) diff --git a/kernel/kpm/kpm.c b/kernel/kpm/kpm.c index 32a58ce..e31384b 100644 --- a/kernel/kpm/kpm.c +++ b/kernel/kpm/kpm.c @@ -9,13 +9,10 @@ * 并参照KernelPatch的标准KPM格式实现加载和控制 */ -#include -#include #include #include #include #include -#include #include #include #include @@ -25,26 +22,25 @@ #include #include #include -#include #include #include #include #include -#include #include -#include #include #include #include #include #include -#include #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,0,0) && defined(CONFIG_MODULES) #include #endif #include "kpm.h" #include "compact.h" +#define KPM_NAME_LEN 32 +#define KPM_ARGS_LEN 1024 + #ifndef NO_OPTIMIZE #if defined(__GNUC__) && !defined(__clang__) #define NO_OPTIMIZE __attribute__((optimize("O0"))) @@ -56,156 +52,231 @@ #endif noinline NO_OPTIMIZE void sukisu_kpm_load_module_path(const char *path, - const char *args, void *ptr, void __user *result) + const char *args, void *ptr, int *result) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_load_module_path). " + pr_info("kpm: Stub function called (sukisu_kpm_load_module_path). " "path=%s args=%s ptr=%p\n", path, args, ptr); __asm__ volatile("nop"); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_load_module_path); noinline NO_OPTIMIZE void sukisu_kpm_unload_module(const char *name, - void *ptr, void __user *result) + void *ptr, int *result) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_unload_module). " + pr_info("kpm: Stub function called (sukisu_kpm_unload_module). " "name=%s ptr=%p\n", name, ptr); __asm__ volatile("nop"); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_unload_module); -noinline NO_OPTIMIZE void sukisu_kpm_num(void __user *result) +noinline NO_OPTIMIZE void sukisu_kpm_num(int *result) { - int res = 0; - - printk("KPM: Stub function called (sukisu_kpm_num).\n"); + pr_info("kpm: Stub function called (sukisu_kpm_num).\n"); __asm__ volatile("nop"); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_num); -noinline NO_OPTIMIZE void sukisu_kpm_info(const char *name, void __user *out, - void __user *result) +noinline NO_OPTIMIZE void sukisu_kpm_info(const char *name, char *buf, int bufferSize, + int *size) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_info). " - "name=%s buffer=%p\n", name, out); + pr_info("kpm: Stub function called (sukisu_kpm_info). " + "name=%s buffer=%p\n", name, buf); __asm__ volatile("nop"); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_info); -noinline NO_OPTIMIZE void sukisu_kpm_list(void __user *out, unsigned int bufferSize, - void __user *result) +noinline NO_OPTIMIZE void sukisu_kpm_list(void *out, int bufferSize, + int *result) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_list). " + pr_info("kpm: Stub function called (sukisu_kpm_list). " "buffer=%p size=%d\n", out, bufferSize); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_list); -noinline NO_OPTIMIZE void sukisu_kpm_control(void __user *name, void __user *args, - void __user *result) +noinline NO_OPTIMIZE void sukisu_kpm_control(const char *name, const char *args, long arg_len, + int *result) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_control). " - "name=%p args=%p\n", name, args); + pr_info("kpm: Stub function called (sukisu_kpm_control). " + "name=%p args=%p arg_len=%ld\n", name, args, arg_len); __asm__ volatile("nop"); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_control); -noinline NO_OPTIMIZE void sukisu_kpm_version(void __user *out, unsigned int bufferSize, - void __user *result) +noinline NO_OPTIMIZE void sukisu_kpm_version(char *buf, int bufferSize) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_version). " - "buffer=%p size=%d\n", out, bufferSize); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); + pr_info("kpm: Stub function called (sukisu_kpm_version). " + "buffer=%p\n", buf); } EXPORT_SYMBOL(sukisu_kpm_version); -noinline int sukisu_handle_kpm(unsigned long arg2, unsigned long arg3, unsigned long arg4, - unsigned long arg5) +noinline int sukisu_handle_kpm(unsigned long control_code, unsigned long arg1, unsigned long arg2, + unsigned long result_code) { - if (arg2 == SUKISU_KPM_LOAD) { - char kernel_load_path[256] = { 0 }; - char kernel_args_buffer[256] = { 0 }; + int res = -1; + if (control_code == SUKISU_KPM_LOAD) { + char kernel_load_path[256]; + char kernel_args_buffer[256]; - if (arg3 == 0) - return -1; + if (arg1 == 0) { + res = -EINVAL; + goto exit; + } + + if (!access_ok(arg1, 255)) { + goto invalid_arg; + } - strncpy_from_user((char *)&kernel_load_path, (const char __user *)arg3, 255); + strncpy_from_user((char *)&kernel_load_path, (const char *)arg1, 255); - if (arg4 != 0) - strncpy_from_user((char *)&kernel_args_buffer, (const char __user *)arg4, 255); + if (arg2 != 0) { + if (!access_ok(arg2, 255)) { + goto invalid_arg; + } + + strncpy_from_user((char *)&kernel_args_buffer, (const char *)arg2, 255); + } sukisu_kpm_load_module_path((const char *)&kernel_load_path, - (const char *)&kernel_args_buffer, NULL, (void __user *)arg5); - } else if (arg2 == SUKISU_KPM_UNLOAD) { - char kernel_name_buffer[256] = { 0 }; + (const char *)&kernel_args_buffer, NULL, &res); + } else if (control_code == SUKISU_KPM_UNLOAD) { + char kernel_name_buffer[256]; - if (arg3 == 0) - return -1; - - strncpy_from_user((char *)&kernel_name_buffer, (const char __user *)arg3, 255); - - sukisu_kpm_unload_module((const char *)&kernel_name_buffer, NULL, - (void __user *)arg5); - } else if (arg2 == SUKISU_KPM_NUM) { - sukisu_kpm_num((void __user *)arg5); - } else if (arg2 == SUKISU_KPM_INFO) { - char kernel_name_buffer[256] = { 0 }; + if (arg1 == 0) { + res = -EINVAL; + goto exit; + } - if (arg3 == 0 || arg4 == 0) - return -1; + if (!access_ok(arg1, sizeof(kernel_name_buffer))) { + goto invalid_arg; + } - strncpy_from_user((char *)&kernel_name_buffer, (const char __user *)arg3, 255); + strncpy_from_user((char *)&kernel_name_buffer, (const char *)arg1, sizeof(kernel_name_buffer)); - sukisu_kpm_info((const char *)&kernel_name_buffer, (char __user *)arg4, - (void __user *)arg5); - } else if (arg2 == SUKISU_KPM_LIST) { - sukisu_kpm_list((char __user *)arg3, (unsigned int)arg4, (void __user *)arg5); - } else if (arg2 == SUKISU_KPM_CONTROL) { - sukisu_kpm_control((char __user *)arg3, (char __user *)arg4, (void __user *)arg5); - } else if (arg2 == SUKISU_KPM_VERSION) { - sukisu_kpm_version((char __user *)arg3, (unsigned int)arg4, (void __user *)arg5); + sukisu_kpm_unload_module((const char *)&kernel_name_buffer, NULL, &res); + } else if (control_code == SUKISU_KPM_NUM) { + sukisu_kpm_num(&res); + } else if (control_code == SUKISU_KPM_INFO) { + char kernel_name_buffer[256]; + char buf[256]; + int size; + + if (arg1 == 0 || arg2 == 0) { + res = -EINVAL; + goto exit; + } + + if (!access_ok(arg1, sizeof(kernel_name_buffer))) { + goto invalid_arg; + } + + strncpy_from_user((char *)&kernel_name_buffer, (const char __user *)arg1, sizeof(kernel_name_buffer)); + + sukisu_kpm_info((const char *)&kernel_name_buffer, (char *)&buf, sizeof(buf), &size); + + if (!access_ok(arg2, size)) { + goto invalid_arg; + } + + res = copy_to_user(arg2, &buf, size); + + } else if (control_code == SUKISU_KPM_LIST) { + char buf[1024]; + int len = (int) arg2; + + if (len <= 0) { + res = -EINVAL; + goto exit; + } + + if (!access_ok(arg2, len)) { + goto invalid_arg; + } + + sukisu_kpm_list((char *)&buf, sizeof(buf), &res); + + if (res > len) { + res = -ENOBUFS; + goto exit; + } + + if (copy_to_user(arg1, &buf, len) != 0) + pr_info("kpm: Copy to user failed."); + + } else if (control_code == SUKISU_KPM_CONTROL) { + char kpm_name[KPM_NAME_LEN] = { 0 }; + char kpm_args[KPM_ARGS_LEN] = { 0 }; + + if (!access_ok(arg1, sizeof(kpm_name))) { + goto invalid_arg; + } + + if (!access_ok(arg2, sizeof(kpm_args))) { + goto invalid_arg; + } + + long name_len = strncpy_from_user((char *)&kpm_name, (const char __user *)arg1, sizeof(kpm_name)); + if (name_len <= 0) { + res = -EINVAL; + goto exit; + } + + long arg_len = strncpy_from_user((char *)&kpm_args, (const char __user *)arg2, sizeof(kpm_args)); + + sukisu_kpm_control((const char *)&kpm_name, (const char *)&kpm_args, arg_len, &res); + + } else if (control_code == SUKISU_KPM_VERSION) { + char buffer[256] = {0}; + + sukisu_kpm_version((char*) &buffer, sizeof(buffer)); + + unsigned int outlen = (unsigned int) arg2; + int len = strlen(buffer); + if (len >= outlen) len = outlen - 1; + + res = copy_to_user(arg1, &buffer, len + 1); } + +exit: + if (copy_to_user(result_code, &res, sizeof(res)) != 0) + pr_info("kpm: Copy to user failed."); return 0; +invalid_arg: + pr_err("kpm: invalid pointer detected! arg1: %px arg2: %px\n", (void *)arg1, (void *)arg2); + res = -EFAULT; + goto exit; } EXPORT_SYMBOL(sukisu_handle_kpm); -int sukisu_is_kpm_control_code(unsigned long arg2) { - return (arg2 >= CMD_KPM_CONTROL && - arg2 <= CMD_KPM_CONTROL_MAX) ? 1 : 0; +int sukisu_is_kpm_control_code(unsigned long control_code) { + return (control_code >= CMD_KPM_CONTROL && + control_code <= CMD_KPM_CONTROL_MAX) ? 1 : 0; } + +int do_kpm(void __user *arg) +{ + struct ksu_kpm_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("kpm: copy_from_user failed\n"); + return -EFAULT; + } + + if (!access_ok(cmd.control_code, sizeof(int))) { + pr_err("kpm: invalid control_code pointer %px\n", (void *)cmd.control_code); + return -EFAULT; + } + + if (!access_ok(cmd.result_code, sizeof(int))) { + pr_err("kpm: invalid result_code pointer %px\n", (void *)cmd.result_code); + return -EFAULT; + } + + return sukisu_handle_kpm(cmd.control_code, cmd.arg1, cmd.arg2, cmd.result_code); +} + diff --git a/kernel/kpm/kpm.h b/kernel/kpm/kpm.h index e8349d3..4fdcc20 100644 --- a/kernel/kpm/kpm.h +++ b/kernel/kpm/kpm.h @@ -1,58 +1,70 @@ #ifndef __SUKISU_KPM_H #define __SUKISU_KPM_H -extern int sukisu_handle_kpm(unsigned long arg2, unsigned long arg3, unsigned long arg4, - unsigned long arg5); -extern int sukisu_is_kpm_control_code(unsigned long arg2); +#include +#include + +struct ksu_kpm_cmd { + __aligned_u64 __user control_code; + __aligned_u64 __user arg1; + __aligned_u64 __user arg2; + __aligned_u64 __user result_code; +}; + +int sukisu_handle_kpm(unsigned long control_code, unsigned long arg3, unsigned long arg4, unsigned long result_code); +int sukisu_is_kpm_control_code(unsigned long control_code); +int do_kpm(void __user *arg); + +#define KSU_IOCTL_KPM _IOC(_IOC_READ|_IOC_WRITE, 'K', 200, 0) /* KPM Control Code */ -#define CMD_KPM_CONTROL 28 -#define CMD_KPM_CONTROL_MAX 35 +#define CMD_KPM_CONTROL 1 +#define CMD_KPM_CONTROL_MAX 10 /* Control Code */ /* - * prctl(xxx, 28, "PATH", "ARGS") + * prctl(xxx, 1, "PATH", "ARGS") * success return 0, error return -N */ -#define SUKISU_KPM_LOAD 28 +#define SUKISU_KPM_LOAD 1 /* - * prctl(xxx, 29, "NAME") + * prctl(xxx, 2, "NAME") * success return 0, error return -N */ -#define SUKISU_KPM_UNLOAD 29 +#define SUKISU_KPM_UNLOAD 2 /* - * num = prctl(xxx, 30) + * num = prctl(xxx, 3) * error return -N * success return +num or 0 */ -#define SUKISU_KPM_NUM 30 +#define SUKISU_KPM_NUM 3 /* - * prctl(xxx, 31, Buffer, BufferSize) + * prctl(xxx, 4, Buffer, BufferSize) * success return +out, error return -N */ -#define SUKISU_KPM_LIST 31 +#define SUKISU_KPM_LIST 4 /* - * prctl(xxx, 32, "NAME", Buffer[256]) + * prctl(xxx, 5, "NAME", Buffer[256]) * success return +out, error return -N */ -#define SUKISU_KPM_INFO 32 +#define SUKISU_KPM_INFO 5 /* - * prctl(xxx, 33, "NAME", "ARGS") + * prctl(xxx, 6, "NAME", "ARGS") * success return KPM's result value * error return -N */ -#define SUKISU_KPM_CONTROL 33 +#define SUKISU_KPM_CONTROL 6 /* - * prctl(xxx, 34, buffer, bufferSize) + * prctl(xxx, 7, buffer, bufferSize) * success return KPM's result value * error return -N */ -#define SUKISU_KPM_VERSION 34 +#define SUKISU_KPM_VERSION 7 #endif diff --git a/kernel/ksu.c b/kernel/ksu.c index 37a1754..850050c 100644 --- a/kernel/ksu.c +++ b/kernel/ksu.c @@ -3,89 +3,103 @@ #include #include #include +#include #include "allowlist.h" -#include "arch.h" -#include "core_hook.h" +#include "feature.h" #include "klog.h" // IWYU pragma: keep -#include "ksu.h" #include "throne_tracker.h" +#include "syscall_hook_manager.h" +#include "ksud.h" +#include "supercalls.h" + +#include "sulog.h" +#include "throne_comm.h" +#include "dynamic_manager.h" static struct workqueue_struct *ksu_workqueue; bool ksu_queue_work(struct work_struct *work) { - return queue_work(ksu_workqueue, work); + return queue_work(ksu_workqueue, work); } -extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, - void *argv, void *envp, int *flags); +void sukisu_custom_config_init(void) +{ +} -extern void ksu_sucompat_init(); -extern void ksu_sucompat_exit(); -extern void ksu_ksud_init(); -extern void ksu_ksud_exit(); -#ifdef CONFIG_KSU_TRACEPOINT_HOOK -extern void ksu_trace_register(); -extern void ksu_trace_unregister(); +void sukisu_custom_config_exit(void) +{ + ksu_uid_exit(); + ksu_throne_comm_exit(); + ksu_dynamic_manager_exit(); +#if __SULOG_GATE + ksu_sulog_exit(); #endif +} int __init kernelsu_init(void) { #ifdef CONFIG_KSU_DEBUG - pr_alert("*************************************************************"); - pr_alert("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE **"); - pr_alert("** **"); - pr_alert("** You are running KernelSU in DEBUG mode **"); - pr_alert("** **"); - pr_alert("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE **"); - pr_alert("*************************************************************"); + pr_alert("*************************************************************"); + pr_alert("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE **"); + pr_alert("** **"); + pr_alert("** You are running KernelSU in DEBUG mode **"); + pr_alert("** **"); + pr_alert("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE **"); + pr_alert("*************************************************************"); #endif - ksu_core_init(); + ksu_feature_init(); - ksu_workqueue = alloc_ordered_workqueue("kernelsu_work_queue", 0); + ksu_supercalls_init(); - ksu_allowlist_init(); + sukisu_custom_config_init(); - ksu_throne_tracker_init(); -#ifdef CONFIG_KSU_KPROBES_HOOK - ksu_sucompat_init(); - ksu_ksud_init(); + ksu_syscall_hook_manager_init(); + + ksu_workqueue = alloc_ordered_workqueue("kernelsu_work_queue", 0); + + ksu_allowlist_init(); + + ksu_throne_tracker_init(); + +#ifdef KSU_KPROBES_HOOK + ksu_ksud_init(); #else - pr_alert("KPROBES is disabled, KernelSU may not work, please check https://kernelsu.org/guide/how-to-integrate-for-non-gki.html"); -#endif - -#ifdef CONFIG_KSU_TRACEPOINT_HOOK - ksu_trace_register(); + pr_alert("KPROBES is disabled, KernelSU may not work, please check https://kernelsu.org/guide/how-to-integrate-for-non-gki.html"); #endif #ifdef MODULE #ifndef CONFIG_KSU_DEBUG - kobject_del(&THIS_MODULE->mkobj.kobj); + kobject_del(&THIS_MODULE->mkobj.kobj); #endif #endif - return 0; + return 0; } +extern void ksu_observer_exit(void); void kernelsu_exit(void) { - ksu_allowlist_exit(); + ksu_allowlist_exit(); - ksu_throne_tracker_exit(); + ksu_observer_exit(); - destroy_workqueue(ksu_workqueue); + ksu_throne_tracker_exit(); -#ifdef CONFIG_KSU_KPROBES_HOOK - ksu_ksud_exit(); - ksu_sucompat_exit(); + destroy_workqueue(ksu_workqueue); + +#ifdef KSU_KPROBES_HOOK + ksu_ksud_exit(); #endif -#ifdef CONFIG_KSU_TRACEPOINT_HOOK - ksu_trace_unregister(); -#endif + ksu_syscall_hook_manager_exit(); - ksu_core_exit(); + sukisu_custom_config_exit(); + + ksu_supercalls_exit(); + + ksu_feature_exit(); } module_init(kernelsu_init); @@ -94,4 +108,9 @@ module_exit(kernelsu_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("weishu"); MODULE_DESCRIPTION("Android KernelSU"); + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) +MODULE_IMPORT_NS("VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver"); +#else MODULE_IMPORT_NS(VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver); +#endif diff --git a/kernel/ksu.h b/kernel/ksu.h index 5320180..93750af 100644 --- a/kernel/ksu.h +++ b/kernel/ksu.h @@ -7,40 +7,12 @@ #define KERNEL_SU_VERSION KSU_VERSION #define KERNEL_SU_OPTION 0xDEADBEEF -#define CMD_GRANT_ROOT 0 -#define CMD_BECOME_MANAGER 1 -#define CMD_GET_VERSION 2 -#define CMD_ALLOW_SU 3 -#define CMD_DENY_SU 4 -#define CMD_GET_ALLOW_LIST 5 -#define CMD_GET_DENY_LIST 6 -#define CMD_REPORT_EVENT 7 -#define CMD_SET_SEPOLICY 8 -#define CMD_CHECK_SAFEMODE 9 -#define CMD_GET_APP_PROFILE 10 -#define CMD_SET_APP_PROFILE 11 -#define CMD_UID_GRANTED_ROOT 12 -#define CMD_UID_SHOULD_UMOUNT 13 -#define CMD_IS_SU_ENABLED 14 -#define CMD_ENABLE_SU 15 - -#define CMD_GET_FULL_VERSION 0xC0FFEE1A - -#define CMD_ENABLE_KPM 100 -#define CMD_HOOK_TYPE 101 -#define CMD_DYNAMIC_MANAGER 103 -#define CMD_GET_MANAGERS 104 +extern bool ksu_uid_scanner_enabled; #define EVENT_POST_FS_DATA 1 #define EVENT_BOOT_COMPLETED 2 #define EVENT_MODULE_MOUNTED 3 -#define KSU_APP_PROFILE_VER 2 -#define KSU_MAX_PACKAGE_NAME 256 -// NGROUPS_MAX for Linux is 65535 generally, but we only supports 32 groups. -#define KSU_MAX_GROUPS 32 -#define KSU_SELINUX_DOMAIN 64 - // SukiSU Ultra kernel su version full strings #ifndef KSU_VERSION_FULL #define KSU_VERSION_FULL "v3.x-00000000@unknown" @@ -51,6 +23,10 @@ #define DYNAMIC_MANAGER_OP_GET 1 #define DYNAMIC_MANAGER_OP_CLEAR 2 +#define UID_SCANNER_OP_GET_STATUS 0 +#define UID_SCANNER_OP_TOGGLE 1 +#define UID_SCANNER_OP_CLEAR_ENV 2 + struct dynamic_manager_user_config { unsigned int operation; unsigned int size; @@ -65,68 +41,22 @@ struct manager_list_info { } managers[2]; }; -struct root_profile { - int32_t uid; - int32_t gid; - - int32_t groups_count; - int32_t groups[KSU_MAX_GROUPS]; - - // kernel_cap_t is u32[2] for capabilities v3 - struct { - u64 effective; - u64 permitted; - u64 inheritable; - } capabilities; - - char selinux_domain[KSU_SELINUX_DOMAIN]; - - int32_t namespaces; -}; - -struct non_root_profile { - bool umount_modules; -}; - -struct app_profile { - // It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this. - u32 version; - - // this is usually the package of the app, but can be other value for special apps - char key[KSU_MAX_PACKAGE_NAME]; - int32_t current_uid; - bool allow_su; - - union { - struct { - bool use_default; - char template_name[KSU_MAX_PACKAGE_NAME]; - - struct root_profile profile; - } rp_config; - - struct { - bool use_default; - - struct non_root_profile profile; - } nrp_config; - }; -}; - bool ksu_queue_work(struct work_struct *work); +#if 0 static inline int startswith(char *s, char *prefix) { - return strncmp(s, prefix, strlen(prefix)); + return strncmp(s, prefix, strlen(prefix)); } static inline int endswith(const char *s, const char *t) { - size_t slen = strlen(s); - size_t tlen = strlen(t); - if (tlen > slen) - return 1; - return strcmp(s + slen - tlen, t); + size_t slen = strlen(s); + size_t tlen = strlen(t); + if (tlen > slen) + return 1; + return strcmp(s + slen - tlen, t); } +#endif #endif diff --git a/kernel/ksu_trace.c b/kernel/ksu_trace.c deleted file mode 100644 index 5acf092..0000000 --- a/kernel/ksu_trace.c +++ /dev/null @@ -1,69 +0,0 @@ -#include "ksu_trace.h" - - -// extern kernelsu functions -extern bool ksu_vfs_read_hook __read_mostly; -extern bool ksu_input_hook __read_mostly; -extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags); -extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, int *flags); -extern int ksu_handle_sys_read(unsigned int fd, char __user **buf_ptr, size_t *count_ptr); -extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags); -extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value); -// end kernelsu functions - - -// tracepoint callback functions -void ksu_trace_execveat_sucompat_hook_callback(void *data, int *fd, struct filename **filename_ptr, - void *argv, void *envp, int *flags) -{ - ksu_handle_execveat_sucompat(fd, filename_ptr, argv, envp, flags); -} - -void ksu_trace_faccessat_hook_callback(void *data, int *dfd, const char __user **filename_user, - int *mode, int *flags) -{ - ksu_handle_faccessat(dfd, filename_user, mode, flags); -} - -void ksu_trace_sys_read_hook_callback(void *data, unsigned int fd, char __user **buf_ptr, - size_t *count_ptr) -{ - if (unlikely(ksu_vfs_read_hook)) - ksu_handle_sys_read(fd, buf_ptr, count_ptr); -} - -void ksu_trace_stat_hook_callback(void *data, int *dfd, const char __user **filename_user, - int *flags) -{ - ksu_handle_stat(dfd, filename_user, flags); -} - -void ksu_trace_input_hook_callback(void *data, unsigned int *type, unsigned int *code, - int *value) -{ - if (unlikely(ksu_input_hook)) - ksu_handle_input_handle_event(type, code, value); -} - -// end tracepoint callback functions - - -// register tracepoint callback functions -void ksu_trace_register(void) -{ - register_trace_ksu_trace_execveat_sucompat_hook(ksu_trace_execveat_sucompat_hook_callback, NULL); - register_trace_ksu_trace_faccessat_hook(ksu_trace_faccessat_hook_callback, NULL); - register_trace_ksu_trace_sys_read_hook(ksu_trace_sys_read_hook_callback, NULL); - register_trace_ksu_trace_stat_hook(ksu_trace_stat_hook_callback, NULL); - register_trace_ksu_trace_input_hook(ksu_trace_input_hook_callback, NULL); -} - -// unregister tracepoint callback functions -void ksu_trace_unregister(void) -{ - unregister_trace_ksu_trace_execveat_sucompat_hook(ksu_trace_execveat_sucompat_hook_callback, NULL); - unregister_trace_ksu_trace_faccessat_hook(ksu_trace_faccessat_hook_callback, NULL); - unregister_trace_ksu_trace_sys_read_hook(ksu_trace_sys_read_hook_callback, NULL); - unregister_trace_ksu_trace_stat_hook(ksu_trace_stat_hook_callback, NULL); - unregister_trace_ksu_trace_input_hook(ksu_trace_input_hook_callback, NULL); -} diff --git a/kernel/ksu_trace.h b/kernel/ksu_trace.h deleted file mode 100644 index dc5394b..0000000 --- a/kernel/ksu_trace.h +++ /dev/null @@ -1,37 +0,0 @@ -#undef TRACE_SYSTEM -#define TRACE_SYSTEM ksu_trace - -#if !defined(_KSU_TRACE_H) || defined(TRACE_HEADER_MULTI_READ) -#define _KSU_TRACE_H - -#include -#include - -DECLARE_TRACE(ksu_trace_execveat_sucompat_hook, - TP_PROTO(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags), - TP_ARGS(fd, filename_ptr, argv, envp, flags)); - -DECLARE_TRACE(ksu_trace_faccessat_hook, - TP_PROTO(int *dfd, const char __user **filename_user, int *mode, int *flags), - TP_ARGS(dfd, filename_user, mode, flags)); - -DECLARE_TRACE(ksu_trace_sys_read_hook, - TP_PROTO(unsigned int fd, char __user **buf_ptr, size_t *count_ptr), - TP_ARGS(fd, buf_ptr, count_ptr)); - -DECLARE_TRACE(ksu_trace_stat_hook, - TP_PROTO(int *dfd, const char __user **filename_user, int *flags), - TP_ARGS(dfd, filename_user, flags)); - -DECLARE_TRACE(ksu_trace_input_hook, - TP_PROTO(unsigned int *type, unsigned int *code, int *value), - TP_ARGS(type, code, value)); - -#endif /* _KSU_TRACE_H */ - -#undef TRACE_INCLUDE_PATH -#define TRACE_INCLUDE_PATH . -#undef TRACE_INCLUDE_FILE -#define TRACE_INCLUDE_FILE ksu_trace - -#include diff --git a/kernel/ksu_trace_export.c b/kernel/ksu_trace_export.c deleted file mode 100644 index afa4472..0000000 --- a/kernel/ksu_trace_export.c +++ /dev/null @@ -1,8 +0,0 @@ -#define CREATE_TRACE_POINTS -#include "ksu_trace.h" - -EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_execveat_sucompat_hook); -EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_faccessat_hook); -EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_sys_read_hook); -EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_stat_hook); -EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_input_hook); diff --git a/kernel/ksud.c b/kernel/ksud.c index f121b62..59f9279 100644 --- a/kernel/ksud.c +++ b/kernel/ksud.c @@ -1,3 +1,6 @@ +#include +#include +#include #include #include #include @@ -11,226 +14,319 @@ #include #include #include +#include #include +#include "manager.h" #include "allowlist.h" #include "arch.h" #include "klog.h" // IWYU pragma: keep #include "ksud.h" -#include "kernel_compat.h" #include "selinux/selinux.h" +#include "throne_tracker.h" +bool ksu_module_mounted __read_mostly = false; +bool ksu_boot_completed __read_mostly = false; static const char KERNEL_SU_RC[] = - "\n" + "\n" - "on post-fs-data\n" - " start logd\n" - // We should wait for the post-fs-data finish - " exec u:r:su:s0 root -- " KSUD_PATH " post-fs-data\n" - "\n" + "on post-fs-data\n" + " start logd\n" + // We should wait for the post-fs-data finish + " exec u:r:su:s0 root -- " KSUD_PATH " post-fs-data\n" + "\n" - "on nonencrypted\n" - " exec u:r:su:s0 root -- " KSUD_PATH " services\n" - "\n" + "on nonencrypted\n" + " exec u:r:su:s0 root -- " KSUD_PATH " services\n" + "\n" - "on property:vold.decrypt=trigger_restart_framework\n" - " exec u:r:su:s0 root -- " KSUD_PATH " services\n" - "\n" + "on property:vold.decrypt=trigger_restart_framework\n" + " exec u:r:su:s0 root -- " KSUD_PATH " services\n" + "\n" - "on property:sys.boot_completed=1\n" - " exec u:r:su:s0 root -- " KSUD_PATH " boot-completed\n" - "\n" + "on property:sys.boot_completed=1\n" + " exec u:r:su:s0 root -- " KSUD_PATH " boot-completed\n" + "\n" - "\n"; + "\n"; static void stop_vfs_read_hook(); static void stop_execve_hook(); static void stop_input_hook(); -#ifdef CONFIG_KSU_KPROBES_HOOK +#ifdef KSU_KPROBES_HOOK static struct work_struct stop_vfs_read_work; static struct work_struct stop_execve_hook_work; static struct work_struct stop_input_hook_work; #else bool ksu_vfs_read_hook __read_mostly = true; +bool ksu_execveat_hook __read_mostly = true; bool ksu_input_hook __read_mostly = true; #endif -bool ksu_execveat_hook __read_mostly = true; -u32 ksu_devpts_sid; +u32 ksu_file_sid; // Detect whether it is on or not static bool is_boot_phase = true; void on_post_fs_data(void) { - static bool done = false; - if (done) { - pr_info("on_post_fs_data already done\n"); - return; - } - done = true; - pr_info("on_post_fs_data!\n"); - ksu_load_allow_list(); - // sanity check, this may influence the performance - stop_input_hook(); + static bool done = false; + if (done) { + pr_info("on_post_fs_data already done\n"); + return; + } + done = true; + pr_info("on_post_fs_data!\n"); + ksu_load_allow_list(); + ksu_observer_init(); + // sanity check, this may influence the performance + stop_input_hook(); - ksu_devpts_sid = ksu_get_devpts_sid(); - pr_info("devpts sid: %d\n", ksu_devpts_sid); - - // End of boot state + // End of boot state is_boot_phase = false; + + ksu_file_sid = ksu_get_ksu_file_sid(); + pr_info("ksu_file sid: %d\n", ksu_file_sid); } -// since _ksud handler only uses argv and envp for comparisons -// this can probably work -// adapted from ksu_handle_execveat_ksud -static int ksu_handle_bprm_ksud(const char *filename, const char *argv1, const char *envp, size_t envp_len) +extern void ext4_unregister_sysfs(struct super_block *sb); +int nuke_ext4_sysfs(const char* mnt) { - static const char app_process[] = "/system/bin/app_process"; - static bool first_app_process = true; +#ifdef CONFIG_EXT4_FS + struct path path; + int err = kern_path(mnt, 0, &path); + if (err) { + pr_err("nuke path err: %d\n", err); + return err; + } - /* This applies to versions Android 10+ */ - static const char system_bin_init[] = "/system/bin/init"; - /* This applies to versions between Android 6 ~ 9 */ - static const char old_system_init[] = "/init"; - static bool init_second_stage_executed = false; + struct super_block *sb = path.dentry->d_inode->i_sb; + const char *name = sb->s_type->name; + if (strcmp(name, "ext4") != 0) { + pr_info("nuke but module aren't mounted\n"); + path_put(&path); + return -EINVAL; + } - // return early when disabled - if (!ksu_execveat_hook) - return 0; + ext4_unregister_sysfs(sb); + path_put(&path); - if (!filename) - return 0; + return 0; +#endif +} - // debug! remove me! - pr_info("%s: filename: %s argv1: %s envp_len: %zu\n", __func__, filename, argv1, envp_len); +void on_module_mounted(void){ + pr_info("on_module_mounted!\n"); + ksu_module_mounted = true; +} -#ifdef CONFIG_KSU_DEBUG - const char *envp_n = envp; - unsigned int envc = 1; - do { - pr_info("%s: envp[%d]: %s\n", __func__, envc, envp_n); - envp_n += strlen(envp_n) + 1; - envc++; - } while (envp_n < envp + 256); +void on_boot_completed(void){ + ksu_boot_completed = true; + pr_info("on_boot_completed!\n"); + track_throne(true); +} + +#define MAX_ARG_STRINGS 0x7FFFFFFF +struct user_arg_ptr { +#ifdef CONFIG_COMPAT + bool is_compat; +#endif + union { + const char __user *const __user *native; +#ifdef CONFIG_COMPAT + const compat_uptr_t __user *compat; +#endif + } ptr; +}; + +static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr) +{ + const char __user *native; + +#ifdef CONFIG_COMPAT + if (unlikely(argv.is_compat)) { + compat_uptr_t compat; + + if (get_user(compat, argv.ptr.compat + nr)) + return ERR_PTR(-EFAULT); + + return compat_ptr(compat); + } #endif - if (init_second_stage_executed) - goto first_app_process; + if (get_user(native, argv.ptr.native + nr)) + return ERR_PTR(-EFAULT); - // /system/bin/init with argv1 - if (!init_second_stage_executed - && (!memcmp(filename, system_bin_init, sizeof(system_bin_init) - 1))) { - if (argv1 && !strcmp(argv1, "second_stage")) { - pr_info("%s: /system/bin/init second_stage executed\n", __func__); - apply_kernelsu_rules(); - init_second_stage_executed = true; - ksu_android_ns_fs_check(); - } - } - - // /init with argv1 - if (!init_second_stage_executed - && (!memcmp(filename, old_system_init, sizeof(old_system_init) - 1))) { - if (argv1 && !strcmp(argv1, "--second-stage")) { - pr_info("%s: /init --second-stage executed\n", __func__); - apply_kernelsu_rules(); - init_second_stage_executed = true; - ksu_android_ns_fs_check(); - } - } - - if (!envp || !envp_len) - goto first_app_process; - - // /init without argv1/useless-argv1 but usable envp - // untested! TODO: test and debug me! - if (!init_second_stage_executed && (!memcmp(filename, old_system_init, sizeof(old_system_init) - 1))) { - - // we hunt for "INIT_SECOND_STAGE" - const char *envp_n = envp; - unsigned int envc = 1; - do { - if (strstarts(envp_n, "INIT_SECOND_STAGE")) - break; - envp_n += strlen(envp_n) + 1; - envc++; - } while (envp_n < envp + envp_len); - pr_info("%s: envp[%d]: %s\n", __func__, envc, envp_n); - - if (!strcmp(envp_n, "INIT_SECOND_STAGE=1") - || !strcmp(envp_n, "INIT_SECOND_STAGE=true") ) { - pr_info("%s: /init +envp: INIT_SECOND_STAGE executed\n", __func__); - apply_kernelsu_rules(); - init_second_stage_executed = true; - ksu_android_ns_fs_check(); - } - } - -first_app_process: - if (first_app_process && !memcmp(filename, app_process, sizeof(app_process) - 1)) { - first_app_process = false; - pr_info("%s: exec app_process, /data prepared, second_stage: %d\n", __func__, init_second_stage_executed); - on_post_fs_data(); - stop_execve_hook(); - } - - return 0; + return native; } -int ksu_handle_pre_ksud(const char *filename) +/* + * count() counts the number of strings in array ARGV. + */ + +/* + * Make sure old GCC compiler can use __maybe_unused, + * Test passed in 4.4.x ~ 4.9.x when use GCC. + */ + +static int __maybe_unused count(struct user_arg_ptr argv, int max) { - if (likely(!ksu_execveat_hook)) - return 0; + int i = 0; - // not /system/bin/init, not /init, not /system/bin/app_process (64/32 thingy) - // return 0; - if (likely(strcmp(filename, "/system/bin/init") && strcmp(filename, "/init") - && !strstarts(filename, "/system/bin/app_process") )) - return 0; + if (argv.ptr.native != NULL) { + for (;;) { + const char __user *p = get_user_arg_ptr(argv, i); - if (!current || !current->mm) - return 0; + if (!p) + break; - // https://elixir.bootlin.com/linux/v4.14.1/source/include/linux/mm_types.h#L429 - // unsigned long arg_start, arg_end, env_start, env_end; - unsigned long arg_start = current->mm->arg_start; - unsigned long arg_end = current->mm->arg_end; - unsigned long env_start = current->mm->env_start; - unsigned long env_end = current->mm->env_end; + if (IS_ERR(p)) + return -EFAULT; - size_t arg_len = arg_end - arg_start; - size_t envp_len = env_end - env_start; + if (i >= max) + return -E2BIG; + ++i; - if (arg_len <= 0 || envp_len <= 0) // this wont make sense, filter it - return 0; + if (fatal_signal_pending(current)) + return -ERESTARTNOHAND; + } + } + return i; +} - #define ARGV_MAX 32 // this is enough for argv1 - #define ENVP_MAX 256 // this is enough for INIT_SECOND_STAGE - char args[ARGV_MAX]; - size_t argv_copy_len = (arg_len > ARGV_MAX) ? ARGV_MAX : arg_len; - char envp[ENVP_MAX]; - size_t envp_copy_len = (envp_len > ENVP_MAX) ? ENVP_MAX : envp_len; +static void on_post_fs_data_cbfun(struct callback_head *cb) +{ + on_post_fs_data(); +} - // we cant use strncpy on here, else it will truncate once it sees \0 - if (ksu_copy_from_user_retry(args, (void __user *)arg_start, argv_copy_len)) - return 0; +static struct callback_head on_post_fs_data_cb = { .func = + on_post_fs_data_cbfun }; - if (ksu_copy_from_user_retry(envp, (void __user *)env_start, envp_copy_len)) - return 0; +// IMPORTANT NOTE: the call from execve_handler_pre WON'T provided correct value for envp and flags in GKI version +int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr, + struct user_arg_ptr *argv, + struct user_arg_ptr *envp, int *flags) +{ +#ifndef KSU_KPROBES_HOOK + if (!ksu_execveat_hook) { + return 0; + } +#endif + struct filename *filename; - args[ARGV_MAX - 1] = '\0'; - envp[ENVP_MAX - 1] = '\0'; + static const char app_process[] = "/system/bin/app_process"; + static bool first_app_process = true; - // we only need argv1 ! - // abuse strlen here since it only gets length up to \0 - char *argv1 = args + strlen(args) + 1; - if (argv1 >= args + argv_copy_len) // out of bounds! - argv1 = ""; + /* This applies to versions Android 10+ */ + static const char system_bin_init[] = "/system/bin/init"; + /* This applies to versions between Android 6 ~ 9 */ + static const char old_system_init[] = "/init"; + static bool init_second_stage_executed = false; - return ksu_handle_bprm_ksud(filename, argv1, envp, envp_copy_len); + if (!filename_ptr) + return 0; + + filename = *filename_ptr; + if (IS_ERR(filename)) { + return 0; + } + + if (unlikely(!memcmp(filename->name, system_bin_init, + sizeof(system_bin_init) - 1) && + argv)) { + // /system/bin/init executed + int argc = count(*argv, MAX_ARG_STRINGS); + pr_info("/system/bin/init argc: %d\n", argc); + if (argc > 1 && !init_second_stage_executed) { + const char __user *p = get_user_arg_ptr(*argv, 1); + if (p && !IS_ERR(p)) { + char first_arg[16]; + strncpy_from_user_nofault(first_arg, p, sizeof(first_arg)); + pr_info("/system/bin/init first arg: %s\n", first_arg); + if (!strcmp(first_arg, "second_stage")) { + pr_info("/system/bin/init second_stage executed\n"); + apply_kernelsu_rules(); + init_second_stage_executed = true; + } + } else { + pr_err("/system/bin/init parse args err!\n"); + } + } + } else if (unlikely(!memcmp(filename->name, old_system_init, + sizeof(old_system_init) - 1) && + argv)) { + // /init executed + int argc = count(*argv, MAX_ARG_STRINGS); + pr_info("/init argc: %d\n", argc); + if (argc > 1 && !init_second_stage_executed) { + /* This applies to versions between Android 6 ~ 7 */ + const char __user *p = get_user_arg_ptr(*argv, 1); + if (p && !IS_ERR(p)) { + char first_arg[16]; + strncpy_from_user_nofault(first_arg, p, sizeof(first_arg)); + pr_info("/init first arg: %s\n", first_arg); + if (!strcmp(first_arg, "--second-stage")) { + pr_info("/init second_stage executed\n"); + apply_kernelsu_rules(); + init_second_stage_executed = true; + } + } else { + pr_err("/init parse args err!\n"); + } + } else if (argc == 1 && !init_second_stage_executed && envp) { + /* This applies to versions between Android 8 ~ 9 */ + int envc = count(*envp, MAX_ARG_STRINGS); + if (envc > 0) { + int n; + for (n = 1; n <= envc; n++) { + const char __user *p = get_user_arg_ptr(*envp, n); + if (!p || IS_ERR(p)) { + continue; + } + char env[256]; + // Reading environment variable strings from user space + if (strncpy_from_user_nofault(env, p, sizeof(env)) < 0) + continue; + // Parsing environment variable names and values + char *env_name = env; + char *env_value = strchr(env, '='); + if (env_value == NULL) + continue; + // Replace equal sign with string terminator + *env_value = '\0'; + env_value++; + // Check if the environment variable name and value are matching + if (!strcmp(env_name, "INIT_SECOND_STAGE") && + (!strcmp(env_value, "1") || + !strcmp(env_value, "true"))) { + pr_info("/init second_stage executed\n"); + apply_kernelsu_rules(); + init_second_stage_executed = true; + } + } + } + } + } + + if (unlikely(first_app_process && !memcmp(filename->name, app_process, + sizeof(app_process) - 1))) { + first_app_process = false; + pr_info("exec app_process, /data prepared, second_stage: %d\n", + init_second_stage_executed); + struct task_struct *init_task; + rcu_read_lock(); + init_task = rcu_dereference(current->real_parent); + if (init_task) { + task_work_add(init_task, &on_post_fs_data_cb, TWA_RESUME); + } + rcu_read_unlock(); + + stop_execve_hook(); + } + + return 0; } static ssize_t (*orig_read)(struct file *, char __user *, size_t, loff_t *); @@ -239,426 +335,326 @@ static struct file_operations fops_proxy; static ssize_t read_count_append = 0; static ssize_t read_proxy(struct file *file, char __user *buf, size_t count, - loff_t *pos) + loff_t *pos) { - bool first_read = file->f_pos == 0; - ssize_t ret = orig_read(file, buf, count, pos); - if (first_read) { - pr_info("read_proxy append %ld + %ld\n", ret, - read_count_append); - ret += read_count_append; - } - return ret; + bool first_read = file->f_pos == 0; + ssize_t ret = orig_read(file, buf, count, pos); + if (first_read) { + pr_info("read_proxy append %ld + %ld\n", ret, read_count_append); + ret += read_count_append; + } + return ret; } static ssize_t read_iter_proxy(struct kiocb *iocb, struct iov_iter *to) { - bool first_read = iocb->ki_pos == 0; - ssize_t ret = orig_read_iter(iocb, to); - if (first_read) { - pr_info("read_iter_proxy append %ld + %ld\n", ret, - read_count_append); - ret += read_count_append; - } - return ret; + bool first_read = iocb->ki_pos == 0; + ssize_t ret = orig_read_iter(iocb, to); + if (first_read) { + pr_info("read_iter_proxy append %ld + %ld\n", ret, read_count_append); + ret += read_count_append; + } + return ret; } -int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr, - size_t *count_ptr, loff_t **pos) +static int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr, + size_t *count_ptr, loff_t **pos) { -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_vfs_read_hook) { - return 0; - } +#ifndef KSU_KPROBES_HOOK + if (!ksu_vfs_read_hook) { + return 0; + } #endif - struct file *file; - char __user *buf; - size_t count; + struct file *file; + char __user *buf; + size_t count; - if (strcmp(current->comm, "init")) { - // we are only interest in `init` process - return 0; - } + if (strcmp(current->comm, "init")) { + // we are only interest in `init` process + return 0; + } - file = *file_ptr; - if (IS_ERR(file)) { - return 0; - } + file = *file_ptr; + if (IS_ERR(file)) { + return 0; + } - if (!d_is_reg(file->f_path.dentry)) { - return 0; - } + if (!d_is_reg(file->f_path.dentry)) { + return 0; + } - const char *short_name = file->f_path.dentry->d_name.name; - if (strcmp(short_name, "atrace.rc")) { - // we are only interest `atrace.rc` file name file - return 0; - } - char path[256]; - char *dpath = d_path(&file->f_path, path, sizeof(path)); + const char *short_name = file->f_path.dentry->d_name.name; + if (strcmp(short_name, "atrace.rc")) { + // we are only interest `atrace.rc` file name file + return 0; + } + char path[256]; + char *dpath = d_path(&file->f_path, path, sizeof(path)); - if (IS_ERR(dpath)) { - return 0; - } + if (IS_ERR(dpath)) { + return 0; + } - if (strcmp(dpath, "/system/etc/init/atrace.rc")) { - return 0; - } + if (strcmp(dpath, "/system/etc/init/atrace.rc")) { + return 0; + } - // we only process the first read - static bool rc_inserted = false; - if (rc_inserted) { - // we don't need this kprobe, unregister it! - stop_vfs_read_hook(); - return 0; - } - rc_inserted = true; + // we only process the first read + static bool rc_inserted = false; + if (rc_inserted) { + // we don't need this kprobe, unregister it! + stop_vfs_read_hook(); + return 0; + } + rc_inserted = true; - // now we can sure that the init process is reading - // `/system/etc/init/atrace.rc` - buf = *buf_ptr; - count = *count_ptr; + // now we can sure that the init process is reading + // `/system/etc/init/atrace.rc` + buf = *buf_ptr; + count = *count_ptr; - size_t rc_count = strlen(KERNEL_SU_RC); + size_t rc_count = strlen(KERNEL_SU_RC); - pr_info("vfs_read: %s, comm: %s, count: %zu, rc_count: %zu\n", dpath, - current->comm, count, rc_count); + pr_info("vfs_read: %s, comm: %s, count: %zu, rc_count: %zu\n", dpath, + current->comm, count, rc_count); - if (count < rc_count) { - pr_err("count: %zu < rc_count: %zu\n", count, rc_count); - return 0; - } + if (count < rc_count) { + pr_err("count: %zu < rc_count: %zu\n", count, rc_count); + return 0; + } - size_t ret = copy_to_user(buf, KERNEL_SU_RC, rc_count); - if (ret) { - pr_err("copy ksud.rc failed: %zu\n", ret); - return 0; - } + size_t ret = copy_to_user(buf, KERNEL_SU_RC, rc_count); + if (ret) { + pr_err("copy ksud.rc failed: %zu\n", ret); + return 0; + } - // we've succeed to insert ksud.rc, now we need to proxy the read and modify the result! - // But, we can not modify the file_operations directly, because it's in read-only memory. - // We just replace the whole file_operations with a proxy one. - memcpy(&fops_proxy, file->f_op, sizeof(struct file_operations)); - orig_read = file->f_op->read; - if (orig_read) { - fops_proxy.read = read_proxy; - } - orig_read_iter = file->f_op->read_iter; - if (orig_read_iter) { - fops_proxy.read_iter = read_iter_proxy; - } - // replace the file_operations - file->f_op = &fops_proxy; - read_count_append = rc_count; + // we've succeed to insert ksud.rc, now we need to proxy the read and modify the result! + // But, we can not modify the file_operations directly, because it's in read-only memory. + // We just replace the whole file_operations with a proxy one. + memcpy(&fops_proxy, file->f_op, sizeof(struct file_operations)); + orig_read = file->f_op->read; + if (orig_read) { + fops_proxy.read = read_proxy; + } + orig_read_iter = file->f_op->read_iter; + if (orig_read_iter) { + fops_proxy.read_iter = read_iter_proxy; + } + // replace the file_operations + file->f_op = &fops_proxy; + read_count_append = rc_count; - *buf_ptr = buf + rc_count; - *count_ptr = count - rc_count; + *buf_ptr = buf + rc_count; + *count_ptr = count - rc_count; - return 0; + return 0; } int ksu_handle_sys_read(unsigned int fd, char __user **buf_ptr, - size_t *count_ptr) + size_t *count_ptr) { - struct file *file = fget(fd); - if (!file) { - return 0; - } - int result = ksu_handle_vfs_read(&file, buf_ptr, count_ptr, NULL); - fput(file); - return result; + struct file *file = fget(fd); + if (!file) { + return 0; + } + int result = ksu_handle_vfs_read(&file, buf_ptr, count_ptr, NULL); + fput(file); + return result; } static unsigned int volumedown_pressed_count = 0; static bool is_volumedown_enough(unsigned int count) { - return count >= 3; + return count >= 3; } int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, - int *value) + int *value) { -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_input_hook) { - return 0; - } +#ifndef KSU_KPROBES_HOOK + if (!ksu_input_hook) { + return 0; + } #endif - if (*type == EV_KEY && *code == KEY_VOLUMEDOWN) { - int val = *value; - pr_info("KEY_VOLUMEDOWN val: %d\n", val); - if (val && is_boot_phase) { - // key pressed, count it - volumedown_pressed_count += 1; - if (is_volumedown_enough(volumedown_pressed_count)) { - stop_input_hook(); - } - } - } + if (*type == EV_KEY && *code == KEY_VOLUMEDOWN) { + int val = *value; + pr_info("KEY_VOLUMEDOWN val: %d\n", val); + if (val && is_boot_phase) { + // key pressed, count it + volumedown_pressed_count += 1; + if (is_volumedown_enough(volumedown_pressed_count)) { + stop_input_hook(); + } + } + } - return 0; + return 0; } bool ksu_is_safe_mode() { - static bool safe_mode = false; - if (safe_mode) { - // don't need to check again, userspace may call multiple times - return true; - } + static bool safe_mode = false; + if (safe_mode) { + // don't need to check again, userspace may call multiple times + return true; + } - // stop hook first! - stop_input_hook(); + // stop hook first! + stop_input_hook(); - pr_info("volumedown_pressed_count: %d\n", volumedown_pressed_count); - if (is_volumedown_enough(volumedown_pressed_count)) { - // pressed over 3 times - pr_info("KEY_VOLUMEDOWN pressed max times, safe mode detected!\n"); - safe_mode = true; - return true; - } + pr_info("volumedown_pressed_count: %d\n", volumedown_pressed_count); + if (is_volumedown_enough(volumedown_pressed_count)) { + // pressed over 3 times + pr_info("KEY_VOLUMEDOWN pressed max times, safe mode detected!\n"); + safe_mode = true; + return true; + } - return false; + return false; } -#ifdef CONFIG_KSU_KPROBES_HOOK +#ifdef KSU_KPROBES_HOOK + static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs) { - /* - asmlinkage int sys_execve(const char __user *filenamei, - const char __user *const __user *argv, - const char __user *const __user *envp, struct pt_regs *regs) - */ - struct pt_regs *real_regs = PT_REAL_REGS(regs); - const char __user *filename_user = (const char __user *)PT_REGS_PARM1(real_regs); - const char __user *const __user *__argv = (const char __user *const __user *)PT_REGS_PARM2(real_regs); - const char __user *const __user *__envp = (const char __user *const __user *)PT_REGS_PARM3(real_regs); - char path[32]; + struct pt_regs *real_regs = PT_REAL_REGS(regs); + const char __user **filename_user = + (const char **)&PT_REGS_PARM1(real_regs); + const char __user *const __user *__argv = + (const char __user *const __user *)PT_REGS_PARM2(real_regs); + struct user_arg_ptr argv = { .ptr.native = __argv }; + struct filename filename_in, *filename_p; + char path[32]; - if (!filename_user) - return 0; + if (!filename_user) + return 0; -// filename stage - if (ksu_copy_from_user_retry(path, filename_user, sizeof(path))) - return 0; + memset(path, 0, sizeof(path)); + strncpy_from_user_nofault(path, *filename_user, 32); + filename_in.name = path; - path[sizeof(path) - 1] = '\0'; - - // not /system/bin/init, not /init, not /system/bin/app_process (64/32 thingy) - // we dont care !! - if (likely(strcmp(path, "/system/bin/init") && strcmp(path, "/init") - && !strstarts(path, "/system/bin/app_process") )) - return 0; - -// argv stage - char argv1[32] = {0}; - // memzero_explicit(argv1, 32); - if (__argv) { - const char __user *arg1_user = NULL; - // grab argv[1] pointer - // this looks like - /* - * 0x1000 ./program << this is __argv - * 0x1001 -o - * 0x1002 arg - */ - if (ksu_copy_from_user_retry(&arg1_user, __argv + 1, sizeof(arg1_user))) - goto no_argv1; // copy argv[1] pointer fail, probably no argv1 !! - - if (arg1_user) - ksu_copy_from_user_retry(argv1, arg1_user, sizeof(argv1)); - } - -no_argv1: - argv1[sizeof(argv1) - 1] = '\0'; - -// envp stage - #define ENVP_MAX 256 - char envp[ENVP_MAX] = {0}; - char *dst = envp; - size_t envp_len = 0; - int i = 0; // to track user pointer offset from __envp - - // memzero_explicit(envp, ENVP_MAX); - - if (__envp) { - do { - const char __user *env_entry_user = NULL; - // this is also like argv above - /* - * 0x1001 PATH=/bin - * 0x1002 VARIABLE=value - * 0x1002 some_more_env_var=1 - */ - - // check if pointer exists - if (ksu_copy_from_user_retry(&env_entry_user, __envp + i, sizeof(env_entry_user))) - break; - - // check if no more env entry - if (!env_entry_user) - break; - - // probably redundant to while condition but ok - if (envp_len >= ENVP_MAX - 1) - break; - - // copy strings from env_entry_user pointer that we collected - // also break if failed - if (ksu_copy_from_user_retry(dst, env_entry_user, ENVP_MAX - envp_len)) - break; - - // get the length of that new copy above - // get lngth of dst as far as ENVP_MAX - current collected envp_len - size_t len = strnlen(dst, ENVP_MAX - envp_len); - if (envp_len + len + 1 > ENVP_MAX) - break; // if more than 255 bytes, bail - - dst[len] = '\0'; - // collect total number of copied strings - envp_len = envp_len + len + 1; - // increment dst address since we need to put something on next iter - dst = dst + len + 1; - // pointer walk, __envp + i - i++; - } while (envp_len < ENVP_MAX); - } - - /* - at this point, we shoul've collected envp from - * 0x1001 PATH=/bin - * 0x1002 VARIABLE=value - * 0x1002 some_more_env_var=1 - to - * 0x1234 PATH=/bin\0VARIABLE=value\0some_more_env_var=1\0\0\0\0 - */ - - envp[ENVP_MAX - 1] = '\0'; - -#ifdef CONFIG_KSU_DEBUG - pr_info("%s: filename: %s argv[1]:%s envp_len: %zu\n", __func__, path, argv1, envp_len); -#endif - return ksu_handle_bprm_ksud(path, argv1, envp, envp_len); + filename_p = &filename_in; + return ksu_handle_execveat_ksud(AT_FDCWD, &filename_p, &argv, NULL, NULL); } static int sys_read_handler_pre(struct kprobe *p, struct pt_regs *regs) { - struct pt_regs *real_regs = PT_REAL_REGS(regs); - unsigned int fd = PT_REGS_PARM1(real_regs); - char __user **buf_ptr = (char __user **)&PT_REGS_PARM2(real_regs); - size_t count_ptr = (size_t *)&PT_REGS_PARM3(real_regs); + struct pt_regs *real_regs = PT_REAL_REGS(regs); + unsigned int fd = PT_REGS_PARM1(real_regs); + char __user **buf_ptr = (char __user **)&PT_REGS_PARM2(real_regs); + size_t count_ptr = (size_t *)&PT_REGS_PARM3(real_regs); - return ksu_handle_sys_read(fd, buf_ptr, count_ptr); + return ksu_handle_sys_read(fd, buf_ptr, count_ptr); } static int input_handle_event_handler_pre(struct kprobe *p, - struct pt_regs *regs) + struct pt_regs *regs) { - unsigned int *type = (unsigned int *)&PT_REGS_PARM2(regs); - unsigned int *code = (unsigned int *)&PT_REGS_PARM3(regs); - int *value = (int *)&PT_REGS_CCALL_PARM4(regs); - return ksu_handle_input_handle_event(type, code, value); + unsigned int *type = (unsigned int *)&PT_REGS_PARM2(regs); + unsigned int *code = (unsigned int *)&PT_REGS_PARM3(regs); + int *value = (int *)&PT_REGS_CCALL_PARM4(regs); + return ksu_handle_input_handle_event(type, code, value); } static struct kprobe execve_kp = { - .symbol_name = SYS_EXECVE_SYMBOL, - .pre_handler = sys_execve_handler_pre, + .symbol_name = SYS_EXECVE_SYMBOL, + .pre_handler = sys_execve_handler_pre, }; static struct kprobe vfs_read_kp = { - .symbol_name = SYS_READ_SYMBOL, - .pre_handler = sys_read_handler_pre, + .symbol_name = SYS_READ_SYMBOL, + .pre_handler = sys_read_handler_pre, }; static struct kprobe input_event_kp = { - .symbol_name = "input_event", - .pre_handler = input_handle_event_handler_pre, + .symbol_name = "input_event", + .pre_handler = input_handle_event_handler_pre, }; - static void do_stop_vfs_read_hook(struct work_struct *work) { - unregister_kprobe(&vfs_read_kp); + unregister_kprobe(&vfs_read_kp); } static void do_stop_execve_hook(struct work_struct *work) { - unregister_kprobe(&execve_kp); + unregister_kprobe(&execve_kp); } static void do_stop_input_hook(struct work_struct *work) { - unregister_kprobe(&input_event_kp); + unregister_kprobe(&input_event_kp); } #endif static void stop_vfs_read_hook() { -#ifdef CONFIG_KSU_KPROBES_HOOK - bool ret = schedule_work(&stop_vfs_read_work); - pr_info("unregister vfs_read kprobe: %d!\n", ret); +#ifdef KSU_KPROBES_HOOK + bool ret = schedule_work(&stop_vfs_read_work); + pr_info("unregister vfs_read kprobe: %d!\n", ret); #else - ksu_vfs_read_hook = false; - pr_info("stop vfs_read_hook\n"); + ksu_vfs_read_hook = false; + pr_info("stop vfs_read_hook\n"); #endif } static void stop_execve_hook() { -#ifdef CONFIG_KSU_KPROBES_HOOK - bool ret = schedule_work(&stop_execve_hook_work); - pr_info("unregister execve kprobe: %d!\n", ret); +#ifdef KSU_KPROBES_HOOK + bool ret = schedule_work(&stop_execve_hook_work); + pr_info("unregister execve kprobe: %d!\n", ret); #else - pr_info("stop execve_hook\n"); + ksu_execveat_hook = false; + pr_info("stop execve_hook\n"); #endif - ksu_execveat_hook = false; } static void stop_input_hook() { - static bool input_hook_stopped = false; - if (input_hook_stopped) { - return; - } - input_hook_stopped = true; -#ifdef CONFIG_KSU_KPROBES_HOOK - bool ret = schedule_work(&stop_input_hook_work); - pr_info("unregister input kprobe: %d!\n", ret); + static bool input_hook_stopped = false; + if (input_hook_stopped) { + return; + } + input_hook_stopped = true; +#ifdef KSU_KPROBES_HOOK + bool ret = schedule_work(&stop_input_hook_work); + pr_info("unregister input kprobe: %d!\n", ret); #else - ksu_input_hook = false; - pr_info("stop input_hook\n"); + ksu_input_hook = false; + pr_info("stop input_hook\n"); #endif } // ksud: module support void ksu_ksud_init() { -#ifdef CONFIG_KSU_KPROBES_HOOK - int ret; +#ifdef KSU_KPROBES_HOOK + int ret; - ret = register_kprobe(&execve_kp); - pr_info("ksud: execve_kp: %d\n", ret); + ret = register_kprobe(&execve_kp); + pr_info("ksud: execve_kp: %d\n", ret); - ret = register_kprobe(&vfs_read_kp); - pr_info("ksud: vfs_read_kp: %d\n", ret); + ret = register_kprobe(&vfs_read_kp); + pr_info("ksud: vfs_read_kp: %d\n", ret); - ret = register_kprobe(&input_event_kp); - pr_info("ksud: input_event_kp: %d\n", ret); + ret = register_kprobe(&input_event_kp); + pr_info("ksud: input_event_kp: %d\n", ret); - INIT_WORK(&stop_vfs_read_work, do_stop_vfs_read_hook); - INIT_WORK(&stop_execve_hook_work, do_stop_execve_hook); - INIT_WORK(&stop_input_hook_work, do_stop_input_hook); + INIT_WORK(&stop_vfs_read_work, do_stop_vfs_read_hook); + INIT_WORK(&stop_execve_hook_work, do_stop_execve_hook); + INIT_WORK(&stop_input_hook_work, do_stop_input_hook); #endif } void ksu_ksud_exit() { -#ifdef CONFIG_KSU_KPROBES_HOOK - unregister_kprobe(&execve_kp); - // this should be done before unregister vfs_read_kp - // unregister_kprobe(&vfs_read_kp); - unregister_kprobe(&input_event_kp); +#ifdef KSU_KPROBES_HOOK + unregister_kprobe(&execve_kp); + // this should be done before unregister vfs_read_kp + // unregister_kprobe(&vfs_read_kp); + unregister_kprobe(&input_event_kp); #endif - - is_boot_phase = false; + is_boot_phase = false; } diff --git a/kernel/ksud.h b/kernel/ksud.h index 7dd4cd0..2ee62e9 100644 --- a/kernel/ksud.h +++ b/kernel/ksud.h @@ -3,13 +3,19 @@ #define KSUD_PATH "/data/adb/ksud" +void ksu_ksud_init(); +void ksu_ksud_exit(); + void on_post_fs_data(void); +void on_module_mounted(void); +void on_boot_completed(void); bool ksu_is_safe_mode(void); -extern u32 ksu_devpts_sid; +int nuke_ext4_sysfs(const char* mnt); -extern bool ksu_execveat_hook __read_mostly; -extern int ksu_handle_pre_ksud(const char *filename); +extern u32 ksu_file_sid; +extern bool ksu_module_mounted; +extern bool ksu_boot_completed; #endif diff --git a/kernel/manager.h b/kernel/manager.h index f27afbb..2421da7 100644 --- a/kernel/manager.h +++ b/kernel/manager.h @@ -13,30 +13,31 @@ extern void ksu_add_manager(uid_t uid, int signature_index); extern void ksu_remove_manager(uid_t uid); extern int ksu_get_manager_signature_index(uid_t uid); -static inline bool ksu_is_manager_uid_valid() +static inline bool ksu_is_manager_uid_valid(void) { - return ksu_manager_uid != KSU_INVALID_UID; + return ksu_manager_uid != KSU_INVALID_UID; } -static inline bool is_manager() +static inline bool is_manager(void) { - return unlikely(ksu_is_any_manager(current_uid().val) || - (ksu_manager_uid != KSU_INVALID_UID && ksu_manager_uid == current_uid().val)); + return unlikely(ksu_is_any_manager(current_uid().val) || + (ksu_manager_uid != KSU_INVALID_UID && ksu_manager_uid == current_uid().val)); } -static inline uid_t ksu_get_manager_uid() +static inline uid_t ksu_get_manager_uid(void) { - return ksu_manager_uid; + return ksu_manager_uid; } static inline void ksu_set_manager_uid(uid_t uid) { - ksu_manager_uid = uid; + ksu_manager_uid = uid; } -static inline void ksu_invalidate_manager_uid() +static inline void ksu_invalidate_manager_uid(void) { - ksu_manager_uid = KSU_INVALID_UID; + ksu_manager_uid = KSU_INVALID_UID; } -#endif \ No newline at end of file +int ksu_observer_init(void); +#endif diff --git a/kernel/manager_sign.h b/kernel/manager_sign.h index b7a8e8c..265ef35 100644 --- a/kernel/manager_sign.h +++ b/kernel/manager_sign.h @@ -9,5 +9,9 @@ #define EXPECTED_SIZE_OTHER 0x300 #define EXPECTED_HASH_OTHER "0000000000000000000000000000000000000000000000000000000000000000" +typedef struct { + unsigned size; + const char *sha256; +} apk_sign_key_t; -#endif /* MANAGER_SIGN_H */ \ No newline at end of file +#endif /* MANAGER_SIGN_H */ diff --git a/kernel/manual_su.c b/kernel/manual_su.c new file mode 100644 index 0000000..dfb3a25 --- /dev/null +++ b/kernel/manual_su.c @@ -0,0 +1,357 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "manual_su.h" +#include "ksu.h" +#include "allowlist.h" +#include "manager.h" +#include "app_profile.h" + +static bool current_verified = false; +static void ksu_cleanup_expired_tokens(void); +static bool is_current_verified(void); +static void add_pending_root(uid_t uid); + +static struct pending_uid pending_uids[MAX_PENDING] = {0}; +static int pending_cnt = 0; +static struct ksu_token_entry auth_tokens[MAX_TOKENS] = {0}; +static int token_count = 0; +static DEFINE_SPINLOCK(token_lock); + +static char* get_token_from_envp(void) +{ + struct mm_struct *mm; + char *envp_start, *envp_end; + char *env_ptr, *token = NULL; + unsigned long env_len; + char *env_copy = NULL; + + if (!current->mm) + return NULL; + + mm = current->mm; + + down_read(&mm->mmap_lock); + + envp_start = (char *)mm->env_start; + envp_end = (char *)mm->env_end; + env_len = envp_end - envp_start; + + if (env_len <= 0 || env_len > PAGE_SIZE * 32) { + up_read(&mm->mmap_lock); + return NULL; + } + + env_copy = kzalloc(env_len + 1, GFP_KERNEL); + if (!env_copy) { + up_read(&mm->mmap_lock); + return NULL; + } + + if (copy_from_user(env_copy, envp_start, env_len)) { + kfree(env_copy); + up_read(&mm->mmap_lock); + return NULL; + } + + up_read(&mm->mmap_lock); + + env_copy[env_len] = '\0'; + env_ptr = env_copy; + + while (env_ptr < env_copy + env_len) { + if (strncmp(env_ptr, KSU_TOKEN_ENV_NAME "=", strlen(KSU_TOKEN_ENV_NAME) + 1) == 0) { + char *token_start = env_ptr + strlen(KSU_TOKEN_ENV_NAME) + 1; + char *token_end = strchr(token_start, '\0'); + + if (token_end && (token_end - token_start) == KSU_TOKEN_LENGTH) { + token = kzalloc(KSU_TOKEN_LENGTH + 1, GFP_KERNEL); + if (token) { + memcpy(token, token_start, KSU_TOKEN_LENGTH); + token[KSU_TOKEN_LENGTH] = '\0'; + pr_info("manual_su: found auth token in environment\n"); + } + } + break; + } + + env_ptr += strlen(env_ptr) + 1; + } + + kfree(env_copy); + return token; +} + +static char* ksu_generate_auth_token(void) +{ + static char token_buffer[KSU_TOKEN_LENGTH + 1]; + unsigned long flags; + int i; + + ksu_cleanup_expired_tokens(); + + spin_lock_irqsave(&token_lock, flags); + + if (token_count >= MAX_TOKENS) { + for (i = 0; i < MAX_TOKENS - 1; i++) { + auth_tokens[i] = auth_tokens[i + 1]; + } + token_count = MAX_TOKENS - 1; + } + + for (i = 0; i < KSU_TOKEN_LENGTH; i++) { + u8 rand_byte; + get_random_bytes(&rand_byte, 1); + int char_type = rand_byte % 3; + if (char_type == 0) { + token_buffer[i] = 'A' + (rand_byte % 26); + } else if (char_type == 1) { + token_buffer[i] = 'a' + (rand_byte % 26); + } else { + token_buffer[i] = '0' + (rand_byte % 10); + } + } + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) + strscpy(auth_tokens[token_count].token, token_buffer, KSU_TOKEN_LENGTH + 1); +#else + strlcpy(auth_tokens[token_count].token, token_buffer, KSU_TOKEN_LENGTH + 1); +#endif + auth_tokens[token_count].expire_time = jiffies + KSU_TOKEN_EXPIRE_TIME * HZ; + auth_tokens[token_count].used = false; + token_count++; + + spin_unlock_irqrestore(&token_lock, flags); + + pr_info("manual_su: generated new auth token (expires in %d seconds)\n", KSU_TOKEN_EXPIRE_TIME); + return token_buffer; +} + +static bool ksu_verify_auth_token(const char *token) +{ + unsigned long flags; + bool valid = false; + int i; + + if (!token || strlen(token) != KSU_TOKEN_LENGTH) { + return false; + } + + spin_lock_irqsave(&token_lock, flags); + + for (i = 0; i < token_count; i++) { + if (!auth_tokens[i].used && + time_before(jiffies, auth_tokens[i].expire_time) && + strcmp(auth_tokens[i].token, token) == 0) { + + auth_tokens[i].used = true; + valid = true; + pr_info("manual_su: auth token verified successfully\n"); + break; + } + } + + spin_unlock_irqrestore(&token_lock, flags); + + if (!valid) { + pr_warn("manual_su: invalid or expired auth token\n"); + } + + return valid; +} + +static void ksu_cleanup_expired_tokens(void) +{ + unsigned long flags; + int i, j; + + spin_lock_irqsave(&token_lock, flags); + + for (i = 0; i < token_count; ) { + if (time_after(jiffies, auth_tokens[i].expire_time) || auth_tokens[i].used) { + for (j = i; j < token_count - 1; j++) { + auth_tokens[j] = auth_tokens[j + 1]; + } + token_count--; + pr_debug("manual_su: cleaned up expired/used token\n"); + } else { + i++; + } + } + + spin_unlock_irqrestore(&token_lock, flags); +} + +static int handle_token_generation(struct manual_su_request *request) +{ + if (current_uid().val > 2000) { + pr_warn("manual_su: token generation denied for app UID %d\n", current_uid().val); + return -EPERM; + } + + char *new_token = ksu_generate_auth_token(); + if (!new_token) { + pr_err("manual_su: failed to generate token\n"); + return -ENOMEM; + } + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) + strscpy(request->token_buffer, new_token, KSU_TOKEN_LENGTH + 1); +#else + strlcpy(request->token_buffer, new_token, KSU_TOKEN_LENGTH + 1); +#endif + + pr_info("manual_su: auth token generated successfully\n"); + return 0; +} + +static int handle_escalation_request(struct manual_su_request *request) +{ + uid_t target_uid = request->target_uid; + pid_t target_pid = request->target_pid; + struct task_struct *tsk; + + rcu_read_lock(); + tsk = pid_task(find_vpid(target_pid), PIDTYPE_PID); + if (!tsk || ksu_task_is_dead(tsk)) { + rcu_read_unlock(); + pr_err("cmd_su: PID %d is invalid or dead\n", target_pid); + return -ESRCH; + } + rcu_read_unlock(); + + if (current_uid().val == 0 || is_manager() || ksu_is_allow_uid_for_current(current_uid().val)) + goto allowed; + + char *env_token = get_token_from_envp(); + if (!env_token) { + pr_warn("manual_su: no auth token found in environment\n"); + return -EACCES; + } + + bool token_valid = ksu_verify_auth_token(env_token); + kfree(env_token); + + if (!token_valid) { + pr_warn("manual_su: token verification failed\n"); + return -EACCES; + } + +allowed: + current_verified = true; + escape_to_root_for_cmd_su(target_uid, target_pid); + return 0; +} + +static int handle_add_pending_request(struct manual_su_request *request) +{ + uid_t target_uid = request->target_uid; + + if (!is_current_verified()) { + pr_warn("manual_su: add_pending denied, not verified\n"); + return -EPERM; + } + + add_pending_root(target_uid); + current_verified = false; + pr_info("manual_su: pending root added for UID %d\n", target_uid); + return 0; +} + +int ksu_handle_manual_su_request(int option, struct manual_su_request *request) +{ + if (!request) { + pr_err("manual_su: invalid request pointer\n"); + return -EINVAL; + } + + switch (option) { + case MANUAL_SU_OP_GENERATE_TOKEN: + pr_info("manual_su: handling token generation request\n"); + return handle_token_generation(request); + + case MANUAL_SU_OP_ESCALATE: + pr_info("manual_su: handling escalation request for UID %d, PID %d\n", + request->target_uid, request->target_pid); + return handle_escalation_request(request); + + case MANUAL_SU_OP_ADD_PENDING: + pr_info("manual_su: handling add pending request for UID %d\n", request->target_uid); + return handle_add_pending_request(request); + + default: + pr_err("manual_su: unknown option %d\n", option); + return -EINVAL; + } +} + +static bool is_current_verified(void) +{ + return current_verified; +} + +bool is_pending_root(uid_t uid) +{ + for (int i = 0; i < pending_cnt; i++) { + if (pending_uids[i].uid == uid) { + pending_uids[i].use_count++; + pending_uids[i].remove_calls++; + return true; + } + } + return false; +} + +void remove_pending_root(uid_t uid) +{ + for (int i = 0; i < pending_cnt; i++) { + if (pending_uids[i].uid == uid) { + pending_uids[i].remove_calls++; + + if (pending_uids[i].remove_calls >= REMOVE_DELAY_CALLS) { + pending_uids[i] = pending_uids[--pending_cnt]; + pr_info("pending_root: removed UID %d after %d calls\n", uid, REMOVE_DELAY_CALLS); + ksu_temp_revoke_root_once(uid); + } else { + pr_info("pending_root: UID %d remove_call=%d (<%d)\n", + uid, pending_uids[i].remove_calls, REMOVE_DELAY_CALLS); + } + return; + } + } +} + +static void add_pending_root(uid_t uid) +{ + if (pending_cnt >= MAX_PENDING) { + pr_warn("pending_root: cache full\n"); + return; + } + for (int i = 0; i < pending_cnt; i++) { + if (pending_uids[i].uid == uid) { + pending_uids[i].use_count = 0; + pending_uids[i].remove_calls = 0; + return; + } + } + pending_uids[pending_cnt++] = (struct pending_uid){uid, 0}; + ksu_temp_grant_root_once(uid); + pr_info("pending_root: cached UID %d\n", uid); +} + +void ksu_try_escalate_for_uid(uid_t uid) +{ + if (!is_pending_root(uid)) + return; + + pr_info("pending_root: UID=%d temporarily allowed\n", uid); + remove_pending_root(uid); +} \ No newline at end of file diff --git a/kernel/manual_su.h b/kernel/manual_su.h new file mode 100644 index 0000000..419dbfc --- /dev/null +++ b/kernel/manual_su.h @@ -0,0 +1,49 @@ +#ifndef __KSU_MANUAL_SU_H +#define __KSU_MANUAL_SU_H + +#include +#include +#include + +#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 7, 0) +#define mmap_lock mmap_sem +#endif + +#define ksu_task_is_dead(t) ((t)->exit_state != 0) + +#define MAX_PENDING 16 +#define REMOVE_DELAY_CALLS 150 +#define MAX_TOKENS 10 + +#define KSU_SU_VERIFIED_BIT (1UL << 0) +#define KSU_TOKEN_LENGTH 32 +#define KSU_TOKEN_ENV_NAME "KSU_AUTH_TOKEN" +#define KSU_TOKEN_EXPIRE_TIME 150 + +#define MANUAL_SU_OP_GENERATE_TOKEN 0 +#define MANUAL_SU_OP_ESCALATE 1 +#define MANUAL_SU_OP_ADD_PENDING 2 + +struct pending_uid { + uid_t uid; + int use_count; + int remove_calls; +}; + +struct manual_su_request { + uid_t target_uid; + pid_t target_pid; + char token_buffer[KSU_TOKEN_LENGTH + 1]; +}; + +struct ksu_token_entry { + char token[KSU_TOKEN_LENGTH + 1]; + unsigned long expire_time; + bool used; +}; + +int ksu_handle_manual_su_request(int option, struct manual_su_request *request); +bool is_pending_root(uid_t uid); +void remove_pending_root(uid_t uid); +void ksu_try_escalate_for_uid(uid_t uid); +#endif \ No newline at end of file diff --git a/kernel/pkg_observer.c b/kernel/pkg_observer.c new file mode 100644 index 0000000..b632cd1 --- /dev/null +++ b/kernel/pkg_observer.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0 +#include +#include +#include +#include +#include +#include +#include +#include "klog.h" // IWYU pragma: keep +#include "ksu.h" +#include "throne_tracker.h" +#include "throne_comm.h" + +#define MASK_SYSTEM (FS_CREATE | FS_MOVE | FS_EVENT_ON_CHILD) + +struct watch_dir { + const char *path; + u32 mask; + struct path kpath; + struct inode *inode; + struct fsnotify_mark *mark; +}; + +static struct fsnotify_group *g; + +static int ksu_handle_inode_event(struct fsnotify_mark *mark, u32 mask, + struct inode *inode, struct inode *dir, + const struct qstr *file_name, u32 cookie) +{ + if (!file_name) + return 0; + if (mask & FS_ISDIR) + return 0; + if (file_name->len == 13 && + !memcmp(file_name->name, "packages.list", 13)) { + pr_info("packages.list detected: %d\n", mask); + if (ksu_uid_scanner_enabled) { + ksu_request_userspace_scan(); + } + track_throne(false); + } + return 0; +} + +static const struct fsnotify_ops ksu_ops = { + .handle_inode_event = ksu_handle_inode_event, +}; + +static int add_mark_on_inode(struct inode *inode, u32 mask, + struct fsnotify_mark **out) +{ + struct fsnotify_mark *m; + + m = kzalloc(sizeof(*m), GFP_KERNEL); + if (!m) + return -ENOMEM; + + fsnotify_init_mark(m, g); + m->mask = mask; + + if (fsnotify_add_inode_mark(m, inode, 0)) { + fsnotify_put_mark(m); + return -EINVAL; + } + *out = m; + return 0; +} + +static int watch_one_dir(struct watch_dir *wd) +{ + int ret = kern_path(wd->path, LOOKUP_FOLLOW, &wd->kpath); + if (ret) { + pr_info("path not ready: %s (%d)\n", wd->path, ret); + return ret; + } + wd->inode = d_inode(wd->kpath.dentry); + ihold(wd->inode); + + ret = add_mark_on_inode(wd->inode, wd->mask, &wd->mark); + if (ret) { + pr_err("Add mark failed for %s (%d)\n", wd->path, ret); + path_put(&wd->kpath); + iput(wd->inode); + wd->inode = NULL; + return ret; + } + pr_info("watching %s\n", wd->path); + return 0; +} + +static void unwatch_one_dir(struct watch_dir *wd) +{ + if (wd->mark) { + fsnotify_destroy_mark(wd->mark, g); + fsnotify_put_mark(wd->mark); + wd->mark = NULL; + } + if (wd->inode) { + iput(wd->inode); + wd->inode = NULL; + } + if (wd->kpath.dentry) { + path_put(&wd->kpath); + memset(&wd->kpath, 0, sizeof(wd->kpath)); + } +} + +static struct watch_dir g_watch = { .path = "/data/system", + .mask = MASK_SYSTEM }; + +int ksu_observer_init(void) +{ + int ret = 0; + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 0, 0) + g = fsnotify_alloc_group(&ksu_ops, 0); +#else + g = fsnotify_alloc_group(&ksu_ops); +#endif + if (IS_ERR(g)) + return PTR_ERR(g); + + ret = watch_one_dir(&g_watch); + pr_info("observer init done\n"); + return 0; +} + +void ksu_observer_exit(void) +{ + unwatch_one_dir(&g_watch); + fsnotify_put_group(g); + pr_info("observer exit done\n"); +} \ No newline at end of file diff --git a/kernel/seccomp_cache.c b/kernel/seccomp_cache.c new file mode 100644 index 0000000..286b5ca --- /dev/null +++ b/kernel/seccomp_cache.c @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include +#include +#include +#include "klog.h" // IWYU pragma: keep +#include "seccomp_cache.h" + + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 2) // Android backport this feature in 5.10.2 +struct action_cache { + DECLARE_BITMAP(allow_native, SECCOMP_ARCH_NATIVE_NR); +#ifdef SECCOMP_ARCH_COMPAT + DECLARE_BITMAP(allow_compat, SECCOMP_ARCH_COMPAT_NR); +#endif +}; + +struct seccomp_filter { + refcount_t refs; + refcount_t users; + bool log; +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 1, 0) + bool wait_killable_recv; +#endif + struct action_cache cache; + struct seccomp_filter *prev; + struct bpf_prog *prog; + struct notification *notif; + struct mutex notify_lock; + wait_queue_head_t wqh; +}; + +void ksu_seccomp_clear_cache(struct seccomp_filter *filter, int nr) +{ + if (!filter) { + return; + } + + if (nr >= 0 && nr < SECCOMP_ARCH_NATIVE_NR) { + clear_bit(nr, filter->cache.allow_native); + } + +#ifdef SECCOMP_ARCH_COMPAT + if (nr >= 0 && nr < SECCOMP_ARCH_COMPAT_NR) { + clear_bit(nr, filter->cache.allow_compat); + } +#endif +} + +void ksu_seccomp_allow_cache(struct seccomp_filter *filter, int nr) +{ + if (!filter) { + return; + } + + if (nr >= 0 && nr < SECCOMP_ARCH_NATIVE_NR) { + set_bit(nr, filter->cache.allow_native); + } + +#ifdef SECCOMP_ARCH_COMPAT + if (nr >= 0 && nr < SECCOMP_ARCH_COMPAT_NR) { + set_bit(nr, filter->cache.allow_compat); + } +#endif +} + +#endif \ No newline at end of file diff --git a/kernel/seccomp_cache.h b/kernel/seccomp_cache.h new file mode 100644 index 0000000..ce88328 --- /dev/null +++ b/kernel/seccomp_cache.h @@ -0,0 +1,12 @@ +#ifndef __KSU_H_SECCOMP_CACHE +#define __KSU_H_SECCOMP_CACHE + +#include +#include + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 2) // Android backport this feature in 5.10.2 +extern void ksu_seccomp_clear_cache(struct seccomp_filter *filter, int nr); +extern void ksu_seccomp_allow_cache(struct seccomp_filter *filter, int nr); +#endif + +#endif \ No newline at end of file diff --git a/kernel/selinux/Makefile b/kernel/selinux/Makefile index 870750b..d35413d 100644 --- a/kernel/selinux/Makefile +++ b/kernel/selinux/Makefile @@ -2,15 +2,7 @@ obj-y += selinux.o obj-y += sepolicy.o obj-y += rules.o -ifeq ($(shell grep -q " current_sid(void)" $(srctree)/security/selinux/include/objsec.h; echo $$?),0) -ccflags-y += -DKSU_COMPAT_HAS_CURRENT_SID -endif - -ifeq ($(shell grep -q "struct selinux_state " $(srctree)/security/selinux/include/security.h; echo $$?),0) -ccflags-y += -DKSU_COMPAT_HAS_SELINUX_STATE -endif - -ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion +ccflags-y += -Wno-strict-prototypes -Wno-int-conversion ccflags-y += -Wno-declaration-after-statement -Wno-unused-function ccflags-y += -I$(srctree)/security/selinux -I$(srctree)/security/selinux/include ccflags-y += -I$(objtree)/security/selinux -include $(srctree)/include/uapi/asm-generic/errno.h diff --git a/kernel/selinux/rules.c b/kernel/selinux/rules.c index c60159c..98d7475 100644 --- a/kernel/selinux/rules.c +++ b/kernel/selinux/rules.c @@ -6,7 +6,7 @@ #include "selinux.h" #include "sepolicy.h" #include "ss/services.h" -#include "linux/lsm_audit.h" +#include "linux/lsm_audit.h" // IWYU pragma: keep #include "xfrm.h" #define SELINUX_POLICY_INSTEAD_SELINUX_SS @@ -18,119 +18,119 @@ static struct policydb *get_policydb(void) { - struct policydb *db; - struct selinux_policy *policy = selinux_state.policy; - db = &policy->policydb; - return db; + struct policydb *db; + struct selinux_policy *policy = selinux_state.policy; + db = &policy->policydb; + return db; } static DEFINE_MUTEX(ksu_rules); void apply_kernelsu_rules() { - struct policydb *db; + struct policydb *db; - if (!getenforce()) { - pr_info("SELinux permissive or disabled, apply rules!\n"); - } + if (!getenforce()) { + pr_info("SELinux permissive or disabled, apply rules!\n"); + } - mutex_lock(&ksu_rules); + mutex_lock(&ksu_rules); - db = get_policydb(); + db = get_policydb(); - ksu_permissive(db, KERNEL_SU_DOMAIN); - ksu_typeattribute(db, KERNEL_SU_DOMAIN, "mlstrustedsubject"); - ksu_typeattribute(db, KERNEL_SU_DOMAIN, "netdomain"); - ksu_typeattribute(db, KERNEL_SU_DOMAIN, "bluetoothdomain"); + ksu_permissive(db, KERNEL_SU_DOMAIN); + ksu_typeattribute(db, KERNEL_SU_DOMAIN, "mlstrustedsubject"); + ksu_typeattribute(db, KERNEL_SU_DOMAIN, "netdomain"); + ksu_typeattribute(db, KERNEL_SU_DOMAIN, "bluetoothdomain"); - // Create unconstrained file type - ksu_type(db, KERNEL_SU_FILE, "file_type"); - ksu_typeattribute(db, KERNEL_SU_FILE, "mlstrustedobject"); - ksu_allow(db, ALL, KERNEL_SU_FILE, ALL, ALL); + // Create unconstrained file type + ksu_type(db, KERNEL_SU_FILE, "file_type"); + ksu_typeattribute(db, KERNEL_SU_FILE, "mlstrustedobject"); + ksu_allow(db, ALL, KERNEL_SU_FILE, ALL, ALL); - // allow all! - ksu_allow(db, KERNEL_SU_DOMAIN, ALL, ALL, ALL); + // allow all! + ksu_allow(db, KERNEL_SU_DOMAIN, ALL, ALL, ALL); - // allow us do any ioctl - if (db->policyvers >= POLICYDB_VERSION_XPERMS_IOCTL) { - ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "blk_file", ALL); - ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "fifo_file", ALL); - ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "chr_file", ALL); - ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "file", ALL); - } + // allow us do any ioctl + if (db->policyvers >= POLICYDB_VERSION_XPERMS_IOCTL) { + ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "blk_file", ALL); + ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "fifo_file", ALL); + ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "chr_file", ALL); + ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "file", ALL); + } - // we need to save allowlist in /data/adb/ksu - ksu_allow(db, "kernel", "adb_data_file", "dir", ALL); - ksu_allow(db, "kernel", "adb_data_file", "file", ALL); - // we need to search /data/app - ksu_allow(db, "kernel", "apk_data_file", "file", "open"); - ksu_allow(db, "kernel", "apk_data_file", "dir", "open"); - ksu_allow(db, "kernel", "apk_data_file", "dir", "read"); - ksu_allow(db, "kernel", "apk_data_file", "dir", "search"); - // we may need to do mount on shell - ksu_allow(db, "kernel", "shell_data_file", "file", ALL); - // we need to read /data/system/packages.list - ksu_allow(db, "kernel", "kernel", "capability", "dac_override"); - // Android 10+: - // http://aospxref.com/android-12.0.0_r3/xref/system/sepolicy/private/file_contexts#512 - ksu_allow(db, "kernel", "packages_list_file", "file", ALL); - // Kernel 4.4 - ksu_allow(db, "kernel", "packages_list_file", "dir", ALL); - // Android 9-: - // http://aospxref.com/android-9.0.0_r61/xref/system/sepolicy/private/file_contexts#360 - ksu_allow(db, "kernel", "system_data_file", "file", ALL); - ksu_allow(db, "kernel", "system_data_file", "dir", ALL); - // our ksud triggered by init - ksu_allow(db, "init", "adb_data_file", "file", ALL); - ksu_allow(db, "init", "adb_data_file", "dir", ALL); // #1289 - ksu_allow(db, "init", KERNEL_SU_DOMAIN, ALL, ALL); - // we need to umount modules in zygote - ksu_allow(db, "zygote", "adb_data_file", "dir", "search"); + // we need to save allowlist in /data/adb/ksu + ksu_allow(db, "kernel", "adb_data_file", "dir", ALL); + ksu_allow(db, "kernel", "adb_data_file", "file", ALL); + // we need to search /data/app + ksu_allow(db, "kernel", "apk_data_file", "file", "open"); + ksu_allow(db, "kernel", "apk_data_file", "dir", "open"); + ksu_allow(db, "kernel", "apk_data_file", "dir", "read"); + ksu_allow(db, "kernel", "apk_data_file", "dir", "search"); + // we may need to do mount on shell + ksu_allow(db, "kernel", "shell_data_file", "file", ALL); + // we need to read /data/system/packages.list + ksu_allow(db, "kernel", "kernel", "capability", "dac_override"); + // Android 10+: + // http://aospxref.com/android-12.0.0_r3/xref/system/sepolicy/private/file_contexts#512 + ksu_allow(db, "kernel", "packages_list_file", "file", ALL); + // Kernel 4.4 + ksu_allow(db, "kernel", "packages_list_file", "dir", ALL); + // Android 9-: + // http://aospxref.com/android-9.0.0_r61/xref/system/sepolicy/private/file_contexts#360 + ksu_allow(db, "kernel", "system_data_file", "file", ALL); + ksu_allow(db, "kernel", "system_data_file", "dir", ALL); + // our ksud triggered by init + ksu_allow(db, "init", "adb_data_file", "file", ALL); + ksu_allow(db, "init", "adb_data_file", "dir", ALL); // #1289 + ksu_allow(db, "init", KERNEL_SU_DOMAIN, ALL, ALL); + // we need to umount modules in zygote + ksu_allow(db, "zygote", "adb_data_file", "dir", "search"); - // copied from Magisk rules - // suRights - ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "dir", "search"); - ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "dir", "read"); - ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "file", "open"); - ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "file", "read"); - ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "process", "getattr"); - ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "process", "sigchld"); + // copied from Magisk rules + // suRights + ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "dir", "search"); + ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "dir", "read"); + ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "file", "open"); + ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "file", "read"); + ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "process", "getattr"); + ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "process", "sigchld"); - // allowLog - ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "dir", "search"); - ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "read"); - ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "open"); - ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "getattr"); + // allowLog + ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "dir", "search"); + ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "read"); + ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "open"); + ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "getattr"); - // dumpsys - ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fd", "use"); - ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "write"); - ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "read"); - ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "open"); - ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "getattr"); + // dumpsys + ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fd", "use"); + ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "write"); + ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "read"); + ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "open"); + ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "getattr"); - // bootctl - ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "dir", "search"); - ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "file", "read"); - ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "file", "open"); - ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "process", - "getattr"); + // bootctl + ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "dir", "search"); + ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "file", "read"); + ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "file", "open"); + ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "process", + "getattr"); - // For mounting loop devices, mirrors, tmpfs - ksu_allow(db, "kernel", ALL, "file", "read"); - ksu_allow(db, "kernel", ALL, "file", "write"); + // For mounting loop devices, mirrors, tmpfs + ksu_allow(db, "kernel", ALL, "file", "read"); + ksu_allow(db, "kernel", ALL, "file", "write"); - // Allow all binder transactions - ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "binder", ALL); + // Allow all binder transactions + ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "binder", ALL); // Allow system server kill su process ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "getpgid"); ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "sigkill"); - // https://android-review.googlesource.com/c/platform/system/logging/+/3725346 - ksu_dontaudit(db, "untrusted_app", KERNEL_SU_DOMAIN, "dir", "getattr"); + // https://android-review.googlesource.com/c/platform/system/logging/+/3725346 + ksu_dontaudit(db, "untrusted_app", KERNEL_SU_DOMAIN, "dir", "getattr"); - mutex_unlock(&ksu_rules); + mutex_unlock(&ksu_rules); } #define MAX_SEPOL_LEN 128 @@ -145,401 +145,333 @@ void apply_kernelsu_rules() #define CMD_TYPE_CHANGE 8 #define CMD_GENFSCON 9 -#ifdef CONFIG_64BIT struct sepol_data { - u32 cmd; - u32 subcmd; - u64 field_sepol1; - u64 field_sepol2; - u64 field_sepol3; - u64 field_sepol4; - u64 field_sepol5; - u64 field_sepol6; - u64 field_sepol7; + u32 cmd; + u32 subcmd; + char __user *sepol1; + char __user *sepol2; + char __user *sepol3; + char __user *sepol4; + char __user *sepol5; + char __user *sepol6; + char __user *sepol7; }; -#ifdef CONFIG_COMPAT -extern bool ksu_is_compat __read_mostly; -struct sepol_compat_data { - u32 cmd; - u32 subcmd; - u32 field_sepol1; - u32 field_sepol2; - u32 field_sepol3; - u32 field_sepol4; - u32 field_sepol5; - u32 field_sepol6; - u32 field_sepol7; -}; -#endif // CONFIG_COMPAT -#else -struct sepol_data { - u32 cmd; - u32 subcmd; - u32 field_sepol1; - u32 field_sepol2; - u32 field_sepol3; - u32 field_sepol4; - u32 field_sepol5; - u32 field_sepol6; - u32 field_sepol7; -}; -#endif // CONFIG_64BIT static int get_object(char *buf, char __user *user_object, size_t buf_sz, - char **object) + char **object) { - if (!user_object) { - *object = ALL; - return 0; - } + if (!user_object) { + *object = ALL; + return 0; + } - if (strncpy_from_user(buf, user_object, buf_sz) < 0) { - return -1; - } + if (strncpy_from_user(buf, user_object, buf_sz) < 0) { + return -EINVAL; + } - *object = buf; + *object = buf; - return 0; + return 0; } - +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) +extern int avc_ss_reset(u32 seqno); +#else +extern int avc_ss_reset(struct selinux_avc *avc, u32 seqno); +#endif // reset avc cache table, otherwise the new rules will not take effect if already denied static void reset_avc_cache() { #if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) - avc_ss_reset(0); - selnl_notify_policyload(0); - selinux_status_update_policyload(0); + avc_ss_reset(0); + selnl_notify_policyload(0); + selinux_status_update_policyload(0); #else - struct selinux_avc *avc = selinux_state.avc; - avc_ss_reset(avc, 0); - selnl_notify_policyload(0); - selinux_status_update_policyload(&selinux_state, 0); + struct selinux_avc *avc = selinux_state.avc; + avc_ss_reset(avc, 0); + selnl_notify_policyload(0); + selinux_status_update_policyload(&selinux_state, 0); #endif - selinux_xfrm_notify_policyload(); + selinux_xfrm_notify_policyload(); } int handle_sepolicy(unsigned long arg3, void __user *arg4) { - struct policydb *db; + struct policydb *db; - if (!arg4) { - return -1; - } + if (!arg4) { + return -EINVAL; + } - if (!getenforce()) { - pr_info("SELinux permissive or disabled when handle policy!\n"); - } - - u32 cmd, subcmd; - char __user *sepol1, *sepol2, *sepol3, *sepol4, *sepol5, *sepol6, *sepol7; + if (!getenforce()) { + pr_info("SELinux permissive or disabled when handle policy!\n"); + } -#if defined(CONFIG_64BIT) && defined(CONFIG_COMPAT) - if (unlikely(ksu_is_compat)) { - struct sepol_compat_data compat_data; - if (copy_from_user(&compat_data, arg4, sizeof(struct sepol_compat_data))) { - pr_err("sepol: copy sepol_data failed.\n"); - return -1; - } - sepol1 = compat_ptr(compat_data.field_sepol1); - sepol2 = compat_ptr(compat_data.field_sepol2); - sepol3 = compat_ptr(compat_data.field_sepol3); - sepol4 = compat_ptr(compat_data.field_sepol4); - sepol5 = compat_ptr(compat_data.field_sepol5); - sepol6 = compat_ptr(compat_data.field_sepol6); - sepol7 = compat_ptr(compat_data.field_sepol7); - cmd = compat_data.cmd; - subcmd = compat_data.subcmd; - } else { - struct sepol_data data; - if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) { - pr_err("sepol: copy sepol_data failed.\n"); - return -1; - } - sepol1 = data.field_sepol1; - sepol2 = data.field_sepol2; - sepol3 = data.field_sepol3; - sepol4 = data.field_sepol4; - sepol5 = data.field_sepol5; - sepol6 = data.field_sepol6; - sepol7 = data.field_sepol7; - cmd = data.cmd; - subcmd = data.subcmd; - } -#else - // basically for full native, say (64BIT=y COMPAT=n) || (64BIT=n) - struct sepol_data data; - if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) { - pr_err("sepol: copy sepol_data failed.\n"); - return -1; - } - sepol1 = data.field_sepol1; - sepol2 = data.field_sepol2; - sepol3 = data.field_sepol3; - sepol4 = data.field_sepol4; - sepol5 = data.field_sepol5; - sepol6 = data.field_sepol6; - sepol7 = data.field_sepol7; - cmd = data.cmd; - subcmd = data.subcmd; -#endif + struct sepol_data data; + if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) { + pr_err("sepol: copy sepol_data failed.\n"); + return -EINVAL; + } - mutex_lock(&ksu_rules); + u32 cmd = data.cmd; + u32 subcmd = data.subcmd; - db = get_policydb(); + mutex_lock(&ksu_rules); - int ret = -1; - if (cmd == CMD_NORMAL_PERM) { - char src_buf[MAX_SEPOL_LEN]; - char tgt_buf[MAX_SEPOL_LEN]; - char cls_buf[MAX_SEPOL_LEN]; - char perm_buf[MAX_SEPOL_LEN]; + db = get_policydb(); - char *s, *t, *c, *p; - if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) { - pr_err("sepol: copy src failed.\n"); - goto exit; - } + int ret = -EINVAL; + if (cmd == CMD_NORMAL_PERM) { + char src_buf[MAX_SEPOL_LEN]; + char tgt_buf[MAX_SEPOL_LEN]; + char cls_buf[MAX_SEPOL_LEN]; + char perm_buf[MAX_SEPOL_LEN]; - if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) { - pr_err("sepol: copy tgt failed.\n"); - goto exit; - } + char *s, *t, *c, *p; + if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) { + pr_err("sepol: copy src failed.\n"); + goto exit; + } - if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) { - pr_err("sepol: copy cls failed.\n"); - goto exit; - } + if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) { + pr_err("sepol: copy tgt failed.\n"); + goto exit; + } - if (get_object(perm_buf, sepol4, sizeof(perm_buf), &p) < - 0) { - pr_err("sepol: copy perm failed.\n"); - goto exit; - } + if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) { + pr_err("sepol: copy cls failed.\n"); + goto exit; + } - bool success = false; - if (subcmd == 1) { - success = ksu_allow(db, s, t, c, p); - } else if (subcmd == 2) { - success = ksu_deny(db, s, t, c, p); - } else if (subcmd == 3) { - success = ksu_auditallow(db, s, t, c, p); - } else if (subcmd == 4) { - success = ksu_dontaudit(db, s, t, c, p); - } else { - pr_err("sepol: unknown subcmd: %d\n", subcmd); - } - ret = success ? 0 : -1; + if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) < + 0) { + pr_err("sepol: copy perm failed.\n"); + goto exit; + } - } else if (cmd == CMD_XPERM) { - char src_buf[MAX_SEPOL_LEN]; - char tgt_buf[MAX_SEPOL_LEN]; - char cls_buf[MAX_SEPOL_LEN]; + bool success = false; + if (subcmd == 1) { + success = ksu_allow(db, s, t, c, p); + } else if (subcmd == 2) { + success = ksu_deny(db, s, t, c, p); + } else if (subcmd == 3) { + success = ksu_auditallow(db, s, t, c, p); + } else if (subcmd == 4) { + success = ksu_dontaudit(db, s, t, c, p); + } else { + pr_err("sepol: unknown subcmd: %d\n", subcmd); + } + ret = success ? 0 : -EINVAL; - char __maybe_unused - operation[MAX_SEPOL_LEN]; // it is always ioctl now! - char perm_set[MAX_SEPOL_LEN]; + } else if (cmd == CMD_XPERM) { + char src_buf[MAX_SEPOL_LEN]; + char tgt_buf[MAX_SEPOL_LEN]; + char cls_buf[MAX_SEPOL_LEN]; - char *s, *t, *c; - if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) { - pr_err("sepol: copy src failed.\n"); - goto exit; - } - if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) { - pr_err("sepol: copy tgt failed.\n"); - goto exit; - } - if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) { - pr_err("sepol: copy cls failed.\n"); - goto exit; - } - if (strncpy_from_user(operation, sepol4, - sizeof(operation)) < 0) { - pr_err("sepol: copy operation failed.\n"); - goto exit; - } - if (strncpy_from_user(perm_set, sepol5, sizeof(perm_set)) < - 0) { - pr_err("sepol: copy perm_set failed.\n"); - goto exit; - } + char __maybe_unused + operation[MAX_SEPOL_LEN]; // it is always ioctl now! + char perm_set[MAX_SEPOL_LEN]; - bool success = false; - if (subcmd == 1) { - success = ksu_allowxperm(db, s, t, c, perm_set); - } else if (subcmd == 2) { - success = ksu_auditallowxperm(db, s, t, c, perm_set); - } else if (subcmd == 3) { - success = ksu_dontauditxperm(db, s, t, c, perm_set); - } else { - pr_err("sepol: unknown subcmd: %d\n", subcmd); - } - ret = success ? 0 : -1; - } else if (cmd == CMD_TYPE_STATE) { - char src[MAX_SEPOL_LEN]; + char *s, *t, *c; + if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) { + pr_err("sepol: copy src failed.\n"); + goto exit; + } + if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) { + pr_err("sepol: copy tgt failed.\n"); + goto exit; + } + if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) { + pr_err("sepol: copy cls failed.\n"); + goto exit; + } + if (strncpy_from_user(operation, data.sepol4, + sizeof(operation)) < 0) { + pr_err("sepol: copy operation failed.\n"); + goto exit; + } + if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) < + 0) { + pr_err("sepol: copy perm_set failed.\n"); + goto exit; + } - if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) { - pr_err("sepol: copy src failed.\n"); - goto exit; - } + bool success = false; + if (subcmd == 1) { + success = ksu_allowxperm(db, s, t, c, perm_set); + } else if (subcmd == 2) { + success = ksu_auditallowxperm(db, s, t, c, perm_set); + } else if (subcmd == 3) { + success = ksu_dontauditxperm(db, s, t, c, perm_set); + } else { + pr_err("sepol: unknown subcmd: %d\n", subcmd); + } + ret = success ? 0 : -EINVAL; + } else if (cmd == CMD_TYPE_STATE) { + char src[MAX_SEPOL_LEN]; - bool success = false; - if (subcmd == 1) { - success = ksu_permissive(db, src); - } else if (subcmd == 2) { - success = ksu_enforce(db, src); - } else { - pr_err("sepol: unknown subcmd: %d\n", subcmd); - } - if (success) - ret = 0; + if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) { + pr_err("sepol: copy src failed.\n"); + goto exit; + } - } else if (cmd == CMD_TYPE || cmd == CMD_TYPE_ATTR) { - char type[MAX_SEPOL_LEN]; - char attr[MAX_SEPOL_LEN]; + bool success = false; + if (subcmd == 1) { + success = ksu_permissive(db, src); + } else if (subcmd == 2) { + success = ksu_enforce(db, src); + } else { + pr_err("sepol: unknown subcmd: %d\n", subcmd); + } + if (success) + ret = 0; - if (strncpy_from_user(type, sepol1, sizeof(type)) < 0) { - pr_err("sepol: copy type failed.\n"); - goto exit; - } - if (strncpy_from_user(attr, sepol2, sizeof(attr)) < 0) { - pr_err("sepol: copy attr failed.\n"); - goto exit; - } + } else if (cmd == CMD_TYPE || cmd == CMD_TYPE_ATTR) { + char type[MAX_SEPOL_LEN]; + char attr[MAX_SEPOL_LEN]; - bool success = false; - if (cmd == CMD_TYPE) { - success = ksu_type(db, type, attr); - } else { - success = ksu_typeattribute(db, type, attr); - } - if (!success) { - pr_err("sepol: %d failed.\n", cmd); - goto exit; - } - ret = 0; + if (strncpy_from_user(type, data.sepol1, sizeof(type)) < 0) { + pr_err("sepol: copy type failed.\n"); + goto exit; + } + if (strncpy_from_user(attr, data.sepol2, sizeof(attr)) < 0) { + pr_err("sepol: copy attr failed.\n"); + goto exit; + } - } else if (cmd == CMD_ATTR) { - char attr[MAX_SEPOL_LEN]; + bool success = false; + if (cmd == CMD_TYPE) { + success = ksu_type(db, type, attr); + } else { + success = ksu_typeattribute(db, type, attr); + } + if (!success) { + pr_err("sepol: %d failed.\n", cmd); + goto exit; + } + ret = 0; - if (strncpy_from_user(attr, sepol1, sizeof(attr)) < 0) { - pr_err("sepol: copy attr failed.\n"); - goto exit; - } - if (!ksu_attribute(db, attr)) { - pr_err("sepol: %d failed.\n", cmd); - goto exit; - } - ret = 0; + } else if (cmd == CMD_ATTR) { + char attr[MAX_SEPOL_LEN]; - } else if (cmd == CMD_TYPE_TRANSITION) { - char src[MAX_SEPOL_LEN]; - char tgt[MAX_SEPOL_LEN]; - char cls[MAX_SEPOL_LEN]; - char default_type[MAX_SEPOL_LEN]; - char object[MAX_SEPOL_LEN]; + if (strncpy_from_user(attr, data.sepol1, sizeof(attr)) < 0) { + pr_err("sepol: copy attr failed.\n"); + goto exit; + } + if (!ksu_attribute(db, attr)) { + pr_err("sepol: %d failed.\n", cmd); + goto exit; + } + ret = 0; - if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) { - pr_err("sepol: copy src failed.\n"); - goto exit; - } - if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) { - pr_err("sepol: copy tgt failed.\n"); - goto exit; - } - if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) { - pr_err("sepol: copy cls failed.\n"); - goto exit; - } - if (strncpy_from_user(default_type, sepol4, - sizeof(default_type)) < 0) { - pr_err("sepol: copy default_type failed.\n"); - goto exit; - } - char *real_object; - if (sepol5 == NULL) { - real_object = NULL; - } else { - if (strncpy_from_user(object, sepol5, - sizeof(object)) < 0) { - pr_err("sepol: copy object failed.\n"); - goto exit; - } - real_object = object; - } + } else if (cmd == CMD_TYPE_TRANSITION) { + char src[MAX_SEPOL_LEN]; + char tgt[MAX_SEPOL_LEN]; + char cls[MAX_SEPOL_LEN]; + char default_type[MAX_SEPOL_LEN]; + char object[MAX_SEPOL_LEN]; - bool success = ksu_type_transition(db, src, tgt, cls, - default_type, real_object); - if (success) - ret = 0; + if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) { + pr_err("sepol: copy src failed.\n"); + goto exit; + } + if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) { + pr_err("sepol: copy tgt failed.\n"); + goto exit; + } + if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) { + pr_err("sepol: copy cls failed.\n"); + goto exit; + } + if (strncpy_from_user(default_type, data.sepol4, + sizeof(default_type)) < 0) { + pr_err("sepol: copy default_type failed.\n"); + goto exit; + } + char *real_object; + if (data.sepol5 == NULL) { + real_object = NULL; + } else { + if (strncpy_from_user(object, data.sepol5, + sizeof(object)) < 0) { + pr_err("sepol: copy object failed.\n"); + goto exit; + } + real_object = object; + } - } else if (cmd == CMD_TYPE_CHANGE) { - char src[MAX_SEPOL_LEN]; - char tgt[MAX_SEPOL_LEN]; - char cls[MAX_SEPOL_LEN]; - char default_type[MAX_SEPOL_LEN]; + bool success = ksu_type_transition(db, src, tgt, cls, + default_type, real_object); + if (success) + ret = 0; - if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) { - pr_err("sepol: copy src failed.\n"); - goto exit; - } - if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) { - pr_err("sepol: copy tgt failed.\n"); - goto exit; - } - if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) { - pr_err("sepol: copy cls failed.\n"); - goto exit; - } - if (strncpy_from_user(default_type, sepol4, - sizeof(default_type)) < 0) { - pr_err("sepol: copy default_type failed.\n"); - goto exit; - } - bool success = false; - if (subcmd == 1) { - success = ksu_type_change(db, src, tgt, cls, - default_type); - } else if (subcmd == 2) { - success = ksu_type_member(db, src, tgt, cls, - default_type); - } else { - pr_err("sepol: unknown subcmd: %d\n", subcmd); - } - if (success) - ret = 0; - } else if (cmd == CMD_GENFSCON) { - char name[MAX_SEPOL_LEN]; - char path[MAX_SEPOL_LEN]; - char context[MAX_SEPOL_LEN]; - if (strncpy_from_user(name, sepol1, sizeof(name)) < 0) { - pr_err("sepol: copy name failed.\n"); - goto exit; - } - if (strncpy_from_user(path, sepol2, sizeof(path)) < 0) { - pr_err("sepol: copy path failed.\n"); - goto exit; - } - if (strncpy_from_user(context, sepol3, sizeof(context)) < - 0) { - pr_err("sepol: copy context failed.\n"); - goto exit; - } + } else if (cmd == CMD_TYPE_CHANGE) { + char src[MAX_SEPOL_LEN]; + char tgt[MAX_SEPOL_LEN]; + char cls[MAX_SEPOL_LEN]; + char default_type[MAX_SEPOL_LEN]; - if (!ksu_genfscon(db, name, path, context)) { - pr_err("sepol: %d failed.\n", cmd); - goto exit; - } - ret = 0; - } else { - pr_err("sepol: unknown cmd: %d\n", cmd); - } + if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) { + pr_err("sepol: copy src failed.\n"); + goto exit; + } + if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) { + pr_err("sepol: copy tgt failed.\n"); + goto exit; + } + if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) { + pr_err("sepol: copy cls failed.\n"); + goto exit; + } + if (strncpy_from_user(default_type, data.sepol4, + sizeof(default_type)) < 0) { + pr_err("sepol: copy default_type failed.\n"); + goto exit; + } + bool success = false; + if (subcmd == 1) { + success = ksu_type_change(db, src, tgt, cls, + default_type); + } else if (subcmd == 2) { + success = ksu_type_member(db, src, tgt, cls, + default_type); + } else { + pr_err("sepol: unknown subcmd: %d\n", subcmd); + } + if (success) + ret = 0; + } else if (cmd == CMD_GENFSCON) { + char name[MAX_SEPOL_LEN]; + char path[MAX_SEPOL_LEN]; + char context[MAX_SEPOL_LEN]; + if (strncpy_from_user(name, data.sepol1, sizeof(name)) < 0) { + pr_err("sepol: copy name failed.\n"); + goto exit; + } + if (strncpy_from_user(path, data.sepol2, sizeof(path)) < 0) { + pr_err("sepol: copy path failed.\n"); + goto exit; + } + if (strncpy_from_user(context, data.sepol3, sizeof(context)) < + 0) { + pr_err("sepol: copy context failed.\n"); + goto exit; + } + + if (!ksu_genfscon(db, name, path, context)) { + pr_err("sepol: %d failed.\n", cmd); + goto exit; + } + ret = 0; + } else { + pr_err("sepol: unknown cmd: %d\n", cmd); + } exit: - mutex_unlock(&ksu_rules); + mutex_unlock(&ksu_rules); - // only allow and xallow needs to reset avc cache, but we cannot do that because - // we are in atomic context. so we just reset it every time. - reset_avc_cache(); + // only allow and xallow needs to reset avc cache, but we cannot do that because + // we are in atomic context. so we just reset it every time. + reset_avc_cache(); - return ret; -} + return ret; +} \ No newline at end of file diff --git a/kernel/selinux/selinux.c b/kernel/selinux/selinux.c index 17a25da..dfc4831 100644 --- a/kernel/selinux/selinux.c +++ b/kernel/selinux/selinux.c @@ -1,4 +1,6 @@ #include "selinux.h" +#include "linux/cred.h" +#include "linux/sched.h" #include "objsec.h" #include "linux/version.h" #include "../klog.h" // IWYU pragma: keep @@ -7,124 +9,146 @@ static int transive_to_domain(const char *domain) { - struct cred *cred; - struct task_security_struct *tsec; - u32 sid; - int error; + struct cred *cred; + struct task_security_struct *tsec; + u32 sid; + int error; - cred = (struct cred *)__task_cred(current); + cred = (struct cred *)__task_cred(current); - tsec = cred->security; - if (!tsec) { - pr_err("tsec == NULL!\n"); - return -1; - } + tsec = cred->security; + if (!tsec) { + pr_err("tsec == NULL!\n"); + return -1; + } - error = security_secctx_to_secid(domain, strlen(domain), &sid); - if (error) { - pr_info("security_secctx_to_secid %s -> sid: %d, error: %d\n", - domain, sid, error); - } - if (!error) { - tsec->sid = sid; - tsec->create_sid = 0; - tsec->keycreate_sid = 0; - tsec->sockcreate_sid = 0; - } - return error; + error = security_secctx_to_secid(domain, strlen(domain), &sid); + if (error) { + pr_info("security_secctx_to_secid %s -> sid: %d, error: %d\n", + domain, sid, error); + } + if (!error) { + tsec->sid = sid; + tsec->create_sid = 0; + tsec->keycreate_sid = 0; + tsec->sockcreate_sid = 0; + } + return error; } void setup_selinux(const char *domain) { - if (transive_to_domain(domain)) { - pr_err("transive domain failed.\n"); - return; - } - - /* we didn't need this now, we have change selinux rules when boot! -if (!is_domain_permissive) { - if (set_domain_permissive() == 0) { - is_domain_permissive = true; - } -}*/ + if (transive_to_domain(domain)) { + pr_err("transive domain failed.\n"); + return; + } } void setenforce(bool enforce) { #ifdef CONFIG_SECURITY_SELINUX_DEVELOP - selinux_state.enforcing = enforce; + selinux_state.enforcing = enforce; #endif } bool getenforce() { #ifdef CONFIG_SECURITY_SELINUX_DISABLE - if (selinux_state.disabled) { - return false; - } + if (selinux_state.disabled) { + return false; + } #endif #ifdef CONFIG_SECURITY_SELINUX_DEVELOP - return selinux_state.enforcing; + return selinux_state.enforcing; #else - return true; + return true; #endif } -#if (LINUX_VERSION_CODE < KERNEL_VERSION(5, 10, 0)) && \ - !defined(KSU_COMPAT_HAS_CURRENT_SID) -/* - * get the subjective security ID of the current task - */ -static inline u32 current_sid(void) +#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 14, 0) +struct lsm_context { + char *context; + u32 len; +}; + +static int __security_secid_to_secctx(u32 secid, struct lsm_context *cp) { - const struct task_security_struct *tsec = current_security(); - - return tsec->sid; + return security_secid_to_secctx(secid, &cp->context, &cp->len); } +static void __security_release_secctx(struct lsm_context *cp) +{ + return security_release_secctx(cp->context, cp->len); +} +#else +#define __security_secid_to_secctx security_secid_to_secctx +#define __security_release_secctx security_release_secctx #endif +bool is_task_ksu_domain(const struct cred* cred) +{ + struct lsm_context ctx; + bool result; + if (!cred) { + return false; + } + const struct task_security_struct *tsec = selinux_cred(cred); + if (!tsec) { + return false; + } + int err = __security_secid_to_secctx(tsec->sid, &ctx); + if (err) { + return false; + } + result = strncmp(KERNEL_SU_DOMAIN, ctx.context, ctx.len) == 0; + __security_release_secctx(&ctx); + return result; +} + bool is_ksu_domain() { - char *domain; - u32 seclen; - bool result; - int err = security_secid_to_secctx(current_sid(), &domain, &seclen); - if (err) { - return false; - } - result = strncmp(KERNEL_SU_DOMAIN, domain, seclen) == 0; - security_release_secctx(domain, seclen); - return result; + current_sid(); + return is_task_ksu_domain(current_cred()); } -bool is_zygote(void *sec) +bool is_context(const struct cred* cred, const char* context) { - struct task_security_struct *tsec = (struct task_security_struct *)sec; - if (!tsec) { - return false; - } - char *domain; - u32 seclen; - bool result; - int err = security_secid_to_secctx(tsec->sid, &domain, &seclen); - if (err) { - return false; - } - result = strncmp("u:r:zygote:s0", domain, seclen) == 0; - security_release_secctx(domain, seclen); - return result; + if (!cred) { + return false; + } + const struct task_security_struct * tsec = selinux_cred(cred); + if (!tsec) { + return false; + } + struct lsm_context ctx; + bool result; + int err = __security_secid_to_secctx(tsec->sid, &ctx); + if (err) { + return false; + } + result = strncmp(context, ctx.context, ctx.len) == 0; + __security_release_secctx(&ctx); + return result; } -#define DEVPTS_DOMAIN "u:object_r:ksu_file:s0" - -u32 ksu_get_devpts_sid() +bool is_zygote(const struct cred* cred) { - u32 devpts_sid = 0; - int err = security_secctx_to_secid(DEVPTS_DOMAIN, strlen(DEVPTS_DOMAIN), - &devpts_sid); - if (err) { - pr_info("get devpts sid err %d\n", err); - } - return devpts_sid; + return is_context(cred, "u:r:zygote:s0"); +} + +bool is_init(const struct cred* cred) { + return is_context(cred, "u:r:init:s0"); +} + +#define KSU_FILE_DOMAIN "u:object_r:ksu_file:s0" + +u32 ksu_get_ksu_file_sid() +{ + u32 ksu_file_sid = 0; + int err = security_secctx_to_secid(KSU_FILE_DOMAIN, strlen(KSU_FILE_DOMAIN), + &ksu_file_sid); + if (err) { + pr_info("get ksufile sid err %d\n", err); + } + return ksu_file_sid; } diff --git a/kernel/selinux/selinux.h b/kernel/selinux/selinux.h index 88f1e7d..431e044 100644 --- a/kernel/selinux/selinux.h +++ b/kernel/selinux/selinux.h @@ -3,6 +3,7 @@ #include "linux/types.h" #include "linux/version.h" +#include "linux/cred.h" void setup_selinux(const char *); @@ -10,12 +11,18 @@ void setenforce(bool); bool getenforce(); +bool is_task_ksu_domain(const struct cred* cred); + bool is_ksu_domain(); -bool is_zygote(void *cred); +bool is_zygote(const struct cred* cred); + +bool is_init(const struct cred* cred); void apply_kernelsu_rules(); -u32 ksu_get_devpts_sid(); +u32 ksu_get_ksu_file_sid(); + +int handle_sepolicy(unsigned long arg3, void __user *arg4); #endif diff --git a/kernel/selinux/sepolicy.c b/kernel/selinux/sepolicy.c index 7759602..b8e0ec8 100644 --- a/kernel/selinux/sepolicy.c +++ b/kernel/selinux/sepolicy.c @@ -6,7 +6,6 @@ #include "sepolicy.h" #include "../klog.h" // IWYU pragma: keep #include "ss/symtab.h" -#include "../kernel_compat.h" // Add check Huawei Device #define KSU_SUPPORT_ADD_TYPE @@ -15,44 +14,44 @@ ////////////////////////////////////////////////////// static struct avtab_node *get_avtab_node(struct policydb *db, - struct avtab_key *key, - struct avtab_extended_perms *xperms); + struct avtab_key *key, + struct avtab_extended_perms *xperms); static bool add_rule(struct policydb *db, const char *s, const char *t, - const char *c, const char *p, int effect, bool invert); + const char *c, const char *p, int effect, bool invert); static void add_rule_raw(struct policydb *db, struct type_datum *src, - struct type_datum *tgt, struct class_datum *cls, - struct perm_datum *perm, int effect, bool invert); + struct type_datum *tgt, struct class_datum *cls, + struct perm_datum *perm, int effect, bool invert); static void add_xperm_rule_raw(struct policydb *db, struct type_datum *src, - struct type_datum *tgt, struct class_datum *cls, - uint16_t low, uint16_t high, int effect, - bool invert); + struct type_datum *tgt, struct class_datum *cls, + uint16_t low, uint16_t high, int effect, + bool invert); static bool add_xperm_rule(struct policydb *db, const char *s, const char *t, - const char *c, const char *range, int effect, - bool invert); + const char *c, const char *range, int effect, + bool invert); static bool add_type_rule(struct policydb *db, const char *s, const char *t, - const char *c, const char *d, int effect); + const char *c, const char *d, int effect); static bool add_filename_trans(struct policydb *db, const char *s, - const char *t, const char *c, const char *d, - const char *o); + const char *t, const char *c, const char *d, + const char *o); static bool add_genfscon(struct policydb *db, const char *fs_name, - const char *path, const char *context); + const char *path, const char *context); static bool add_type(struct policydb *db, const char *type_name, bool attr); static bool set_type_state(struct policydb *db, const char *type_name, - bool permissive); + bool permissive); static void add_typeattribute_raw(struct policydb *db, struct type_datum *type, - struct type_datum *attr); + struct type_datum *attr); static bool add_typeattribute(struct policydb *db, const char *type, - const char *attr); + const char *attr); ////////////////////////////////////////////////////// // Implementation @@ -63,18 +62,18 @@ static bool add_typeattribute(struct policydb *db, const char *type, #define strip_av(effect, invert) ((effect == AVTAB_AUDITDENY) == !invert) #define ksu_hash_for_each(node_ptr, n_slot, cur) \ - int i; \ - for (i = 0; i < n_slot; ++i) \ - for (cur = node_ptr[i]; cur; cur = cur->next) + int i; \ + for (i = 0; i < n_slot; ++i) \ + for (cur = node_ptr[i]; cur; cur = cur->next) // htable is a struct instead of pointer above 5.8.0: // https://elixir.bootlin.com/linux/v5.8-rc1/source/security/selinux/ss/symtab.h #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 8, 0) #define ksu_hashtab_for_each(htab, cur) \ - ksu_hash_for_each(htab.htable, htab.size, cur) + ksu_hash_for_each(htab.htable, htab.size, cur) #else #define ksu_hashtab_for_each(htab, cur) \ - ksu_hash_for_each(htab->htable, htab->size, cur) + ksu_hash_for_each(htab->htable, htab->size, cur) #endif // symtab_search is introduced on 5.9.0: @@ -85,186 +84,186 @@ static bool add_typeattribute(struct policydb *db, const char *type, #endif #define avtab_for_each(avtab, cur) \ - ksu_hash_for_each(avtab.htable, avtab.nslot, cur); + ksu_hash_for_each(avtab.htable, avtab.nslot, cur); static struct avtab_node *get_avtab_node(struct policydb *db, - struct avtab_key *key, - struct avtab_extended_perms *xperms) + struct avtab_key *key, + struct avtab_extended_perms *xperms) { - struct avtab_node *node; + struct avtab_node *node; - /* AVTAB_XPERMS entries are not necessarily unique */ - if (key->specified & AVTAB_XPERMS) { - bool match = false; - node = avtab_search_node(&db->te_avtab, key); - while (node) { - if ((node->datum.u.xperms->specified == - xperms->specified) && - (node->datum.u.xperms->driver == xperms->driver)) { - match = true; - break; - } - node = avtab_search_node_next(node, key->specified); - } - if (!match) - node = NULL; - } else { - node = avtab_search_node(&db->te_avtab, key); - } + /* AVTAB_XPERMS entries are not necessarily unique */ + if (key->specified & AVTAB_XPERMS) { + bool match = false; + node = avtab_search_node(&db->te_avtab, key); + while (node) { + if ((node->datum.u.xperms->specified == + xperms->specified) && + (node->datum.u.xperms->driver == xperms->driver)) { + match = true; + break; + } + node = avtab_search_node_next(node, key->specified); + } + if (!match) + node = NULL; + } else { + node = avtab_search_node(&db->te_avtab, key); + } - if (!node) { - struct avtab_datum avdatum = {}; - /* + if (!node) { + struct avtab_datum avdatum = {}; + /* * AUDITDENY, aka DONTAUDIT, are &= assigned, versus |= for * others. Initialize the data accordingly. */ - if (key->specified & AVTAB_XPERMS) { - avdatum.u.xperms = xperms; - } else { - avdatum.u.data = - key->specified == AVTAB_AUDITDENY ? ~0U : 0U; - } - /* this is used to get the node - insertion is actually unique */ - node = avtab_insert_nonunique(&db->te_avtab, key, &avdatum); + if (key->specified & AVTAB_XPERMS) { + avdatum.u.xperms = xperms; + } else { + avdatum.u.data = + key->specified == AVTAB_AUDITDENY ? ~0U : 0U; + } + /* this is used to get the node - insertion is actually unique */ + node = avtab_insert_nonunique(&db->te_avtab, key, &avdatum); - int grow_size = sizeof(struct avtab_key); - grow_size += sizeof(struct avtab_datum); - if (key->specified & AVTAB_XPERMS) { - grow_size += sizeof(u8); - grow_size += sizeof(u8); - grow_size += sizeof(u32) * - ARRAY_SIZE(avdatum.u.xperms->perms.p); - } - db->len += grow_size; - } + int grow_size = sizeof(struct avtab_key); + grow_size += sizeof(struct avtab_datum); + if (key->specified & AVTAB_XPERMS) { + grow_size += sizeof(u8); + grow_size += sizeof(u8); + grow_size += sizeof(u32) * + ARRAY_SIZE(avdatum.u.xperms->perms.p); + } + db->len += grow_size; + } - return node; + return node; } static bool add_rule(struct policydb *db, const char *s, const char *t, - const char *c, const char *p, int effect, bool invert) + const char *c, const char *p, int effect, bool invert) { - struct type_datum *src = NULL, *tgt = NULL; - struct class_datum *cls = NULL; - struct perm_datum *perm = NULL; + struct type_datum *src = NULL, *tgt = NULL; + struct class_datum *cls = NULL; + struct perm_datum *perm = NULL; - if (s) { - src = symtab_search(&db->p_types, s); - if (src == NULL) { - pr_info("source type %s does not exist\n", s); - return false; - } - } + if (s) { + src = symtab_search(&db->p_types, s); + if (src == NULL) { + pr_info("source type %s does not exist\n", s); + return false; + } + } - if (t) { - tgt = symtab_search(&db->p_types, t); - if (tgt == NULL) { - pr_info("target type %s does not exist\n", t); - return false; - } - } + if (t) { + tgt = symtab_search(&db->p_types, t); + if (tgt == NULL) { + pr_info("target type %s does not exist\n", t); + return false; + } + } - if (c) { - cls = symtab_search(&db->p_classes, c); - if (cls == NULL) { - pr_info("class %s does not exist\n", c); - return false; - } - } + if (c) { + cls = symtab_search(&db->p_classes, c); + if (cls == NULL) { + pr_info("class %s does not exist\n", c); + return false; + } + } - if (p) { - if (c == NULL) { - pr_info("No class is specified, cannot add perm [%s] \n", - p); - return false; - } + if (p) { + if (c == NULL) { + pr_info("No class is specified, cannot add perm [%s] \n", + p); + return false; + } - perm = symtab_search(&cls->permissions, p); - if (perm == NULL && cls->comdatum != NULL) { - perm = symtab_search(&cls->comdatum->permissions, p); - } - if (perm == NULL) { - pr_info("perm %s does not exist in class %s\n", p, c); - return false; - } - } - add_rule_raw(db, src, tgt, cls, perm, effect, invert); - return true; + perm = symtab_search(&cls->permissions, p); + if (perm == NULL && cls->comdatum != NULL) { + perm = symtab_search(&cls->comdatum->permissions, p); + } + if (perm == NULL) { + pr_info("perm %s does not exist in class %s\n", p, c); + return false; + } + } + add_rule_raw(db, src, tgt, cls, perm, effect, invert); + return true; } static void add_rule_raw(struct policydb *db, struct type_datum *src, - struct type_datum *tgt, struct class_datum *cls, - struct perm_datum *perm, int effect, bool invert) + struct type_datum *tgt, struct class_datum *cls, + struct perm_datum *perm, int effect, bool invert) { - if (src == NULL) { - struct hashtab_node *node; - if (strip_av(effect, invert)) { - ksu_hashtab_for_each(db->p_types.table, node) - { - add_rule_raw(db, - (struct type_datum *)node->datum, - tgt, cls, perm, effect, invert); - }; - } else { - ksu_hashtab_for_each(db->p_types.table, node) - { - struct type_datum *type = - (struct type_datum *)(node->datum); - if (type->attribute) { - add_rule_raw(db, type, tgt, cls, perm, - effect, invert); - } - }; - } - } else if (tgt == NULL) { - struct hashtab_node *node; - if (strip_av(effect, invert)) { - ksu_hashtab_for_each(db->p_types.table, node) - { - add_rule_raw(db, src, - (struct type_datum *)node->datum, - cls, perm, effect, invert); - }; - } else { - ksu_hashtab_for_each(db->p_types.table, node) - { - struct type_datum *type = - (struct type_datum *)(node->datum); - if (type->attribute) { - add_rule_raw(db, src, type, cls, perm, - effect, invert); - } - }; - } - } else if (cls == NULL) { - struct hashtab_node *node; - ksu_hashtab_for_each(db->p_classes.table, node) - { - add_rule_raw(db, src, tgt, - (struct class_datum *)node->datum, perm, - effect, invert); - } - } else { - struct avtab_key key; - key.source_type = src->value; - key.target_type = tgt->value; - key.target_class = cls->value; - key.specified = effect; + if (src == NULL) { + struct hashtab_node *node; + if (strip_av(effect, invert)) { + ksu_hashtab_for_each(db->p_types.table, node) + { + add_rule_raw(db, + (struct type_datum *)node->datum, + tgt, cls, perm, effect, invert); + }; + } else { + ksu_hashtab_for_each(db->p_types.table, node) + { + struct type_datum *type = + (struct type_datum *)(node->datum); + if (type->attribute) { + add_rule_raw(db, type, tgt, cls, perm, + effect, invert); + } + }; + } + } else if (tgt == NULL) { + struct hashtab_node *node; + if (strip_av(effect, invert)) { + ksu_hashtab_for_each(db->p_types.table, node) + { + add_rule_raw(db, src, + (struct type_datum *)node->datum, + cls, perm, effect, invert); + }; + } else { + ksu_hashtab_for_each(db->p_types.table, node) + { + struct type_datum *type = + (struct type_datum *)(node->datum); + if (type->attribute) { + add_rule_raw(db, src, type, cls, perm, + effect, invert); + } + }; + } + } else if (cls == NULL) { + struct hashtab_node *node; + ksu_hashtab_for_each(db->p_classes.table, node) + { + add_rule_raw(db, src, tgt, + (struct class_datum *)node->datum, perm, + effect, invert); + } + } else { + struct avtab_key key; + key.source_type = src->value; + key.target_type = tgt->value; + key.target_class = cls->value; + key.specified = effect; - struct avtab_node *node = get_avtab_node(db, &key, NULL); - if (invert) { - if (perm) - node->datum.u.data &= - ~(1U << (perm->value - 1)); - else - node->datum.u.data = 0U; - } else { - if (perm) - node->datum.u.data |= 1U << (perm->value - 1); - else - node->datum.u.data = ~0U; - } - } + struct avtab_node *node = get_avtab_node(db, &key, NULL); + if (invert) { + if (perm) + node->datum.u.data &= + ~(1U << (perm->value - 1)); + else + node->datum.u.data = 0U; + } else { + if (perm) + node->datum.u.data |= 1U << (perm->value - 1); + else + node->datum.u.data = ~0U; + } + } } #define ioctl_driver(x) (x >> 8 & 0xFF) @@ -275,183 +274,183 @@ static void add_rule_raw(struct policydb *db, struct type_datum *src, #define xperm_clear(x, p) (p[x >> 5] &= ~(1 << (x & 0x1f))) static void add_xperm_rule_raw(struct policydb *db, struct type_datum *src, - struct type_datum *tgt, struct class_datum *cls, - uint16_t low, uint16_t high, int effect, - bool invert) + struct type_datum *tgt, struct class_datum *cls, + uint16_t low, uint16_t high, int effect, + bool invert) { - if (src == NULL) { - struct hashtab_node *node; - ksu_hashtab_for_each(db->p_types.table, node) - { - struct type_datum *type = - (struct type_datum *)(node->datum); - if (type->attribute) { - add_xperm_rule_raw(db, type, tgt, cls, low, - high, effect, invert); - } - }; - } else if (tgt == NULL) { - struct hashtab_node *node; - ksu_hashtab_for_each(db->p_types.table, node) - { - struct type_datum *type = - (struct type_datum *)(node->datum); - if (type->attribute) { - add_xperm_rule_raw(db, src, type, cls, low, - high, effect, invert); - } - }; - } else if (cls == NULL) { - struct hashtab_node *node; - ksu_hashtab_for_each(db->p_classes.table, node) - { - add_xperm_rule_raw(db, src, tgt, - (struct class_datum *)(node->datum), - low, high, effect, invert); - }; - } else { - struct avtab_key key; - key.source_type = src->value; - key.target_type = tgt->value; - key.target_class = cls->value; - key.specified = effect; + if (src == NULL) { + struct hashtab_node *node; + ksu_hashtab_for_each(db->p_types.table, node) + { + struct type_datum *type = + (struct type_datum *)(node->datum); + if (type->attribute) { + add_xperm_rule_raw(db, type, tgt, cls, low, + high, effect, invert); + } + }; + } else if (tgt == NULL) { + struct hashtab_node *node; + ksu_hashtab_for_each(db->p_types.table, node) + { + struct type_datum *type = + (struct type_datum *)(node->datum); + if (type->attribute) { + add_xperm_rule_raw(db, src, type, cls, low, + high, effect, invert); + } + }; + } else if (cls == NULL) { + struct hashtab_node *node; + ksu_hashtab_for_each(db->p_classes.table, node) + { + add_xperm_rule_raw(db, src, tgt, + (struct class_datum *)(node->datum), + low, high, effect, invert); + }; + } else { + struct avtab_key key; + key.source_type = src->value; + key.target_type = tgt->value; + key.target_class = cls->value; + key.specified = effect; - struct avtab_datum *datum; - struct avtab_node *node; - struct avtab_extended_perms xperms; + struct avtab_datum *datum; + struct avtab_node *node; + struct avtab_extended_perms xperms; - memset(&xperms, 0, sizeof(xperms)); - if (ioctl_driver(low) != ioctl_driver(high)) { - xperms.specified = AVTAB_XPERMS_IOCTLDRIVER; - xperms.driver = 0; - } else { - xperms.specified = AVTAB_XPERMS_IOCTLFUNCTION; - xperms.driver = ioctl_driver(low); - } - int i; - if (xperms.specified == AVTAB_XPERMS_IOCTLDRIVER) { - for (i = ioctl_driver(low); i <= ioctl_driver(high); - ++i) { - if (invert) - xperm_clear(i, xperms.perms.p); - else - xperm_set(i, xperms.perms.p); - } - } else { - for (i = ioctl_func(low); i <= ioctl_func(high); ++i) { - if (invert) - xperm_clear(i, xperms.perms.p); - else - xperm_set(i, xperms.perms.p); - } - } + memset(&xperms, 0, sizeof(xperms)); + if (ioctl_driver(low) != ioctl_driver(high)) { + xperms.specified = AVTAB_XPERMS_IOCTLDRIVER; + xperms.driver = 0; + } else { + xperms.specified = AVTAB_XPERMS_IOCTLFUNCTION; + xperms.driver = ioctl_driver(low); + } + int i; + if (xperms.specified == AVTAB_XPERMS_IOCTLDRIVER) { + for (i = ioctl_driver(low); i <= ioctl_driver(high); + ++i) { + if (invert) + xperm_clear(i, xperms.perms.p); + else + xperm_set(i, xperms.perms.p); + } + } else { + for (i = ioctl_func(low); i <= ioctl_func(high); ++i) { + if (invert) + xperm_clear(i, xperms.perms.p); + else + xperm_set(i, xperms.perms.p); + } + } - node = get_avtab_node(db, &key, &xperms); - if (!node) { - pr_warn("add_xperm_rule_raw cannot found node!\n"); - return; - } - datum = &node->datum; + node = get_avtab_node(db, &key, &xperms); + if (!node) { + pr_warn("add_xperm_rule_raw cannot found node!\n"); + return; + } + datum = &node->datum; - if (datum->u.xperms == NULL) { - datum->u.xperms = - (struct avtab_extended_perms *)(kmalloc( - sizeof(xperms), GFP_KERNEL)); - if (!datum->u.xperms) { - pr_err("alloc xperms failed\n"); - return; - } - memcpy(datum->u.xperms, &xperms, sizeof(xperms)); - } - } + if (datum->u.xperms == NULL) { + datum->u.xperms = + (struct avtab_extended_perms *)(kzalloc( + sizeof(xperms), GFP_KERNEL)); + if (!datum->u.xperms) { + pr_err("alloc xperms failed\n"); + return; + } + memcpy(datum->u.xperms, &xperms, sizeof(xperms)); + } + } } static bool add_xperm_rule(struct policydb *db, const char *s, const char *t, - const char *c, const char *range, int effect, - bool invert) + const char *c, const char *range, int effect, + bool invert) { - struct type_datum *src = NULL, *tgt = NULL; - struct class_datum *cls = NULL; + struct type_datum *src = NULL, *tgt = NULL; + struct class_datum *cls = NULL; - if (s) { - src = symtab_search(&db->p_types, s); - if (src == NULL) { - pr_info("source type %s does not exist\n", s); - return false; - } - } + if (s) { + src = symtab_search(&db->p_types, s); + if (src == NULL) { + pr_info("source type %s does not exist\n", s); + return false; + } + } - if (t) { - tgt = symtab_search(&db->p_types, t); - if (tgt == NULL) { - pr_info("target type %s does not exist\n", t); - return false; - } - } + if (t) { + tgt = symtab_search(&db->p_types, t); + if (tgt == NULL) { + pr_info("target type %s does not exist\n", t); + return false; + } + } - if (c) { - cls = symtab_search(&db->p_classes, c); - if (cls == NULL) { - pr_info("class %s does not exist\n", c); - return false; - } - } + if (c) { + cls = symtab_search(&db->p_classes, c); + if (cls == NULL) { + pr_info("class %s does not exist\n", c); + return false; + } + } - u16 low, high; + u16 low, high; - if (range) { - if (strchr(range, '-')) { - sscanf(range, "%hx-%hx", &low, &high); - } else { - sscanf(range, "%hx", &low); - high = low; - } - } else { - low = 0; - high = 0xFFFF; - } + if (range) { + if (strchr(range, '-')) { + sscanf(range, "%hx-%hx", &low, &high); + } else { + sscanf(range, "%hx", &low); + high = low; + } + } else { + low = 0; + high = 0xFFFF; + } - add_xperm_rule_raw(db, src, tgt, cls, low, high, effect, invert); - return true; + add_xperm_rule_raw(db, src, tgt, cls, low, high, effect, invert); + return true; } static bool add_type_rule(struct policydb *db, const char *s, const char *t, - const char *c, const char *d, int effect) + const char *c, const char *d, int effect) { - struct type_datum *src, *tgt, *def; - struct class_datum *cls; + struct type_datum *src, *tgt, *def; + struct class_datum *cls; - src = symtab_search(&db->p_types, s); - if (src == NULL) { - pr_info("source type %s does not exist\n", s); - return false; - } - tgt = symtab_search(&db->p_types, t); - if (tgt == NULL) { - pr_info("target type %s does not exist\n", t); - return false; - } - cls = symtab_search(&db->p_classes, c); - if (cls == NULL) { - pr_info("class %s does not exist\n", c); - return false; - } - def = symtab_search(&db->p_types, d); - if (def == NULL) { - pr_info("default type %s does not exist\n", d); - return false; - } + src = symtab_search(&db->p_types, s); + if (src == NULL) { + pr_info("source type %s does not exist\n", s); + return false; + } + tgt = symtab_search(&db->p_types, t); + if (tgt == NULL) { + pr_info("target type %s does not exist\n", t); + return false; + } + cls = symtab_search(&db->p_classes, c); + if (cls == NULL) { + pr_info("class %s does not exist\n", c); + return false; + } + def = symtab_search(&db->p_types, d); + if (def == NULL) { + pr_info("default type %s does not exist\n", d); + return false; + } - struct avtab_key key; - key.source_type = src->value; - key.target_type = tgt->value; - key.target_class = cls->value; - key.specified = effect; + struct avtab_key key; + key.source_type = src->value; + key.target_type = tgt->value; + key.target_class = cls->value; + key.specified = effect; - struct avtab_node *node = get_avtab_node(db, &key, NULL); - node->datum.u.data = def->value; + struct avtab_node *node = get_avtab_node(db, &key, NULL); + node->datum.u.data = def->value; - return true; + return true; } // 5.9.0 : static inline int hashtab_insert(struct hashtab *h, void *key, void @@ -460,287 +459,287 @@ static bool add_type_rule(struct policydb *db, const char *s, const char *t, #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0) static u32 filenametr_hash(const void *k) { - const struct filename_trans_key *ft = k; - unsigned long hash; - unsigned int byte_num; - unsigned char focus; + const struct filename_trans_key *ft = k; + unsigned long hash; + unsigned int byte_num; + unsigned char focus; - hash = ft->ttype ^ ft->tclass; + hash = ft->ttype ^ ft->tclass; - byte_num = 0; - while ((focus = ft->name[byte_num++])) - hash = partial_name_hash(focus, hash); - return hash; + byte_num = 0; + while ((focus = ft->name[byte_num++])) + hash = partial_name_hash(focus, hash); + return hash; } static int filenametr_cmp(const void *k1, const void *k2) { - const struct filename_trans_key *ft1 = k1; - const struct filename_trans_key *ft2 = k2; - int v; + const struct filename_trans_key *ft1 = k1; + const struct filename_trans_key *ft2 = k2; + int v; - v = ft1->ttype - ft2->ttype; - if (v) - return v; + v = ft1->ttype - ft2->ttype; + if (v) + return v; - v = ft1->tclass - ft2->tclass; - if (v) - return v; + v = ft1->tclass - ft2->tclass; + if (v) + return v; - return strcmp(ft1->name, ft2->name); + return strcmp(ft1->name, ft2->name); } static const struct hashtab_key_params filenametr_key_params = { - .hash = filenametr_hash, - .cmp = filenametr_cmp, + .hash = filenametr_hash, + .cmp = filenametr_cmp, }; #endif static bool add_filename_trans(struct policydb *db, const char *s, - const char *t, const char *c, const char *d, - const char *o) + const char *t, const char *c, const char *d, + const char *o) { - struct type_datum *src, *tgt, *def; - struct class_datum *cls; + struct type_datum *src, *tgt, *def; + struct class_datum *cls; - src = symtab_search(&db->p_types, s); - if (src == NULL) { - pr_warn("source type %s does not exist\n", s); - return false; - } - tgt = symtab_search(&db->p_types, t); - if (tgt == NULL) { - pr_warn("target type %s does not exist\n", t); - return false; - } - cls = symtab_search(&db->p_classes, c); - if (cls == NULL) { - pr_warn("class %s does not exist\n", c); - return false; - } - def = symtab_search(&db->p_types, d); - if (def == NULL) { - pr_warn("default type %s does not exist\n", d); - return false; - } + src = symtab_search(&db->p_types, s); + if (src == NULL) { + pr_warn("source type %s does not exist\n", s); + return false; + } + tgt = symtab_search(&db->p_types, t); + if (tgt == NULL) { + pr_warn("target type %s does not exist\n", t); + return false; + } + cls = symtab_search(&db->p_classes, c); + if (cls == NULL) { + pr_warn("class %s does not exist\n", c); + return false; + } + def = symtab_search(&db->p_types, d); + if (def == NULL) { + pr_warn("default type %s does not exist\n", d); + return false; + } - struct filename_trans_key key; - key.ttype = tgt->value; - key.tclass = cls->value; - key.name = (char *)o; + struct filename_trans_key key; + key.ttype = tgt->value; + key.tclass = cls->value; + key.name = (char *)o; - struct filename_trans_datum *last = NULL; + struct filename_trans_datum *last = NULL; - struct filename_trans_datum *trans = - policydb_filenametr_search(db, &key); - while (trans) { - if (ebitmap_get_bit(&trans->stypes, src->value - 1)) { - // Duplicate, overwrite existing data and return - trans->otype = def->value; - return true; - } - if (trans->otype == def->value) - break; - last = trans; - trans = trans->next; - } + struct filename_trans_datum *trans = + policydb_filenametr_search(db, &key); + while (trans) { + if (ebitmap_get_bit(&trans->stypes, src->value - 1)) { + // Duplicate, overwrite existing data and return + trans->otype = def->value; + return true; + } + if (trans->otype == def->value) + break; + last = trans; + trans = trans->next; + } - if (trans == NULL) { - trans = (struct filename_trans_datum *)kcalloc(sizeof(*trans), - 1, GFP_ATOMIC); - struct filename_trans_key *new_key = - (struct filename_trans_key *)kmalloc(sizeof(*new_key), - GFP_ATOMIC); - *new_key = key; - new_key->name = kstrdup(key.name, GFP_ATOMIC); - trans->next = last; - trans->otype = def->value; - hashtab_insert(&db->filename_trans, new_key, trans, - filenametr_key_params); - } + if (trans == NULL) { + trans = (struct filename_trans_datum *)kcalloc(1 ,sizeof(*trans), + GFP_ATOMIC); + struct filename_trans_key *new_key = + (struct filename_trans_key *)kzalloc(sizeof(*new_key), + GFP_ATOMIC); + *new_key = key; + new_key->name = kstrdup(key.name, GFP_ATOMIC); + trans->next = last; + trans->otype = def->value; + hashtab_insert(&db->filename_trans, new_key, trans, + filenametr_key_params); + } - db->compat_filename_trans_count++; - return ebitmap_set_bit(&trans->stypes, src->value - 1, 1) == 0; + db->compat_filename_trans_count++; + return ebitmap_set_bit(&trans->stypes, src->value - 1, 1) == 0; } static bool add_genfscon(struct policydb *db, const char *fs_name, - const char *path, const char *context) + const char *path, const char *context) { - return false; + return false; } static void *ksu_realloc(void *old, size_t new_size, size_t old_size) { - // we can't use krealloc, because it may be read-only - void *new = kzalloc(new_size, GFP_ATOMIC); - if (!new) { - return NULL; - } - if (old_size) { - memcpy(new, old, old_size); - } - // we can't use kfree, because it may be read-only - // there maybe some leaks, maybe we can check ptr_write, but it's not a big deal - // kfree(old); - return new; + // we can't use krealloc, because it may be read-only + void *new = kzalloc(new_size, GFP_ATOMIC); + if (!new) { + return NULL; + } + if (old_size) { + memcpy(new, old, old_size); + } + // we can't use kfree, because it may be read-only + // there maybe some leaks, maybe we can check ptr_write, but it's not a big deal + // kfree(old); + return new; } static bool add_type(struct policydb *db, const char *type_name, bool attr) { - struct type_datum *type = symtab_search(&db->p_types, type_name); - if (type) { - pr_warn("Type %s already exists\n", type_name); - return true; - } + struct type_datum *type = symtab_search(&db->p_types, type_name); + if (type) { + pr_warn("Type %s already exists\n", type_name); + return true; + } - u32 value = ++db->p_types.nprim; - type = (struct type_datum *)kzalloc(sizeof(struct type_datum), - GFP_ATOMIC); - if (!type) { - pr_err("add_type: alloc type_datum failed.\n"); - return false; - } + u32 value = ++db->p_types.nprim; + type = (struct type_datum *)kzalloc(sizeof(struct type_datum), + GFP_ATOMIC); + if (!type) { + pr_err("add_type: alloc type_datum failed.\n"); + return false; + } - type->primary = 1; - type->value = value; - type->attribute = attr; + type->primary = 1; + type->value = value; + type->attribute = attr; - char *key = kstrdup(type_name, GFP_ATOMIC); - if (!key) { - pr_err("add_type: alloc key failed.\n"); - return false; - } + char *key = kstrdup(type_name, GFP_ATOMIC); + if (!key) { + pr_err("add_type: alloc key failed.\n"); + return false; + } - if (symtab_insert(&db->p_types, key, type)) { - pr_err("add_type: insert symtab failed.\n"); - return false; - } + if (symtab_insert(&db->p_types, key, type)) { + pr_err("add_type: insert symtab failed.\n"); + return false; + } - struct ebitmap *new_type_attr_map_array = - ksu_realloc(db->type_attr_map_array, - value * sizeof(struct ebitmap), - (value - 1) * sizeof(struct ebitmap)); + struct ebitmap *new_type_attr_map_array = + ksu_realloc(db->type_attr_map_array, + value * sizeof(struct ebitmap), + (value - 1) * sizeof(struct ebitmap)); - if (!new_type_attr_map_array) { - pr_err("add_type: alloc type_attr_map_array failed\n"); - return false; - } + if (!new_type_attr_map_array) { + pr_err("add_type: alloc type_attr_map_array failed\n"); + return false; + } - struct type_datum **new_type_val_to_struct = - ksu_realloc(db->type_val_to_struct, - sizeof(*db->type_val_to_struct) * value, - sizeof(*db->type_val_to_struct) * (value - 1)); + struct type_datum **new_type_val_to_struct = + ksu_realloc(db->type_val_to_struct, + sizeof(*db->type_val_to_struct) * value, + sizeof(*db->type_val_to_struct) * (value - 1)); - if (!new_type_val_to_struct) { - pr_err("add_type: alloc type_val_to_struct failed\n"); - return false; - } + if (!new_type_val_to_struct) { + pr_err("add_type: alloc type_val_to_struct failed\n"); + return false; + } - char **new_val_to_name_types = - ksu_realloc(db->sym_val_to_name[SYM_TYPES], - sizeof(char *) * value, - sizeof(char *) * (value - 1)); - if (!new_val_to_name_types) { - pr_err("add_type: alloc val_to_name failed\n"); - return false; - } + char **new_val_to_name_types = + ksu_realloc(db->sym_val_to_name[SYM_TYPES], + sizeof(char *) * value, + sizeof(char *) * (value - 1)); + if (!new_val_to_name_types) { + pr_err("add_type: alloc val_to_name failed\n"); + return false; + } - db->type_attr_map_array = new_type_attr_map_array; - ebitmap_init(&db->type_attr_map_array[value - 1]); - ebitmap_set_bit(&db->type_attr_map_array[value - 1], value - 1, 1); + db->type_attr_map_array = new_type_attr_map_array; + ebitmap_init(&db->type_attr_map_array[value - 1]); + ebitmap_set_bit(&db->type_attr_map_array[value - 1], value - 1, 1); - db->type_val_to_struct = new_type_val_to_struct; - db->type_val_to_struct[value - 1] = type; + db->type_val_to_struct = new_type_val_to_struct; + db->type_val_to_struct[value - 1] = type; - db->sym_val_to_name[SYM_TYPES] = new_val_to_name_types; - db->sym_val_to_name[SYM_TYPES][value - 1] = key; + db->sym_val_to_name[SYM_TYPES] = new_val_to_name_types; + db->sym_val_to_name[SYM_TYPES][value - 1] = key; - int i; - for (i = 0; i < db->p_roles.nprim; ++i) { - ebitmap_set_bit(&db->role_val_to_struct[i]->types, value - 1, - 1); - } + int i; + for (i = 0; i < db->p_roles.nprim; ++i) { + ebitmap_set_bit(&db->role_val_to_struct[i]->types, value - 1, + 1); + } - return true; + return true; } static bool set_type_state(struct policydb *db, const char *type_name, - bool permissive) + bool permissive) { - struct type_datum *type; - if (type_name == NULL) { - struct hashtab_node *node; - ksu_hashtab_for_each(db->p_types.table, node) - { - type = (struct type_datum *)(node->datum); - if (ebitmap_set_bit(&db->permissive_map, type->value, - permissive)) - pr_info("Could not set bit in permissive map\n"); - }; - } else { - type = (struct type_datum *)symtab_search(&db->p_types, - type_name); - if (type == NULL) { - pr_info("type %s does not exist\n", type_name); - return false; - } - if (ebitmap_set_bit(&db->permissive_map, type->value, - permissive)) { - pr_info("Could not set bit in permissive map\n"); - return false; - } - } - return true; + struct type_datum *type; + if (type_name == NULL) { + struct hashtab_node *node; + ksu_hashtab_for_each(db->p_types.table, node) + { + type = (struct type_datum *)(node->datum); + if (ebitmap_set_bit(&db->permissive_map, type->value, + permissive)) + pr_info("Could not set bit in permissive map\n"); + }; + } else { + type = (struct type_datum *)symtab_search(&db->p_types, + type_name); + if (type == NULL) { + pr_info("type %s does not exist\n", type_name); + return false; + } + if (ebitmap_set_bit(&db->permissive_map, type->value, + permissive)) { + pr_info("Could not set bit in permissive map\n"); + return false; + } + } + return true; } static void add_typeattribute_raw(struct policydb *db, struct type_datum *type, - struct type_datum *attr) + struct type_datum *attr) { - struct ebitmap *sattr = &db->type_attr_map_array[type->value - 1]; - ebitmap_set_bit(sattr, attr->value - 1, 1); + struct ebitmap *sattr = &db->type_attr_map_array[type->value - 1]; + ebitmap_set_bit(sattr, attr->value - 1, 1); - struct hashtab_node *node; - struct constraint_node *n; - struct constraint_expr *e; - ksu_hashtab_for_each(db->p_classes.table, node) - { - struct class_datum *cls = (struct class_datum *)(node->datum); - for (n = cls->constraints; n; n = n->next) { - for (e = n->expr; e; e = e->next) { - if (e->expr_type == CEXPR_NAMES && - ebitmap_get_bit(&e->type_names->types, - attr->value - 1)) { - ebitmap_set_bit(&e->names, - type->value - 1, 1); - } - } - } - }; + struct hashtab_node *node; + struct constraint_node *n; + struct constraint_expr *e; + ksu_hashtab_for_each(db->p_classes.table, node) + { + struct class_datum *cls = (struct class_datum *)(node->datum); + for (n = cls->constraints; n; n = n->next) { + for (e = n->expr; e; e = e->next) { + if (e->expr_type == CEXPR_NAMES && + ebitmap_get_bit(&e->type_names->types, + attr->value - 1)) { + ebitmap_set_bit(&e->names, + type->value - 1, 1); + } + } + } + }; } static bool add_typeattribute(struct policydb *db, const char *type, - const char *attr) + const char *attr) { - struct type_datum *type_d = symtab_search(&db->p_types, type); - if (type_d == NULL) { - pr_info("type %s does not exist\n", type); - return false; - } else if (type_d->attribute) { - pr_info("type %s is an attribute\n", attr); - return false; - } + struct type_datum *type_d = symtab_search(&db->p_types, type); + if (type_d == NULL) { + pr_info("type %s does not exist\n", type); + return false; + } else if (type_d->attribute) { + pr_info("type %s is an attribute\n", attr); + return false; + } - struct type_datum *attr_d = symtab_search(&db->p_types, attr); - if (attr_d == NULL) { - pr_info("attribute %s does not exist\n", type); - return false; - } else if (!attr_d->attribute) { - pr_info("type %s is not an attribute \n", attr); - return false; - } + struct type_datum *attr_d = symtab_search(&db->p_types, attr); + if (attr_d == NULL) { + pr_info("attribute %s does not exist\n", type); + return false; + } else if (!attr_d->attribute) { + pr_info("type %s is not an attribute \n", attr); + return false; + } - add_typeattribute_raw(db, type_d, attr_d); - return true; + add_typeattribute_raw(db, type_d, attr_d); + return true; } ////////////////////////////////////////////////////////////////////////// @@ -748,106 +747,106 @@ static bool add_typeattribute(struct policydb *db, const char *type, // Operation on types bool ksu_type(struct policydb *db, const char *name, const char *attr) { - return add_type(db, name, false) && add_typeattribute(db, name, attr); + return add_type(db, name, false) && add_typeattribute(db, name, attr); } bool ksu_attribute(struct policydb *db, const char *name) { - return add_type(db, name, true); + return add_type(db, name, true); } bool ksu_permissive(struct policydb *db, const char *type) { - return set_type_state(db, type, true); + return set_type_state(db, type, true); } bool ksu_enforce(struct policydb *db, const char *type) { - return set_type_state(db, type, false); + return set_type_state(db, type, false); } bool ksu_typeattribute(struct policydb *db, const char *type, const char *attr) { - return add_typeattribute(db, type, attr); + return add_typeattribute(db, type, attr); } bool ksu_exists(struct policydb *db, const char *type) { - return symtab_search(&db->p_types, type) != NULL; + return symtab_search(&db->p_types, type) != NULL; } // Access vector rules bool ksu_allow(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm) + const char *cls, const char *perm) { - return add_rule(db, src, tgt, cls, perm, AVTAB_ALLOWED, false); + return add_rule(db, src, tgt, cls, perm, AVTAB_ALLOWED, false); } bool ksu_deny(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm) + const char *cls, const char *perm) { - return add_rule(db, src, tgt, cls, perm, AVTAB_ALLOWED, true); + return add_rule(db, src, tgt, cls, perm, AVTAB_ALLOWED, true); } bool ksu_auditallow(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm) + const char *cls, const char *perm) { - return add_rule(db, src, tgt, cls, perm, AVTAB_AUDITALLOW, false); + return add_rule(db, src, tgt, cls, perm, AVTAB_AUDITALLOW, false); } bool ksu_dontaudit(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm) + const char *cls, const char *perm) { - return add_rule(db, src, tgt, cls, perm, AVTAB_AUDITDENY, true); + return add_rule(db, src, tgt, cls, perm, AVTAB_AUDITDENY, true); } // Extended permissions access vector rules bool ksu_allowxperm(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *range) + const char *cls, const char *range) { - return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_ALLOWED, - false); + return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_ALLOWED, + false); } bool ksu_auditallowxperm(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *range) + const char *cls, const char *range) { - return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_AUDITALLOW, - false); + return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_AUDITALLOW, + false); } bool ksu_dontauditxperm(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *range) + const char *cls, const char *range) { - return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_DONTAUDIT, - false); + return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_DONTAUDIT, + false); } // Type rules bool ksu_type_transition(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *def, const char *obj) + const char *cls, const char *def, const char *obj) { - if (obj) { - return add_filename_trans(db, src, tgt, cls, def, obj); - } else { - return add_type_rule(db, src, tgt, cls, def, AVTAB_TRANSITION); - } + if (obj) { + return add_filename_trans(db, src, tgt, cls, def, obj); + } else { + return add_type_rule(db, src, tgt, cls, def, AVTAB_TRANSITION); + } } bool ksu_type_change(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *def) + const char *cls, const char *def) { - return add_type_rule(db, src, tgt, cls, def, AVTAB_CHANGE); + return add_type_rule(db, src, tgt, cls, def, AVTAB_CHANGE); } bool ksu_type_member(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *def) + const char *cls, const char *def) { - return add_type_rule(db, src, tgt, cls, def, AVTAB_MEMBER); + return add_type_rule(db, src, tgt, cls, def, AVTAB_MEMBER); } // File system labeling bool ksu_genfscon(struct policydb *db, const char *fs_name, const char *path, - const char *ctx) + const char *ctx) { - return add_genfscon(db, fs_name, path, ctx); + return add_genfscon(db, fs_name, path, ctx); } diff --git a/kernel/selinux/sepolicy.h b/kernel/selinux/sepolicy.h index 675d149..fd062ce 100644 --- a/kernel/selinux/sepolicy.h +++ b/kernel/selinux/sepolicy.h @@ -15,32 +15,32 @@ bool ksu_exists(struct policydb *db, const char *type); // Access vector rules bool ksu_allow(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm); + const char *cls, const char *perm); bool ksu_deny(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm); + const char *cls, const char *perm); bool ksu_auditallow(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm); + const char *cls, const char *perm); bool ksu_dontaudit(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm); + const char *cls, const char *perm); // Extended permissions access vector rules bool ksu_allowxperm(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *range); + const char *cls, const char *range); bool ksu_auditallowxperm(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *range); + const char *cls, const char *range); bool ksu_dontauditxperm(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *range); + const char *cls, const char *range); // Type rules bool ksu_type_transition(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *def, const char *obj); + const char *cls, const char *def, const char *obj); bool ksu_type_change(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *def); + const char *cls, const char *def); bool ksu_type_member(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *def); + const char *cls, const char *def); // File system labeling bool ksu_genfscon(struct policydb *db, const char *fs_name, const char *path, - const char *ctx); + const char *ctx); #endif diff --git a/kernel/setuid_hook.c b/kernel/setuid_hook.c new file mode 100644 index 0000000..44dffd9 --- /dev/null +++ b/kernel/setuid_hook.c @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "allowlist.h" +#include "setuid_hook.h" +#include "feature.h" +#include "klog.h" // IWYU pragma: keep +#include "manager.h" +#include "selinux/selinux.h" +#include "seccomp_cache.h" +#include "supercalls.h" +#include "syscall_hook_manager.h" +#include "kernel_umount.h" +#include "app_profile.h" + +static bool ksu_enhanced_security_enabled = false; + +static int enhanced_security_feature_get(u64 *value) +{ + *value = ksu_enhanced_security_enabled ? 1 : 0; + return 0; +} + +static int enhanced_security_feature_set(u64 value) +{ + bool enable = value != 0; + ksu_enhanced_security_enabled = enable; + pr_info("enhanced_security: set to %d\n", enable); + return 0; +} + +static const struct ksu_feature_handler enhanced_security_handler = { + .feature_id = KSU_FEATURE_ENHANCED_SECURITY, + .name = "enhanced_security", + .get_handler = enhanced_security_feature_get, + .set_handler = enhanced_security_feature_set, +}; + +static inline bool is_allow_su() +{ + if (is_manager()) { + // we are manager, allow! + return true; + } + return ksu_is_allow_uid_for_current(current_uid().val); +} + +int ksu_handle_setresuid(uid_t ruid, uid_t euid, uid_t suid) +{ + uid_t new_uid = ruid; + uid_t old_uid = current_uid().val; + + pr_info("handle_setresuid from %d to %d\n", old_uid, new_uid); + + // if old process is root, ignore it. + if (old_uid != 0 && ksu_enhanced_security_enabled) { + // disallow any non-ksu domain escalation from non-root to root! + // euid is what we care about here as it controls permission + if (unlikely(euid == 0)) { + if (!is_ksu_domain()) { + pr_warn("find suspicious EoP: %d %s, from %d to %d\n", + current->pid, current->comm, old_uid, new_uid); + force_sig(SIGKILL); + return 0; + } + } + // disallow appuid decrease to any other uid if it is not allowed to su + if (is_appuid(old_uid)) { + if (euid < current_euid().val && !ksu_is_allow_uid_for_current(old_uid)) { + pr_warn("find suspicious EoP: %d %s, from %d to %d\n", + current->pid, current->comm, old_uid, new_uid); + force_sig(SIGKILL); + return 0; + } + } + return 0; + } + + // if on private space, see if its possibly the manager + if (new_uid > PER_USER_RANGE && new_uid % PER_USER_RANGE == ksu_get_manager_uid()) { + ksu_set_manager_uid(new_uid); + } + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0) + if (ksu_get_manager_uid() == new_uid) { + pr_info("install fd for manager: %d\n", new_uid); + ksu_install_fd(); + spin_lock_irq(¤t->sighand->siglock); + ksu_seccomp_allow_cache(current->seccomp.filter, __NR_reboot); + ksu_set_task_tracepoint_flag(current); + spin_unlock_irq(¤t->sighand->siglock); + return 0; + } + + if (ksu_is_allow_uid_for_current(new_uid)) { + if (current->seccomp.mode == SECCOMP_MODE_FILTER && + current->seccomp.filter) { + spin_lock_irq(¤t->sighand->siglock); + ksu_seccomp_allow_cache(current->seccomp.filter, __NR_reboot); + spin_unlock_irq(¤t->sighand->siglock); + } + ksu_set_task_tracepoint_flag(current); + } else { + ksu_clear_task_tracepoint_flag_if_needed(current); + } +#else + if (ksu_is_allow_uid_for_current(new_uid)) { + spin_lock_irq(¤t->sighand->siglock); + disable_seccomp(); + spin_unlock_irq(¤t->sighand->siglock); + + if (ksu_get_manager_uid() == new_uid) { + pr_info("install fd for ksu manager(uid=%d)\n", + new_uid); + ksu_install_fd(); + } + + return 0; + } +#endif + + // Handle kernel umount + ksu_handle_umount(old_uid, new_uid); + + return 0; +} + +void ksu_setuid_hook_init(void) +{ + ksu_kernel_umount_init(); + if (ksu_register_feature_handler(&enhanced_security_handler)) { + pr_err("Failed to register enhanced security feature handler\n"); + } +} + +void ksu_setuid_hook_exit(void) +{ + pr_info("ksu_core_exit\n"); + ksu_kernel_umount_exit(); + ksu_unregister_feature_handler(KSU_FEATURE_ENHANCED_SECURITY); +} diff --git a/kernel/setuid_hook.h b/kernel/setuid_hook.h new file mode 100644 index 0000000..fc5b93a --- /dev/null +++ b/kernel/setuid_hook.h @@ -0,0 +1,14 @@ +#ifndef __KSU_H_KSU_CORE +#define __KSU_H_KSU_CORE + +#include +#include +#include "apk_sign.h" +#include + +void ksu_setuid_hook_init(void); +void ksu_setuid_hook_exit(void); + +int ksu_handle_setresuid(uid_t ruid, uid_t euid, uid_t suid); + +#endif diff --git a/kernel/sucompat.c b/kernel/sucompat.c index ca94a60..f31e319 100644 --- a/kernel/sucompat.c +++ b/kernel/sucompat.c @@ -1,337 +1,187 @@ -#include -#include +#include "linux/compiler.h" +#include "linux/printk.h" #include #include -#include #include -#include #include #include #include #include +#include -#include "objsec.h" #include "allowlist.h" -#include "arch.h" +#include "feature.h" #include "klog.h" // IWYU pragma: keep #include "ksud.h" -#include "kernel_compat.h" +#include "sucompat.h" +#include "app_profile.h" +#include "syscall_hook_manager.h" + +#include "sulog.h" #define SU_PATH "/system/bin/su" #define SH_PATH "/system/bin/sh" -extern void escape_to_root(); +bool ksu_su_compat_enabled __read_mostly = true; -#ifndef CONFIG_KSU_KPROBES_HOOK -static bool ksu_sucompat_hook_state __read_mostly = true; -#endif +static int su_compat_feature_get(u64 *value) +{ + *value = ksu_su_compat_enabled ? 1 : 0; + return 0; +} + +static int su_compat_feature_set(u64 value) +{ + bool enable = value != 0; + ksu_su_compat_enabled = enable; + pr_info("su_compat: set to %d\n", enable); + return 0; +} + +static const struct ksu_feature_handler su_compat_handler = { + .feature_id = KSU_FEATURE_SU_COMPAT, + .name = "su_compat", + .get_handler = su_compat_feature_get, + .set_handler = su_compat_feature_set, +}; static void __user *userspace_stack_buffer(const void *d, size_t len) { - /* To avoid having to mmap a page in userspace, just write below the stack - * pointer. */ - char __user *p = (void __user *)current_user_stack_pointer() - len; + // To avoid having to mmap a page in userspace, just write below the stack + // pointer. + char __user *p = (void __user *)current_user_stack_pointer() - len; - return copy_to_user(p, d, len) ? NULL : p; + return copy_to_user(p, d, len) ? NULL : p; } static char __user *sh_user_path(void) { - static const char sh_path[] = "/system/bin/sh"; + static const char sh_path[] = "/system/bin/sh"; - return userspace_stack_buffer(sh_path, sizeof(sh_path)); + return userspace_stack_buffer(sh_path, sizeof(sh_path)); } static char __user *ksud_user_path(void) { - static const char ksud_path[] = KSUD_PATH; + static const char ksud_path[] = KSUD_PATH; - return userspace_stack_buffer(ksud_path, sizeof(ksud_path)); + return userspace_stack_buffer(ksud_path, sizeof(ksud_path)); } int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, - int *__unused_flags) + int *__unused_flags) { - const char su[] = SU_PATH; + const char su[] = SU_PATH; -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_sucompat_hook_state) { - return 0; - } + if (!ksu_is_allow_uid_for_current(current_uid().val)) { + return 0; + } + + char path[sizeof(su) + 1]; + memset(path, 0, sizeof(path)); + strncpy_from_user_nofault(path, *filename_user, sizeof(path)); + + if (unlikely(!memcmp(path, su, sizeof(su)))) { +#if __SULOG_GATE + ksu_sulog_report_syscall(current_uid().val, NULL, "faccessat", path); #endif + pr_info("faccessat su->sh!\n"); + *filename_user = sh_user_path(); + } - if (!ksu_is_allow_uid(current_uid().val)) { - return 0; - } - - char path[sizeof(su) + 1]; - memset(path, 0, sizeof(path)); - ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path)); - - if (unlikely(!memcmp(path, su, sizeof(su)))) { - pr_info("faccessat su->sh!\n"); - *filename_user = sh_user_path(); - } - - return 0; + return 0; } int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags) { - // const char sh[] = SH_PATH; - const char su[] = SU_PATH; + // const char sh[] = SH_PATH; + const char su[] = SU_PATH; -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_sucompat_hook_state) { - return 0; - } -#endif - if (!ksu_is_allow_uid(current_uid().val)) { - return 0; - } + if (!ksu_is_allow_uid_for_current(current_uid().val)) { + return 0; + } - if (unlikely(!filename_user)) { - return 0; - } + if (unlikely(!filename_user)) { + return 0; + } - char path[sizeof(su) + 1]; - memset(path, 0, sizeof(path)); + char path[sizeof(su) + 1]; + memset(path, 0, sizeof(path)); // Remove this later!! we use syscall hook, so this will never happen!!!!! #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 18, 0) && 0 - // it becomes a `struct filename *` after 5.18 - // https://elixir.bootlin.com/linux/v5.18/source/fs/stat.c#L216 - const char sh[] = SH_PATH; - struct filename *filename = *((struct filename **)filename_user); - if (IS_ERR(filename)) { - return 0; - } - if (likely(memcmp(filename->name, su, sizeof(su)))) - return 0; - pr_info("vfs_statx su->sh!\n"); - memcpy((void *)filename->name, sh, sizeof(sh)); + // it becomes a `struct filename *` after 5.18 + // https://elixir.bootlin.com/linux/v5.18/source/fs/stat.c#L216 + const char sh[] = SH_PATH; + struct filename *filename = *((struct filename **)filename_user); + if (IS_ERR(filename)) { + return 0; + } + if (likely(memcmp(filename->name, su, sizeof(su)))) + return 0; + pr_info("vfs_statx su->sh!\n"); + memcpy((void *)filename->name, sh, sizeof(sh)); #else - ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path)); + strncpy_from_user_nofault(path, *filename_user, sizeof(path)); - if (unlikely(!memcmp(path, su, sizeof(su)))) { - pr_info("newfstatat su->sh!\n"); - *filename_user = sh_user_path(); - } + if (unlikely(!memcmp(path, su, sizeof(su)))) { +#if __SULOG_GATE + ksu_sulog_report_syscall(current_uid().val, NULL, "newfstatat", path); +#endif + pr_info("newfstatat su->sh!\n"); + *filename_user = sh_user_path(); + } #endif - return 0; + return 0; } -int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv, - void *envp, int *flags) +int ksu_handle_execve_sucompat(const char __user **filename_user, + void *__never_use_argv, void *__never_use_envp, + int *__never_use_flags) { - return ksu_handle_execveat_sucompat(fd, filename_ptr, argv, envp, flags); -} + const char su[] = SU_PATH; + char path[sizeof(su) + 1]; -// the call from execve_handler_pre won't provided correct value for __never_use_argument, use them after fix execve_handler_pre, keeping them for consistence for manually patched code -int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, - void *__never_use_argv, void *__never_use_envp, - int *__never_use_flags) -{ - struct filename *filename; - const char sh[] = KSUD_PATH; - const char su[] = SU_PATH; + if (unlikely(!filename_user)) + return 0; -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_sucompat_hook_state) { - return 0; - } -#endif - if (unlikely(!filename_ptr)) - return 0; + memset(path, 0, sizeof(path)); + strncpy_from_user_nofault(path, *filename_user, sizeof(path)); - filename = *filename_ptr; - if (IS_ERR(filename)) { - return 0; - } + if (likely(memcmp(path, su, sizeof(su)))) + return 0; - if (likely(memcmp(filename->name, su, sizeof(su)))) - return 0; +#if __SULOG_GATE + bool is_allowed = ksu_is_allow_uid_for_current(current_uid().val); + ksu_sulog_report_syscall(current_uid().val, NULL, "execve", path); + + if (!is_allowed) + return 0; - if (!ksu_is_allow_uid(current_uid().val)) - return 0; - - pr_info("do_execveat_common su found\n"); - memcpy((void *)filename->name, sh, sizeof(sh)); - - escape_to_root(); - - return 0; -} - -int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user, - void *__never_use_argv, void *__never_use_envp, - int *__never_use_flags) -{ - const char su[] = SU_PATH; - char path[sizeof(su) + 1]; - -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_sucompat_hook_state){ - return 0; - } -#endif - if (unlikely(!filename_user)) - return 0; - - memset(path, 0, sizeof(path)); - ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path)); - - if (likely(memcmp(path, su, sizeof(su)))) - return 0; - - if (!ksu_is_allow_uid(current_uid().val)) - return 0; - - pr_info("sys_execve su found\n"); - *filename_user = ksud_user_path(); - - escape_to_root(); - - return 0; -} - -// dummified -int ksu_handle_devpts(struct inode *inode) -{ - return 0; -} - -int __ksu_handle_devpts(struct inode *inode) -{ - -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_sucompat_hook_state) - return 0; -#endif - - if (!current->mm) { - return 0; - } - - uid_t uid = current_uid().val; - if (uid % 100000 < 10000) { - // not untrusted_app, ignore it - return 0; - } - - if (likely(!ksu_is_allow_uid(uid))) - return 0; - - struct inode_security_struct *sec = selinux_inode(inode); - - if (ksu_devpts_sid && sec) - sec->sid = ksu_devpts_sid; - - return 0; -} - -#ifdef CONFIG_KSU_KPROBES_HOOK -static int faccessat_handler_pre(struct kprobe *p, struct pt_regs *regs) -{ - struct pt_regs *real_regs = PT_REAL_REGS(regs); - int *dfd = (int *)&PT_REGS_PARM1(real_regs); - const char __user **filename_user = - (const char **)&PT_REGS_PARM2(real_regs); - int *mode = (int *)&PT_REGS_PARM3(real_regs); - - return ksu_handle_faccessat(dfd, filename_user, mode, NULL); -} - -static int newfstatat_handler_pre(struct kprobe *p, struct pt_regs *regs) -{ - struct pt_regs *real_regs = PT_REAL_REGS(regs); - int *dfd = (int *)&PT_REGS_PARM1(real_regs); - const char __user **filename_user = - (const char **)&PT_REGS_PARM2(real_regs); - int *flags = (int *)&PT_REGS_SYSCALL_PARM4(real_regs); - - return ksu_handle_stat(dfd, filename_user, flags); -} - -static int execve_handler_pre(struct kprobe *p, struct pt_regs *regs) -{ - struct pt_regs *real_regs = PT_REAL_REGS(regs); - const char __user **filename_user = - (const char **)&PT_REGS_PARM1(real_regs); - - return ksu_handle_execve_sucompat(AT_FDCWD, filename_user, NULL, NULL, - NULL); -} - -static struct kprobe *su_kps[4]; -static int pts_unix98_lookup_pre(struct kprobe *p, struct pt_regs *regs) -{ - struct inode *inode; -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 6, 0) - struct file *file = (struct file *)PT_REGS_PARM2(regs); - inode = file->f_path.dentry->d_inode; + ksu_sulog_report_su_attempt(current_uid().val, NULL, path, is_allowed); #else - inode = (struct inode *)PT_REGS_PARM2(regs); + if (!ksu_is_allow_uid_for_current(current_uid().val)) { + return 0; + } #endif - return ksu_handle_devpts(inode); + pr_info("sys_execve su found\n"); + *filename_user = ksud_user_path(); + + escape_with_root_profile(); + + return 0; } -static struct kprobe *init_kprobe(const char *name, - kprobe_pre_handler_t handler) -{ - struct kprobe *kp = kzalloc(sizeof(struct kprobe), GFP_KERNEL); - if (!kp) - return NULL; - kp->symbol_name = name; - kp->pre_handler = handler; - - int ret = register_kprobe(kp); - pr_info("sucompat: register_%s kprobe: %d\n", name, ret); - if (ret) { - kfree(kp); - return NULL; - } - - return kp; -} - -static void destroy_kprobe(struct kprobe **kp_ptr) -{ - struct kprobe *kp = *kp_ptr; - if (!kp) - return; - unregister_kprobe(kp); - synchronize_rcu(); - kfree(kp); - *kp_ptr = NULL; -} - -#endif - -// sucompat: permited process can execute 'su' to gain root access. +// sucompat: permitted process can execute 'su' to gain root access. void ksu_sucompat_init() { -#ifdef CONFIG_KSU_KPROBES_HOOK - su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre); - su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre); - su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre); - su_kps[3] = init_kprobe("pts_unix98_lookup", pts_unix98_lookup_pre); -#else - ksu_sucompat_hook_state = true; - pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n"); -#endif + if (ksu_register_feature_handler(&su_compat_handler)) { + pr_err("Failed to register su_compat feature handler\n"); + } } void ksu_sucompat_exit() { -#ifdef CONFIG_KSU_KPROBES_HOOK - int i; - for (i = 0; i < ARRAY_SIZE(su_kps); i++) { - destroy_kprobe(&su_kps[i]); - } -#else - ksu_sucompat_hook_state = false; - pr_info("ksu_sucompat_exit: hooks disabled: execve/execveat_su, faccessat, stat\n"); -#endif -} + ksu_unregister_feature_handler(KSU_FEATURE_SU_COMPAT); +} \ No newline at end of file diff --git a/kernel/sucompat.h b/kernel/sucompat.h new file mode 100644 index 0000000..82161f7 --- /dev/null +++ b/kernel/sucompat.h @@ -0,0 +1,18 @@ +#ifndef __KSU_H_SUCOMPAT +#define __KSU_H_SUCOMPAT +#include + +extern bool ksu_su_compat_enabled; + +void ksu_sucompat_init(void); +void ksu_sucompat_exit(void); + +// Handler functions exported for hook_manager +int ksu_handle_faccessat(int *dfd, const char __user **filename_user, + int *mode, int *__unused_flags); +int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags); +int ksu_handle_execve_sucompat(const char __user **filename_user, + void *__never_use_argv, void *__never_use_envp, + int *__never_use_flags); + +#endif \ No newline at end of file diff --git a/kernel/sulog.c b/kernel/sulog.c new file mode 100644 index 0000000..84993d2 --- /dev/null +++ b/kernel/sulog.c @@ -0,0 +1,369 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "klog.h" + +#include "sulog.h" +#include "ksu.h" +#include "feature.h" + +#if __SULOG_GATE + +struct dedup_entry dedup_tbl[SULOG_COMM_LEN]; +static DEFINE_SPINLOCK(dedup_lock); +static LIST_HEAD(sulog_queue); +static struct workqueue_struct *sulog_workqueue; +static struct work_struct sulog_work; +static bool sulog_enabled __read_mostly = true; + +static int sulog_feature_get(u64 *value) +{ + *value = sulog_enabled ? 1 : 0; + return 0; +} + +static int sulog_feature_set(u64 value) +{ + bool enable = value != 0; + sulog_enabled = enable; + pr_info("sulog: set to %d\n", enable); + return 0; +} + +static const struct ksu_feature_handler sulog_handler = { + .feature_id = KSU_FEATURE_SULOG, + .name = "sulog", + .get_handler = sulog_feature_get, + .set_handler = sulog_feature_set, +}; + +static void get_timestamp(char *buf, size_t len) +{ + struct timespec64 ts; + struct tm tm; + + ktime_get_real_ts64(&ts); + time64_to_tm(ts.tv_sec - sys_tz.tz_minuteswest * 60, 0, &tm); + + snprintf(buf, len, "%04ld-%02d-%02d %02d:%02d:%02d", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +static void ksu_get_cmdline(char *full_comm, const char *comm, size_t buf_len) +{ + if (!full_comm || buf_len <= 0) + return; + + if (comm && strlen(comm) > 0) { + KSU_STRSCPY(full_comm, comm, buf_len); + return; + } + + if (in_atomic() || in_interrupt() || irqs_disabled()) { + KSU_STRSCPY(full_comm, current->comm, buf_len); + return; + } + + if (!current->mm) { + KSU_STRSCPY(full_comm, current->comm, buf_len); + return; + } + + int n = get_cmdline(current, full_comm, buf_len); + if (n <= 0) { + KSU_STRSCPY(full_comm, current->comm, buf_len); + return; + } + + for (int i = 0; i < n && i < buf_len - 1; i++) { + if (full_comm[i] == '\0') + full_comm[i] = ' '; + } + full_comm[n < buf_len ? n : buf_len - 1] = '\0'; +} + +static void sanitize_string(char *str, size_t len) +{ + if (!str || len == 0) + return; + + size_t read_pos = 0, write_pos = 0; + + while (read_pos < len && str[read_pos] != '\0') { + char c = str[read_pos]; + + if (c == '\n' || c == '\r') { + read_pos++; + continue; + } + + if (c == ' ' && write_pos > 0 && str[write_pos - 1] == ' ') { + read_pos++; + continue; + } + + str[write_pos++] = c; + read_pos++; + } + + str[write_pos] = '\0'; +} + +static bool dedup_should_print(uid_t uid, u8 type, const char *content, size_t len) +{ + struct dedup_key key = { + .crc = dedup_calc_hash(content, len), + .uid = uid, + .type = type, + }; + u64 now = ktime_get_ns(); + u64 delta_ns = DEDUP_SECS * NSEC_PER_SEC; + + u32 idx = key.crc & (SULOG_COMM_LEN - 1); + spin_lock(&dedup_lock); + + struct dedup_entry *e = &dedup_tbl[idx]; + if (e->key.crc == key.crc && + e->key.uid == key.uid && + e->key.type == key.type && + (now - e->ts_ns) < delta_ns) { + spin_unlock(&dedup_lock); + return false; + } + + e->key = key; + e->ts_ns = now; + spin_unlock(&dedup_lock); + return true; +} + +static void sulog_work_handler(struct work_struct *work) +{ + struct file *fp; + struct sulog_entry *entry, *tmp; + LIST_HEAD(local_queue); + loff_t pos = 0; + unsigned long flags; + + spin_lock_irqsave(&dedup_lock, flags); + list_splice_init(&sulog_queue, &local_queue); + spin_unlock_irqrestore(&dedup_lock, flags); + + if (list_empty(&local_queue)) + return; + + fp = filp_open(SULOG_PATH, O_WRONLY | O_CREAT | O_APPEND, 0640); + if (IS_ERR(fp)) { + pr_err("sulog: failed to open log file: %ld\n", PTR_ERR(fp)); + goto cleanup; + } + + if (fp->f_inode->i_size > SULOG_MAX_SIZE) { + if (vfs_truncate(&fp->f_path, 0)) + pr_err("sulog: failed to truncate log file\n"); + pos = 0; + } else { + pos = fp->f_inode->i_size; + } + + list_for_each_entry(entry, &local_queue, list) + kernel_write(fp, entry->content, strlen(entry->content), &pos); + + vfs_fsync(fp, 0); + filp_close(fp, 0); + +cleanup: + list_for_each_entry_safe(entry, tmp, &local_queue, list) { + list_del(&entry->list); + kfree(entry); + } +} + +static void sulog_add_entry(char *log_buf, size_t len, uid_t uid, u8 dedup_type) +{ + struct sulog_entry *entry; + unsigned long flags; + + if (!sulog_enabled || !log_buf || len == 0) + return; + + if (!dedup_should_print(uid, dedup_type, log_buf, len)) + return; + + entry = kzalloc(sizeof(*entry), GFP_ATOMIC); + if (!entry) + return; + + KSU_STRSCPY(entry->content, log_buf, SULOG_ENTRY_MAX_LEN); + + spin_lock_irqsave(&dedup_lock, flags); + list_add_tail(&entry->list, &sulog_queue); + spin_unlock_irqrestore(&dedup_lock, flags); + + if (sulog_workqueue) + queue_work(sulog_workqueue, &sulog_work); +} + +void ksu_sulog_report_su_grant(uid_t uid, const char *comm, const char *method) +{ + char log_buf[SULOG_ENTRY_MAX_LEN]; + char timestamp[32]; + char full_comm[SULOG_COMM_LEN]; + + if (!sulog_enabled) + return; + + get_timestamp(timestamp, sizeof(timestamp)); + ksu_get_cmdline(full_comm, comm, sizeof(full_comm)); + + sanitize_string(full_comm, sizeof(full_comm)); + + snprintf(log_buf, sizeof(log_buf), + "[%s] SU_GRANT: UID=%d COMM=%s METHOD=%s PID=%d\n", + timestamp, uid, full_comm, method ? method : "unknown", current->pid); + + sulog_add_entry(log_buf, strlen(log_buf), uid, DEDUP_SU_GRANT); +} + +void ksu_sulog_report_su_attempt(uid_t uid, const char *comm, const char *target_path, bool success) +{ + char log_buf[SULOG_ENTRY_MAX_LEN]; + char timestamp[32]; + char full_comm[SULOG_COMM_LEN]; + + if (!sulog_enabled) + return; + + get_timestamp(timestamp, sizeof(timestamp)); + ksu_get_cmdline(full_comm, comm, sizeof(full_comm)); + + sanitize_string(full_comm, sizeof(full_comm)); + + snprintf(log_buf, sizeof(log_buf), + "[%s] SU_EXEC: UID=%d COMM=%s TARGET=%s RESULT=%s PID=%d\n", + timestamp, uid, full_comm, target_path ? target_path : "unknown", + success ? "SUCCESS" : "DENIED", current->pid); + + sulog_add_entry(log_buf, strlen(log_buf), uid, DEDUP_SU_ATTEMPT); +} + +void ksu_sulog_report_permission_check(uid_t uid, const char *comm, bool allowed) +{ + char log_buf[SULOG_ENTRY_MAX_LEN]; + char timestamp[32]; + char full_comm[SULOG_COMM_LEN]; + + if (!sulog_enabled) + return; + + get_timestamp(timestamp, sizeof(timestamp)); + ksu_get_cmdline(full_comm, comm, sizeof(full_comm)); + + sanitize_string(full_comm, sizeof(full_comm)); + + snprintf(log_buf, sizeof(log_buf), + "[%s] PERM_CHECK: UID=%d COMM=%s RESULT=%s PID=%d\n", + timestamp, uid, full_comm, allowed ? "ALLOWED" : "DENIED", current->pid); + + sulog_add_entry(log_buf, strlen(log_buf), uid, DEDUP_PERM_CHECK); +} + +void ksu_sulog_report_manager_operation(const char *operation, uid_t manager_uid, uid_t target_uid) +{ + char log_buf[SULOG_ENTRY_MAX_LEN]; + char timestamp[32]; + char full_comm[SULOG_COMM_LEN]; + + if (!sulog_enabled) + return; + + get_timestamp(timestamp, sizeof(timestamp)); + ksu_get_cmdline(full_comm, NULL, sizeof(full_comm)); + + sanitize_string(full_comm, sizeof(full_comm)); + + snprintf(log_buf, sizeof(log_buf), + "[%s] MANAGER_OP: OP=%s MANAGER_UID=%d TARGET_UID=%d COMM=%s PID=%d\n", + timestamp, operation ? operation : "unknown", manager_uid, target_uid, full_comm, current->pid); + + sulog_add_entry(log_buf, strlen(log_buf), manager_uid, DEDUP_MANAGER_OP); +} + +void ksu_sulog_report_syscall(uid_t uid, const char *comm, const char *syscall, const char *args) +{ + char log_buf[SULOG_ENTRY_MAX_LEN]; + char timestamp[32]; + char full_comm[SULOG_COMM_LEN]; + + if (!sulog_enabled) + return; + + get_timestamp(timestamp, sizeof(timestamp)); + ksu_get_cmdline(full_comm, comm, sizeof(full_comm)); + + sanitize_string(full_comm, sizeof(full_comm)); + + snprintf(log_buf, sizeof(log_buf), + "[%s] SYSCALL: UID=%d COMM=%s SYSCALL=%s ARGS=%s PID=%d\n", + timestamp, uid, full_comm, syscall ? syscall : "unknown", + args ? args : "none", current->pid); + + sulog_add_entry(log_buf, strlen(log_buf), uid, DEDUP_SYSCALL); +} + +int ksu_sulog_init(void) +{ + if (ksu_register_feature_handler(&sulog_handler)) { + pr_err("Failed to register sulog feature handler\n"); + } + + sulog_workqueue = alloc_workqueue("ksu_sulog", WQ_UNBOUND | WQ_HIGHPRI, 1); + if (!sulog_workqueue) { + pr_err("sulog: failed to create workqueue\n"); + return -ENOMEM; + } + + INIT_WORK(&sulog_work, sulog_work_handler); + pr_info("sulog: initialized successfully\n"); + return 0; +} + +void ksu_sulog_exit(void) +{ + struct sulog_entry *entry, *tmp; + unsigned long flags; + + ksu_unregister_feature_handler(KSU_FEATURE_SULOG); + + sulog_enabled = false; + + if (sulog_workqueue) { + flush_workqueue(sulog_workqueue); + destroy_workqueue(sulog_workqueue); + sulog_workqueue = NULL; + } + + spin_lock_irqsave(&dedup_lock, flags); + list_for_each_entry_safe(entry, tmp, &sulog_queue, list) { + list_del(&entry->list); + kfree(entry); + } + spin_unlock_irqrestore(&dedup_lock, flags); + + pr_info("sulog: cleaned up successfully\n"); +} + +#endif // __SULOG_GATE diff --git a/kernel/sulog.h b/kernel/sulog.h new file mode 100644 index 0000000..13144fb --- /dev/null +++ b/kernel/sulog.h @@ -0,0 +1,93 @@ +#ifndef __KSU_SULOG_H +#define __KSU_SULOG_H + +#include +#include +#include // needed for function dedup_calc_hash + +#define __SULOG_GATE 1 + +#if __SULOG_GATE + +extern struct timezone sys_tz; + +#define SULOG_PATH "/data/adb/ksu/log/sulog.log" +#define SULOG_MAX_SIZE (32 * 1024 * 1024) // 128MB +#define SULOG_ENTRY_MAX_LEN 512 +#define SULOG_COMM_LEN 256 +#define DEDUP_SECS 10 + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 10, 0) +static inline size_t strlcpy(char *dest, const char *src, size_t size) +{ + return strscpy(dest, src, size); +} +#endif + +#define KSU_STRSCPY(dst, src, size) \ + do { \ + if (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0)) { \ + strscpy(dst, src, size); \ + } else { \ + strlcpy(dst, src, size); \ + } \ + } while (0) + +#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 8, 0) +#include + +static inline void time64_to_tm(time64_t totalsecs, int offset, struct tm *result) +{ + struct rtc_time rtc_tm; + rtc_time64_to_tm(totalsecs, &rtc_tm); + + result->tm_sec = rtc_tm.tm_sec; + result->tm_min = rtc_tm.tm_min; + result->tm_hour = rtc_tm.tm_hour; + result->tm_mday = rtc_tm.tm_mday; + result->tm_mon = rtc_tm.tm_mon; + result->tm_year = rtc_tm.tm_year; +} +#endif + +struct dedup_key { + u32 crc; + uid_t uid; + u8 type; + u8 _pad[1]; +}; + +struct dedup_entry { + struct dedup_key key; + u64 ts_ns; +}; + +enum { + DEDUP_SU_GRANT = 0, + DEDUP_SU_ATTEMPT, + DEDUP_PERM_CHECK, + DEDUP_MANAGER_OP, + DEDUP_SYSCALL, +}; + +static inline u32 dedup_calc_hash(const char *content, size_t len) +{ + return crc32(0, content, len); +} + +struct sulog_entry { + struct list_head list; + char content[SULOG_ENTRY_MAX_LEN]; +}; + +void ksu_sulog_report_su_grant(uid_t uid, const char *comm, const char *method); +void ksu_sulog_report_su_attempt(uid_t uid, const char *comm, const char *target_path, bool success); +void ksu_sulog_report_permission_check(uid_t uid, const char *comm, bool allowed); +void ksu_sulog_report_manager_operation(const char *operation, uid_t manager_uid, uid_t target_uid); +void ksu_sulog_report_syscall(uid_t uid, const char *comm, const char *syscall, const char *args); + +int ksu_sulog_init(void); +void ksu_sulog_exit(void); +#endif // __SULOG_GATE + +#endif /* __KSU_SULOG_H */ diff --git a/kernel/supercalls.c b/kernel/supercalls.c new file mode 100644 index 0000000..e42007f --- /dev/null +++ b/kernel/supercalls.c @@ -0,0 +1,1072 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "supercalls.h" +#include "arch.h" +#include "allowlist.h" +#include "feature.h" +#include "klog.h" // IWYU pragma: keep +#include "ksud.h" +#include "kernel_umount.h" +#include "manager.h" +#include "selinux/selinux.h" +#include "objsec.h" +#include "file_wrapper.h" +#include "syscall_hook_manager.h" +#include "throne_comm.h" +#include "dynamic_manager.h" +#include "umount_manager.h" + +#include "sulog.h" +#ifdef CONFIG_KSU_MANUAL_SU +#include "manual_su.h" +#endif + +bool ksu_uid_scanner_enabled = false; + +// Permission check functions +bool only_manager(void) +{ + return is_manager(); +} + +bool only_root(void) +{ + return current_uid().val == 0; +} + +bool manager_or_root(void) +{ + return current_uid().val == 0 || is_manager(); +} + +bool always_allow(void) +{ + return true; // No permission check +} + +bool allowed_for_su(void) +{ + bool is_allowed = is_manager() || ksu_is_allow_uid_for_current(current_uid().val); +#if __SULOG_GATE + ksu_sulog_report_permission_check(current_uid().val, current->comm, is_allowed); +#endif + return is_allowed; +} + +static void init_uid_scanner(void) +{ + ksu_uid_init(); + do_load_throne_state(NULL); + + if (ksu_uid_scanner_enabled) { + int ret = ksu_throne_comm_init(); + if (ret != 0) { + pr_err("Failed to initialize throne communication: %d\n", ret); + } + } +} + +static int do_grant_root(void __user *arg) +{ + // we already check uid above on allowed_for_su() + + pr_info("allow root for: %d\n", current_uid().val); + escape_with_root_profile(); + + return 0; +} + +static int do_get_info(void __user *arg) +{ + struct ksu_get_info_cmd cmd = {.version = KERNEL_SU_VERSION, .flags = 0}; + +#ifdef MODULE + cmd.flags |= 0x1; +#endif + if (is_manager()) { + cmd.flags |= 0x2; + } + cmd.features = KSU_FEATURE_MAX; + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_version: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_report_event(void __user *arg) +{ + struct ksu_report_event_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + return -EFAULT; + } + + switch (cmd.event) { + case EVENT_POST_FS_DATA: { + static bool post_fs_data_lock = false; + if (!post_fs_data_lock) { + post_fs_data_lock = true; + pr_info("post-fs-data triggered\n"); + on_post_fs_data(); + init_uid_scanner(); +#if __SULOG_GATE + ksu_sulog_init(); +#endif + ksu_dynamic_manager_init(); + } + break; + } + case EVENT_BOOT_COMPLETED: { + static bool boot_complete_lock = false; + if (!boot_complete_lock) { + boot_complete_lock = true; + pr_info("boot_complete triggered\n"); + on_boot_completed(); + } + break; + } + case EVENT_MODULE_MOUNTED: { + pr_info("module mounted!\n"); + on_module_mounted(); + break; + } + default: + break; + } + + return 0; +} + +static int do_set_sepolicy(void __user *arg) +{ + struct ksu_set_sepolicy_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + return -EFAULT; + } + + return handle_sepolicy(cmd.cmd, (void __user *)cmd.arg); +} + +static int do_check_safemode(void __user *arg) +{ + struct ksu_check_safemode_cmd cmd; + + cmd.in_safe_mode = ksu_is_safe_mode(); + + if (cmd.in_safe_mode) { + pr_warn("safemode enabled!\n"); + } + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("check_safemode: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_get_allow_list(void __user *arg) +{ + struct ksu_get_allow_list_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + return -EFAULT; + } + + bool success = ksu_get_allow_list((int *)cmd.uids, (int *)&cmd.count, true); + + if (!success) { + return -EFAULT; + } + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_allow_list: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_get_deny_list(void __user *arg) +{ + struct ksu_get_allow_list_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + return -EFAULT; + } + + bool success = ksu_get_allow_list((int *)cmd.uids, (int *)&cmd.count, false); + + if (!success) { + return -EFAULT; + } + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_deny_list: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_uid_granted_root(void __user *arg) +{ + struct ksu_uid_granted_root_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + return -EFAULT; + } + + cmd.granted = ksu_is_allow_uid_for_current(cmd.uid); + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("uid_granted_root: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_uid_should_umount(void __user *arg) +{ + struct ksu_uid_should_umount_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + return -EFAULT; + } + + cmd.should_umount = ksu_uid_should_umount(cmd.uid); + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("uid_should_umount: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_get_manager_uid(void __user *arg) +{ + struct ksu_get_manager_uid_cmd cmd; + + cmd.uid = ksu_get_manager_uid(); + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_manager_uid: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_get_app_profile(void __user *arg) +{ + struct ksu_get_app_profile_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("get_app_profile: copy_from_user failed\n"); + return -EFAULT; + } + + if (!ksu_get_app_profile(&cmd.profile)) { + return -ENOENT; + } + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_app_profile: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_set_app_profile(void __user *arg) +{ + struct ksu_set_app_profile_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("set_app_profile: copy_from_user failed\n"); + return -EFAULT; + } + + if (!ksu_set_app_profile(&cmd.profile, true)) { +#if __SULOG_GATE + ksu_sulog_report_manager_operation("SET_APP_PROFILE", + current_uid().val, cmd.profile.current_uid); +#endif + return -EFAULT; + } + + return 0; +} + +static int do_get_feature(void __user *arg) +{ + struct ksu_get_feature_cmd cmd; + bool supported; + int ret; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("get_feature: copy_from_user failed\n"); + return -EFAULT; + } + + ret = ksu_get_feature(cmd.feature_id, &cmd.value, &supported); + cmd.supported = supported ? 1 : 0; + + if (ret && supported) { + pr_err("get_feature: failed for feature %u: %d\n", cmd.feature_id, ret); + return ret; + } + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_feature: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_set_feature(void __user *arg) +{ + struct ksu_set_feature_cmd cmd; + int ret; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("set_feature: copy_from_user failed\n"); + return -EFAULT; + } + + ret = ksu_set_feature(cmd.feature_id, cmd.value); + if (ret) { + pr_err("set_feature: failed for feature %u: %d\n", cmd.feature_id, ret); + return ret; + } + + return 0; +} + +static int do_get_wrapper_fd(void __user *arg) { + if (!ksu_file_sid) { + return -EINVAL; + } + + struct ksu_get_wrapper_fd_cmd cmd; + int ret; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("get_wrapper_fd: copy_from_user failed\n"); + return -EFAULT; + } + + struct file* f = fget(cmd.fd); + if (!f) { + return -EBADF; + } + + struct ksu_file_wrapper *data = ksu_create_file_wrapper(f); + if (data == NULL) { + ret = -ENOMEM; + goto put_orig_file; + } + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0) +#define getfd_secure anon_inode_create_getfd +#else +#define getfd_secure anon_inode_getfd_secure +#endif + ret = getfd_secure("[ksu_fdwrapper]", &data->ops, data, f->f_flags, NULL); + if (ret < 0) { + pr_err("ksu_fdwrapper: getfd failed: %d\n", ret); + goto put_wrapper_data; + } + struct file* pf = fget(ret); + + struct inode* wrapper_inode = file_inode(pf); + // copy original inode mode + wrapper_inode->i_mode = file_inode(f)->i_mode; + struct inode_security_struct *sec = selinux_inode(wrapper_inode); + if (sec) { + sec->sid = ksu_file_sid; + } + + fput(pf); + goto put_orig_file; +put_wrapper_data: + ksu_delete_file_wrapper(data); +put_orig_file: + fput(f); + + return ret; +} + +static int do_manage_mark(void __user *arg) +{ + struct ksu_manage_mark_cmd cmd; + int ret = 0; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("manage_mark: copy_from_user failed\n"); + return -EFAULT; + } + + switch (cmd.operation) { + case KSU_MARK_GET: { + // Get task mark status + ret = ksu_get_task_mark(cmd.pid); + if (ret < 0) { + pr_err("manage_mark: get failed for pid %d: %d\n", cmd.pid, ret); + return ret; + } + cmd.result = (u32)ret; + break; + } + case KSU_MARK_MARK: { + if (cmd.pid == 0) { + ksu_mark_all_process(); + } else { + ret = ksu_set_task_mark(cmd.pid, true); + if (ret < 0) { + pr_err("manage_mark: set_mark failed for pid %d: %d\n", cmd.pid, + ret); + return ret; + } + } + break; + } + case KSU_MARK_UNMARK: { + if (cmd.pid == 0) { + ksu_unmark_all_process(); + } else { + ret = ksu_set_task_mark(cmd.pid, false); + if (ret < 0) { + pr_err("manage_mark: set_unmark failed for pid %d: %d\n", + cmd.pid, ret); + return ret; + } + } + break; + } + case KSU_MARK_REFRESH: { + ksu_mark_running_process(); + pr_info("manage_mark: refreshed running processes\n"); + break; + } + default: { + pr_err("manage_mark: invalid operation %u\n", cmd.operation); + return -EINVAL; + } + } + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("manage_mark: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_nuke_ext4_sysfs(void __user *arg) +{ + struct ksu_nuke_ext4_sysfs_cmd cmd; + char mnt[256]; + long ret; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) + return -EFAULT; + + if (!cmd.arg) + return -EINVAL; + + memset(mnt, 0, sizeof(mnt)); + + ret = strncpy_from_user(mnt, cmd.arg, sizeof(mnt)); + if (ret < 0) { + pr_err("nuke ext4 copy mnt failed: %ld\\n", ret); + return -EFAULT; // 或者 return ret; + } + + if (ret == sizeof(mnt)) { + pr_err("nuke ext4 mnt path too long\\n"); + return -ENAMETOOLONG; + } + + pr_info("do_nuke_ext4_sysfs: %s\n", mnt); + + return nuke_ext4_sysfs(mnt); +} + +struct list_head mount_list = LIST_HEAD_INIT(mount_list); +DECLARE_RWSEM(mount_list_lock); + +static int add_try_umount(void __user *arg) +{ + struct mount_entry *new_entry, *entry, *tmp; + struct ksu_add_try_umount_cmd cmd; + char buf[256] = {0}; + + if (copy_from_user(&cmd, arg, sizeof cmd)) + return -EFAULT; + + switch (cmd.mode) { + case KSU_UMOUNT_WIPE: { + struct mount_entry *entry, *tmp; + down_write(&mount_list_lock); + list_for_each_entry_safe(entry, tmp, &mount_list, list) { + pr_info("wipe_umount_list: removing entry: %s\n", entry->umountable); + list_del(&entry->list); + kfree(entry->umountable); + kfree(entry); + } + up_write(&mount_list_lock); + + return 0; + } + + case KSU_UMOUNT_ADD: { + long len = strncpy_from_user(buf, (const char __user *)cmd.arg, 256); + if (len <= 0) + return -EFAULT; + + buf[sizeof(buf) - 1] = '\0'; + + new_entry = kzalloc(sizeof(*new_entry), GFP_KERNEL); + if (!new_entry) + return -ENOMEM; + + new_entry->umountable = kstrdup(buf, GFP_KERNEL); + if (!new_entry->umountable) { + kfree(new_entry); + return -1; + } + + down_write(&mount_list_lock); + + // disallow dupes + // if this gets too many, we can consider moving this whole task to a kthread + list_for_each_entry(entry, &mount_list, list) { + if (!strcmp(entry->umountable, buf)) { + pr_info("cmd_add_try_umount: %s is already here!\n", buf); + up_write(&mount_list_lock); + kfree(new_entry->umountable); + kfree(new_entry); + return -1; + } + } + + // now check flags and add + // this also serves as a null check + if (cmd.flags) + new_entry->flags = cmd.flags; + else + new_entry->flags = 0; + + // debug + list_add(&new_entry->list, &mount_list); + up_write(&mount_list_lock); + pr_info("cmd_add_try_umount: %s added!\n", buf); + + return 0; + } + + // this is just strcmp'd wipe anyway + case KSU_UMOUNT_DEL: { + long len = strncpy_from_user(buf, (const char __user *)cmd.arg, sizeof(buf) - 1); + if (len <= 0) + return -EFAULT; + + buf[sizeof(buf) - 1] = '\0'; + + down_write(&mount_list_lock); + list_for_each_entry_safe(entry, tmp, &mount_list, list) { + if (!strcmp(entry->umountable, buf)) { + pr_info("cmd_add_try_umount: entry removed: %s\n", entry->umountable); + list_del(&entry->list); + kfree(entry->umountable); + kfree(entry); + } + } + up_write(&mount_list_lock); + + return 0; + } + + default: { + pr_err("cmd_add_try_umount: invalid operation %u\n", cmd.mode); + return -EINVAL; + } + + } // switch(cmd.mode) + + return 0; +} + +// 100. GET_FULL_VERSION - Get full version string +static int do_get_full_version(void __user *arg) +{ + struct ksu_get_full_version_cmd cmd = {0}; + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) + strscpy(cmd.version_full, KSU_VERSION_FULL, sizeof(cmd.version_full)); +#else + strlcpy(cmd.version_full, KSU_VERSION_FULL, sizeof(cmd.version_full)); +#endif + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_full_version: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +// 101. HOOK_TYPE - Get hook type +static int do_get_hook_type(void __user *arg) +{ + struct ksu_hook_type_cmd cmd = {0}; + const char *type = "Tracepoint"; + +#if defined(KSU_MANUAL_HOOK) + type = "Manual"; +#endif + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) + strscpy(cmd.hook_type, type, sizeof(cmd.hook_type)); +#else + strlcpy(cmd.hook_type, type, sizeof(cmd.hook_type)); +#endif + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_hook_type: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +// 102. ENABLE_KPM - Check if KPM is enabled +static int do_enable_kpm(void __user *arg) +{ + struct ksu_enable_kpm_cmd cmd; + + cmd.enabled = IS_ENABLED(CONFIG_KPM); + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("enable_kpm: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_dynamic_manager(void __user *arg) +{ + struct ksu_dynamic_manager_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("dynamic_manager: copy_from_user failed\n"); + return -EFAULT; + } + + int ret = ksu_handle_dynamic_manager(&cmd.config); + if (ret) + return ret; + + if (cmd.config.operation == DYNAMIC_MANAGER_OP_GET && + copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("dynamic_manager: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_get_managers(void __user *arg) +{ + struct ksu_get_managers_cmd cmd; + + int ret = ksu_get_active_managers(&cmd.manager_info); + if (ret) + return ret; + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_managers: copy_from_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_enable_uid_scanner(void __user *arg) +{ + struct ksu_enable_uid_scanner_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("enable_uid_scanner: copy_from_user failed\n"); + return -EFAULT; + } + + switch (cmd.operation) { + case UID_SCANNER_OP_GET_STATUS: { + bool status = ksu_uid_scanner_enabled; + if (copy_to_user((void __user *)cmd.status_ptr, &status, sizeof(status))) { + pr_err("enable_uid_scanner: copy status failed\n"); + return -EFAULT; + } + break; + } + case UID_SCANNER_OP_TOGGLE: { + bool enabled = cmd.enabled; + + if (enabled == ksu_uid_scanner_enabled) { + pr_info("enable_uid_scanner: no need to change, already %s\n", + enabled ? "enabled" : "disabled"); + break; + } + + if (enabled) { + // Enable UID scanner + int ret = ksu_throne_comm_init(); + if (ret != 0) { + pr_err("enable_uid_scanner: failed to initialize: %d\n", ret); + return -EFAULT; + } + pr_info("enable_uid_scanner: enabled\n"); + } else { + // Disable UID scanner + ksu_throne_comm_exit(); + pr_info("enable_uid_scanner: disabled\n"); + } + + ksu_uid_scanner_enabled = enabled; + ksu_throne_comm_save_state(); + break; + } + case UID_SCANNER_OP_CLEAR_ENV: { + // Clear environment (force exit) + ksu_throne_comm_exit(); + ksu_uid_scanner_enabled = false; + ksu_throne_comm_save_state(); + pr_info("enable_uid_scanner: environment cleared\n"); + break; + } + default: + pr_err("enable_uid_scanner: invalid operation\n"); + return -EINVAL; + } + + return 0; +} + +#ifdef CONFIG_KSU_MANUAL_SU +static bool system_uid_check(void) +{ + return current_uid().val <= 2000; +} + +static int do_manual_su(void __user *arg) +{ + struct ksu_manual_su_cmd cmd; + struct manual_su_request request; + int res; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("manual_su: copy_from_user failed\n"); + return -EFAULT; + } + + pr_info("manual_su request, option=%d, uid=%d, pid=%d\n", + cmd.option, cmd.target_uid, cmd.target_pid); + + memset(&request, 0, sizeof(request)); + request.target_uid = cmd.target_uid; + request.target_pid = cmd.target_pid; + + if (cmd.option == MANUAL_SU_OP_GENERATE_TOKEN || + cmd.option == MANUAL_SU_OP_ESCALATE) { + memcpy(request.token_buffer, cmd.token_buffer, sizeof(request.token_buffer)); + } + + res = ksu_handle_manual_su_request(cmd.option, &request); + + if (cmd.option == MANUAL_SU_OP_GENERATE_TOKEN && res == 0) { + memcpy(cmd.token_buffer, request.token_buffer, sizeof(cmd.token_buffer)); + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("manual_su: copy_to_user failed\n"); + return -EFAULT; + } + } + + return res; +} +#endif + +static int do_umount_manager(void __user *arg) +{ + struct ksu_umount_manager_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("umount_manager: copy_from_user failed\n"); + return -EFAULT; + } + + switch (cmd.operation) { + case UMOUNT_OP_ADD: { + return ksu_umount_manager_add(cmd.path, cmd.flags, false); + } + case UMOUNT_OP_REMOVE: { + return ksu_umount_manager_remove(cmd.path); + } + case UMOUNT_OP_LIST: { + struct ksu_umount_entry_info __user *entries = + (struct ksu_umount_entry_info __user *)cmd.entries_ptr; + return ksu_umount_manager_get_entries(entries, &cmd.count); + } + case UMOUNT_OP_CLEAR_CUSTOM: { + return ksu_umount_manager_clear_custom(); + } + default: + return -EINVAL; + } +} + +// IOCTL handlers mapping table +static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { + { .cmd = KSU_IOCTL_GRANT_ROOT, .name = "GRANT_ROOT", .handler = do_grant_root, .perm_check = allowed_for_su }, + { .cmd = KSU_IOCTL_GET_INFO, .name = "GET_INFO", .handler = do_get_info, .perm_check = always_allow }, + { .cmd = KSU_IOCTL_REPORT_EVENT, .name = "REPORT_EVENT", .handler = do_report_event, .perm_check = only_root }, + { .cmd = KSU_IOCTL_SET_SEPOLICY, .name = "SET_SEPOLICY", .handler = do_set_sepolicy, .perm_check = only_root }, + { .cmd = KSU_IOCTL_CHECK_SAFEMODE, .name = "CHECK_SAFEMODE", .handler = do_check_safemode, .perm_check = always_allow }, + { .cmd = KSU_IOCTL_GET_ALLOW_LIST, .name = "GET_ALLOW_LIST", .handler = do_get_allow_list, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_GET_DENY_LIST, .name = "GET_DENY_LIST", .handler = do_get_deny_list, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_UID_GRANTED_ROOT, .name = "UID_GRANTED_ROOT", .handler = do_uid_granted_root, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_UID_SHOULD_UMOUNT, .name = "UID_SHOULD_UMOUNT", .handler = do_uid_should_umount, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_GET_MANAGER_UID, .name = "GET_MANAGER_UID", .handler = do_get_manager_uid, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_GET_APP_PROFILE, .name = "GET_APP_PROFILE", .handler = do_get_app_profile, .perm_check = only_manager }, + { .cmd = KSU_IOCTL_SET_APP_PROFILE, .name = "SET_APP_PROFILE", .handler = do_set_app_profile, .perm_check = only_manager }, + { .cmd = KSU_IOCTL_GET_FEATURE, .name = "GET_FEATURE", .handler = do_get_feature, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_SET_FEATURE, .name = "SET_FEATURE", .handler = do_set_feature, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_GET_WRAPPER_FD, .name = "GET_WRAPPER_FD", .handler = do_get_wrapper_fd, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_MANAGE_MARK, .name = "MANAGE_MARK", .handler = do_manage_mark, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_NUKE_EXT4_SYSFS, .name = "NUKE_EXT4_SYSFS", .handler = do_nuke_ext4_sysfs, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_ADD_TRY_UMOUNT, .name = "ADD_TRY_UMOUNT", .handler = add_try_umount, .perm_check = manager_or_root }, + { .cmd = KSU_IOCTL_GET_FULL_VERSION,.name = "GET_FULL_VERSION", .handler = do_get_full_version, .perm_check = always_allow}, + { .cmd = KSU_IOCTL_HOOK_TYPE,.name = "GET_HOOK_TYPE", .handler = do_get_hook_type, .perm_check = manager_or_root}, + { .cmd = KSU_IOCTL_ENABLE_KPM, .name = "GET_ENABLE_KPM", .handler = do_enable_kpm, .perm_check = manager_or_root}, + { .cmd = KSU_IOCTL_DYNAMIC_MANAGER, .name = "SET_DYNAMIC_MANAGER", .handler = do_dynamic_manager, .perm_check = manager_or_root}, + { .cmd = KSU_IOCTL_GET_MANAGERS, .name = "GET_MANAGERS", .handler = do_get_managers, .perm_check = manager_or_root}, + { .cmd = KSU_IOCTL_ENABLE_UID_SCANNER, .name = "SET_ENABLE_UID_SCANNER", .handler = do_enable_uid_scanner, .perm_check = manager_or_root}, +#ifdef CONFIG_KSU_MANUAL_SU + { .cmd = KSU_IOCTL_MANUAL_SU, .name = "MANUAL_SU", .handler = do_manual_su, .perm_check = system_uid_check}, +#endif +#ifdef CONFIG_KPM + { .cmd = KSU_IOCTL_KPM, .name = "KPM_OPERATION", .handler = do_kpm, .perm_check = manager_or_root}, +#endif + { .cmd = KSU_IOCTL_UMOUNT_MANAGER, .name = "UMOUNT_MANAGER", .handler = do_umount_manager, .perm_check = manager_or_root}, + { .cmd = 0, .name = NULL, .handler = NULL, .perm_check = NULL} // Sentine +}; + +struct ksu_install_fd_tw { + struct callback_head cb; + int __user *outp; +}; + +static void ksu_install_fd_tw_func(struct callback_head *cb) +{ + struct ksu_install_fd_tw *tw = container_of(cb, struct ksu_install_fd_tw, cb); + int fd = ksu_install_fd(); + pr_info("[%d] install ksu fd: %d\n", current->pid, fd); + + if (copy_to_user(tw->outp, &fd, sizeof(fd))) { + pr_err("install ksu fd reply err\n"); +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) + close_fd(fd); +#else + ksys_close(fd); +#endif + } + + kfree(tw); +} + +// downstream: make sure to pass arg as reference, this can allow us to extend things. +int ksu_handle_sys_reboot(int magic1, int magic2, unsigned int cmd, void __user **arg) +{ + struct ksu_install_fd_tw *tw; + + if (magic1 != KSU_INSTALL_MAGIC1) + return 0; + +#ifdef CONFIG_KSU_DEBUG + pr_info("sys_reboot: intercepted call! magic: 0x%x id: %d\n", magic1, magic2); +#endif + + // Check if this is a request to install KSU fd + if (magic2 == KSU_INSTALL_MAGIC2) { + tw = kzalloc(sizeof(*tw), GFP_ATOMIC); + if (!tw) + return 0; + + tw->outp = (int __user *)*arg; + tw->cb.func = ksu_install_fd_tw_func; + + if (task_work_add(current, &tw->cb, TWA_RESUME)) { + kfree(tw); + pr_warn("install fd add task_work failed\n"); + } + + return 0; + } + + // extensions + + return 0; +} + +#ifdef KSU_KPROBES_HOOK +// Reboot hook for installing fd +static int reboot_handler_pre(struct kprobe *p, struct pt_regs *regs) +{ + struct pt_regs *real_regs = PT_REAL_REGS(regs); + int magic1 = (int)PT_REGS_PARM1(real_regs); + int magic2 = (int)PT_REGS_PARM2(real_regs); + int cmd = (int)PT_REGS_PARM3(real_regs); + void __user **arg = (void __user **)&PT_REGS_SYSCALL_PARM4(real_regs); + + return ksu_handle_sys_reboot(magic1, magic2, cmd, arg); +} + +static struct kprobe reboot_kp = { + .symbol_name = REBOOT_SYMBOL, + .pre_handler = reboot_handler_pre, +}; +#endif + +void ksu_supercalls_init(void) +{ + int i; + + pr_info("KernelSU IOCTL Commands:\n"); + for (i = 0; ksu_ioctl_handlers[i].handler; i++) { + pr_info(" %-18s = 0x%08x\n", ksu_ioctl_handlers[i].name, ksu_ioctl_handlers[i].cmd); + } +#ifdef KSU_KPROBES_HOOK + int rc = register_kprobe(&reboot_kp); + if (rc) { + pr_err("reboot kprobe failed: %d\n", rc); + } else { + pr_info("reboot kprobe registered successfully\n"); + } +#endif +} + +void ksu_supercalls_exit(void) { +#ifdef KSU_KPROBES_HOOK + unregister_kprobe(&reboot_kp); +#endif +} + +static inline void ksu_ioctl_audit(unsigned int cmd, const char *cmd_name, uid_t uid, int ret) +{ +#if __SULOG_GATE + const char *result = (ret == 0) ? "SUCCESS" : + (ret == -EPERM) ? "DENIED" : "FAILED"; + ksu_sulog_report_syscall(uid, NULL, cmd_name, result); +#endif +} + +// IOCTL dispatcher +static long anon_ksu_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) +{ + void __user *argp = (void __user *)arg; + int i; + +#ifdef CONFIG_KSU_DEBUG + pr_info("ksu ioctl: cmd=0x%x from uid=%d\n", cmd, current_uid().val); +#endif + + for (i = 0; ksu_ioctl_handlers[i].handler; i++) { + if (cmd == ksu_ioctl_handlers[i].cmd) { + // Check permission first + if (ksu_ioctl_handlers[i].perm_check && + !ksu_ioctl_handlers[i].perm_check()) { + pr_warn("ksu ioctl: permission denied for cmd=0x%x uid=%d\n", + cmd, current_uid().val); + ksu_ioctl_audit(cmd, ksu_ioctl_handlers[i].name, + current_uid().val, -EPERM); + return -EPERM; + } + // Execute handler + int ret = ksu_ioctl_handlers[i].handler(argp); + ksu_ioctl_audit(cmd, ksu_ioctl_handlers[i].name, + current_uid().val, ret); + return ret; + } + } + + pr_warn("ksu ioctl: unsupported command 0x%x\n", cmd); + return -ENOTTY; +} + +// File release handler +static int anon_ksu_release(struct inode *inode, struct file *filp) +{ + pr_info("ksu fd released\n"); + return 0; +} + +// File operations structure +static const struct file_operations anon_ksu_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = anon_ksu_ioctl, + .compat_ioctl = anon_ksu_ioctl, + .release = anon_ksu_release, +}; + +// Install KSU fd to current process +int ksu_install_fd(void) +{ + struct file *filp; + int fd; + + // Get unused fd + fd = get_unused_fd_flags(O_CLOEXEC); + if (fd < 0) { + pr_err("ksu_install_fd: failed to get unused fd\n"); + return fd; + } + + // Create anonymous inode file + filp = anon_inode_getfile("[ksu_driver]", &anon_ksu_fops, NULL, O_RDWR | O_CLOEXEC); + if (IS_ERR(filp)) { + pr_err("ksu_install_fd: failed to create anon inode file\n"); + put_unused_fd(fd); + return PTR_ERR(filp); + } + + // Install fd + fd_install(fd, filp); + +#if __SULOG_GATE + ksu_sulog_report_permission_check(current_uid().val, current->comm, fd >= 0); +#endif + + pr_info("ksu fd installed: %d for pid %d\n", fd, current->pid); + + return fd; +} \ No newline at end of file diff --git a/kernel/supercalls.h b/kernel/supercalls.h new file mode 100644 index 0000000..6caca80 --- /dev/null +++ b/kernel/supercalls.h @@ -0,0 +1,197 @@ +#ifndef __KSU_H_SUPERCALLS +#define __KSU_H_SUPERCALLS + +#include +#include +#include "ksu.h" +#include "app_profile.h" + +#ifdef CONFIG_KPM +#include "kpm/kpm.h" +#endif + +// Magic numbers for reboot hook to install fd +#define KSU_INSTALL_MAGIC1 0xDEADBEEF +#define KSU_INSTALL_MAGIC2 0xCAFEBABE + +// Command structures for ioctl + +struct ksu_become_daemon_cmd { + __u8 token[65]; // Input: daemon token (null-terminated) +}; + +struct ksu_get_info_cmd { + __u32 version; // Output: KERNEL_SU_VERSION + __u32 flags; // Output: flags (bit 0: MODULE mode) + __u32 features; // Output: max feature ID supported +}; + +struct ksu_report_event_cmd { + __u32 event; // Input: EVENT_POST_FS_DATA, EVENT_BOOT_COMPLETED, etc. +}; + +struct ksu_set_sepolicy_cmd { + __u64 cmd; // Input: sepolicy command + __aligned_u64 arg; // Input: sepolicy argument pointer +}; + +struct ksu_check_safemode_cmd { + __u8 in_safe_mode; // Output: true if in safe mode, false otherwise +}; + +struct ksu_get_allow_list_cmd { + __u32 uids[128]; // Output: array of allowed/denied UIDs + __u32 count; // Output: number of UIDs in array + __u8 allow; // Input: true for allow list, false for deny list +}; + +struct ksu_uid_granted_root_cmd { + __u32 uid; // Input: target UID to check + __u8 granted; // Output: true if granted, false otherwise +}; + +struct ksu_uid_should_umount_cmd { + __u32 uid; // Input: target UID to check + __u8 should_umount; // Output: true if should umount, false otherwise +}; + +struct ksu_get_manager_uid_cmd { + __u32 uid; // Output: manager UID +}; + +struct ksu_get_app_profile_cmd { + struct app_profile profile; // Input/Output: app profile structure +}; + +struct ksu_set_app_profile_cmd { + struct app_profile profile; // Input: app profile structure +}; + +struct ksu_get_feature_cmd { + __u32 feature_id; // Input: feature ID (enum ksu_feature_id) + __u64 value; // Output: feature value/state + __u8 supported; // Output: true if feature is supported, false otherwise +}; + +struct ksu_set_feature_cmd { + __u32 feature_id; // Input: feature ID (enum ksu_feature_id) + __u64 value; // Input: feature value/state to set +}; + +struct ksu_get_wrapper_fd_cmd { + __u32 fd; // Input: userspace fd + __u32 flags; // Input: flags of userspace fd +}; + +struct ksu_manage_mark_cmd { + __u32 operation; // Input: KSU_MARK_* + __s32 pid; // Input: target pid (0 for all processes) + __u32 result; // Output: for get operation - mark status or reg_count +}; + +#define KSU_MARK_GET 1 +#define KSU_MARK_MARK 2 +#define KSU_MARK_UNMARK 3 +#define KSU_MARK_REFRESH 4 + +struct ksu_nuke_ext4_sysfs_cmd { + __aligned_u64 arg; // Input: mnt pointer +}; + +struct ksu_add_try_umount_cmd { + __aligned_u64 arg; // char ptr, this is the mountpoint + __u32 flags; // this is the flag we use for it + __u8 mode; // denotes what to do with it 0:wipe_list 1:add_to_list 2:delete_entry +}; + +#define KSU_UMOUNT_WIPE 0 // ignore everything and wipe list +#define KSU_UMOUNT_ADD 1 // add entry (path + flags) +#define KSU_UMOUNT_DEL 2 // delete entry, strcmp + + +// Other command structures +struct ksu_get_full_version_cmd { + char version_full[KSU_FULL_VERSION_STRING]; // Output: full version string +}; + +struct ksu_hook_type_cmd { + char hook_type[32]; // Output: hook type string +}; + +struct ksu_enable_kpm_cmd { + __u8 enabled; // Output: true if KPM is enabled +}; + +struct ksu_dynamic_manager_cmd { + struct dynamic_manager_user_config config; // Input/Output: dynamic manager config +}; + +struct ksu_get_managers_cmd { + struct manager_list_info manager_info; // Output: manager list information +}; + +struct ksu_enable_uid_scanner_cmd { + __u32 operation; // Input: operation type (UID_SCANNER_OP_GET_STATUS, UID_SCANNER_OP_TOGGLE, UID_SCANNER_OP_CLEAR_ENV) + __u32 enabled; // Input: enable or disable (for UID_SCANNER_OP_TOGGLE) + void __user *status_ptr; // Input: pointer to store status (for UID_SCANNER_OP_GET_STATUS) +}; + +#ifdef CONFIG_KSU_MANUAL_SU +struct ksu_manual_su_cmd { + __u32 option; // Input: operation type (MANUAL_SU_OP_GENERATE_TOKEN, MANUAL_SU_OP_ESCALATE, MANUAL_SU_OP_ADD_PENDING) + __u32 target_uid; // Input: target UID + __u32 target_pid; // Input: target PID + char token_buffer[33]; // Input/Output: token buffer +}; +#endif + +// IOCTL command definitions +#define KSU_IOCTL_GRANT_ROOT _IOC(_IOC_NONE, 'K', 1, 0) +#define KSU_IOCTL_GET_INFO _IOC(_IOC_READ, 'K', 2, 0) +#define KSU_IOCTL_REPORT_EVENT _IOC(_IOC_WRITE, 'K', 3, 0) +#define KSU_IOCTL_SET_SEPOLICY _IOC(_IOC_READ|_IOC_WRITE, 'K', 4, 0) +#define KSU_IOCTL_CHECK_SAFEMODE _IOC(_IOC_READ, 'K', 5, 0) +#define KSU_IOCTL_GET_ALLOW_LIST _IOC(_IOC_READ|_IOC_WRITE, 'K', 6, 0) +#define KSU_IOCTL_GET_DENY_LIST _IOC(_IOC_READ|_IOC_WRITE, 'K', 7, 0) +#define KSU_IOCTL_UID_GRANTED_ROOT _IOC(_IOC_READ|_IOC_WRITE, 'K', 8, 0) +#define KSU_IOCTL_UID_SHOULD_UMOUNT _IOC(_IOC_READ|_IOC_WRITE, 'K', 9, 0) +#define KSU_IOCTL_GET_MANAGER_UID _IOC(_IOC_READ, 'K', 10, 0) +#define KSU_IOCTL_GET_APP_PROFILE _IOC(_IOC_READ|_IOC_WRITE, 'K', 11, 0) +#define KSU_IOCTL_SET_APP_PROFILE _IOC(_IOC_WRITE, 'K', 12, 0) +#define KSU_IOCTL_GET_FEATURE _IOC(_IOC_READ|_IOC_WRITE, 'K', 13, 0) +#define KSU_IOCTL_SET_FEATURE _IOC(_IOC_WRITE, 'K', 14, 0) +#define KSU_IOCTL_GET_WRAPPER_FD _IOC(_IOC_WRITE, 'K', 15, 0) +#define KSU_IOCTL_MANAGE_MARK _IOC(_IOC_READ|_IOC_WRITE, 'K', 16, 0) +#define KSU_IOCTL_NUKE_EXT4_SYSFS _IOC(_IOC_WRITE, 'K', 17, 0) +#define KSU_IOCTL_ADD_TRY_UMOUNT _IOC(_IOC_WRITE, 'K', 18, 0) +// Other IOCTL command definitions +#define KSU_IOCTL_GET_FULL_VERSION _IOC(_IOC_READ, 'K', 100, 0) +#define KSU_IOCTL_HOOK_TYPE _IOC(_IOC_READ, 'K', 101, 0) +#define KSU_IOCTL_ENABLE_KPM _IOC(_IOC_READ, 'K', 102, 0) +#define KSU_IOCTL_DYNAMIC_MANAGER _IOC(_IOC_READ|_IOC_WRITE, 'K', 103, 0) +#define KSU_IOCTL_GET_MANAGERS _IOC(_IOC_READ|_IOC_WRITE, 'K', 104, 0) +#define KSU_IOCTL_ENABLE_UID_SCANNER _IOC(_IOC_READ|_IOC_WRITE, 'K', 105, 0) +#ifdef CONFIG_KSU_MANUAL_SU +#define KSU_IOCTL_MANUAL_SU _IOC(_IOC_READ|_IOC_WRITE, 'K', 106, 0) +#endif +#define KSU_IOCTL_UMOUNT_MANAGER _IOC(_IOC_READ|_IOC_WRITE, 'K', 107, 0) + +// IOCTL handler types +typedef int (*ksu_ioctl_handler_t)(void __user *arg); +typedef bool (*ksu_perm_check_t)(void); + +// IOCTL command mapping +struct ksu_ioctl_cmd_map { + unsigned int cmd; + const char *name; + ksu_ioctl_handler_t handler; + ksu_perm_check_t perm_check; // Permission check function +}; + +// Install KSU fd to current process +int ksu_install_fd(void); + +void ksu_supercalls_init(void); +void ksu_supercalls_exit(void); + +#endif // __KSU_H_SUPERCALLS \ No newline at end of file diff --git a/kernel/syscall_hook_manager.c b/kernel/syscall_hook_manager.c new file mode 100644 index 0000000..14d258a --- /dev/null +++ b/kernel/syscall_hook_manager.c @@ -0,0 +1,374 @@ +#include "linux/compiler.h" +#include "linux/cred.h" +#include "linux/printk.h" +#include "selinux/selinux.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include "allowlist.h" +#include "arch.h" +#include "klog.h" // IWYU pragma: keep +#include "syscall_hook_manager.h" +#include "sucompat.h" +#include "setuid_hook.h" +#include "selinux/selinux.h" + +// Tracepoint registration count management +// == 1: just us +// > 1: someone else is also using syscall tracepoint e.g. ftrace +static int tracepoint_reg_count = 0; +static DEFINE_SPINLOCK(tracepoint_reg_lock); + +void ksu_clear_task_tracepoint_flag_if_needed(struct task_struct *t) +{ + unsigned long flags; + spin_lock_irqsave(&tracepoint_reg_lock, flags); + if (tracepoint_reg_count <= 1) { + ksu_clear_task_tracepoint_flag(t); + } + spin_unlock_irqrestore(&tracepoint_reg_lock, flags); +} + +// Process marking management +static void handle_process_mark(bool mark) +{ + struct task_struct *p, *t; + read_lock(&tasklist_lock); + for_each_process_thread(p, t) { + if (mark) + ksu_set_task_tracepoint_flag(t); + else + ksu_clear_task_tracepoint_flag(t); + } + read_unlock(&tasklist_lock); +} + +void ksu_mark_all_process(void) +{ + handle_process_mark(true); + pr_info("hook_manager: mark all user process done!\n"); +} + +void ksu_unmark_all_process(void) +{ + handle_process_mark(false); + pr_info("hook_manager: unmark all user process done!\n"); +} + +static void ksu_mark_running_process_locked() +{ + struct task_struct *p, *t; + read_lock(&tasklist_lock); + for_each_process_thread (p, t) { + if (!t->mm) { // only user processes + continue; + } + int uid = task_uid(t).val; + const struct cred *cred = get_task_cred(t); + bool ksu_root_process = + uid == 0 && is_task_ksu_domain(cred); + bool is_zygote_process = is_zygote(cred); + bool is_shell = uid == 2000; + // before boot completed, we shall mark init for marking zygote + bool is_init = t->pid == 1; + if (ksu_root_process || is_zygote_process || is_shell || is_init + || ksu_is_allow_uid(uid)) { + ksu_set_task_tracepoint_flag(t); + pr_info("hook_manager: mark process: pid:%d, uid: %d, comm:%s\n", + t->pid, uid, t->comm); + } else { + ksu_clear_task_tracepoint_flag(t); + pr_info("hook_manager: unmark process: pid:%d, uid: %d, comm:%s\n", + t->pid, uid, t->comm); + } + put_cred(cred); + } + read_unlock(&tasklist_lock); +} + +void ksu_mark_running_process() +{ + unsigned long flags; + spin_lock_irqsave(&tracepoint_reg_lock, flags); + if (tracepoint_reg_count <= 1) { + ksu_mark_running_process_locked(); + } else { + pr_info("hook_manager: not mark running process since syscall tracepoint is in use\n"); + } + spin_unlock_irqrestore(&tracepoint_reg_lock, flags); +} + +// Get task mark status +// Returns: 1 if marked, 0 if not marked, -ESRCH if task not found +int ksu_get_task_mark(pid_t pid) +{ + struct task_struct *task; + int marked = -ESRCH; + + rcu_read_lock(); + task = find_task_by_vpid(pid); + if (task) { + get_task_struct(task); + rcu_read_unlock(); +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) + marked = test_task_syscall_work(task, SYSCALL_TRACEPOINT) ? 1 : 0; +#else + marked = test_tsk_thread_flag(task, TIF_SYSCALL_TRACEPOINT) ? 1 : 0; +#endif + put_task_struct(task); + } else { + rcu_read_unlock(); + } + + return marked; +} + +// Set task mark status +// Returns: 0 on success, -ESRCH if task not found +int ksu_set_task_mark(pid_t pid, bool mark) +{ + struct task_struct *task; + int ret = -ESRCH; + + rcu_read_lock(); + task = find_task_by_vpid(pid); + if (task) { + get_task_struct(task); + rcu_read_unlock(); + if (mark) { + ksu_set_task_tracepoint_flag(task); + pr_info("hook_manager: marked task pid=%d comm=%s\n", pid, task->comm); + } else { + ksu_clear_task_tracepoint_flag(task); + pr_info("hook_manager: unmarked task pid=%d comm=%s\n", pid, task->comm); + } + put_task_struct(task); + ret = 0; + } else { + rcu_read_unlock(); + } + + return ret; +} + +#ifdef CONFIG_KRETPROBES + +static struct kretprobe *init_kretprobe(const char *name, + kretprobe_handler_t handler) +{ + struct kretprobe *rp = kzalloc(sizeof(struct kretprobe), GFP_KERNEL); + if (!rp) + return NULL; + rp->kp.symbol_name = name; + rp->handler = handler; + rp->data_size = 0; + rp->maxactive = 0; + + int ret = register_kretprobe(rp); + pr_info("hook_manager: register_%s kretprobe: %d\n", name, ret); + if (ret) { + kfree(rp); + return NULL; + } + + return rp; +} + +static void destroy_kretprobe(struct kretprobe **rp_ptr) +{ + struct kretprobe *rp = *rp_ptr; + if (!rp) + return; + unregister_kretprobe(rp); + synchronize_rcu(); + kfree(rp); + *rp_ptr = NULL; +} + +static int syscall_regfunc_handler(struct kretprobe_instance *ri, struct pt_regs *regs) +{ + unsigned long flags; + spin_lock_irqsave(&tracepoint_reg_lock, flags); + if (tracepoint_reg_count < 1) { + // while install our tracepoint, mark our processes + ksu_mark_running_process_locked(); + } else if (tracepoint_reg_count == 1) { + // while other tracepoint first added, mark all processes + ksu_mark_all_process(); + } + tracepoint_reg_count++; + spin_unlock_irqrestore(&tracepoint_reg_lock, flags); + return 0; +} + +static int syscall_unregfunc_handler(struct kretprobe_instance *ri, struct pt_regs *regs) +{ + unsigned long flags; + spin_lock_irqsave(&tracepoint_reg_lock, flags); + tracepoint_reg_count--; + if (tracepoint_reg_count <= 0) { + // while no tracepoint left, unmark all processes + ksu_unmark_all_process(); + } else if (tracepoint_reg_count == 1) { + // while just our tracepoint left, unmark disallowed processes + ksu_mark_running_process_locked(); + } + spin_unlock_irqrestore(&tracepoint_reg_lock, flags); + return 0; +} + +static struct kretprobe *syscall_regfunc_rp = NULL; +static struct kretprobe *syscall_unregfunc_rp = NULL; +#endif + +static inline bool check_syscall_fastpath(int nr) +{ + switch (nr) { + case __NR_newfstatat: + case __NR_faccessat: + case __NR_execve: + case __NR_setresuid: + case __NR_clone: + case __NR_clone3: + return true; + default: + return false; + } +} + +// Unmark init's child that are not zygote, adbd or ksud +int ksu_handle_init_mark_tracker(const char __user **filename_user) +{ + char path[64]; + + if (unlikely(!filename_user)) + return 0; + + memset(path, 0, sizeof(path)); + strncpy_from_user_nofault(path, *filename_user, sizeof(path)); + + if (likely(strstr(path, "/app_process") == NULL && strstr(path, "/adbd") == NULL && strstr(path, "/ksud") == NULL)) { + pr_info("hook_manager: unmark %d exec %s", current->pid, path); + ksu_clear_task_tracepoint_flag_if_needed(current); + } + + return 0; +} +#ifdef CONFIG_KSU_MANUAL_SU +#include "manual_su.h" +static inline void ksu_handle_task_alloc(struct pt_regs *regs) +{ + ksu_try_escalate_for_uid(current_uid().val); +} +#endif + +#ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS +// Generic sys_enter handler that dispatches to specific handlers +static void ksu_sys_enter_handler(void *data, struct pt_regs *regs, long id) +{ + if (unlikely(check_syscall_fastpath(id))) { +#ifdef KSU_TP_HOOK + if (ksu_su_compat_enabled) { + // Handle newfstatat + if (id == __NR_newfstatat) { + int *dfd = (int *)&PT_REGS_PARM1(regs); + const char __user **filename_user = + (const char __user **)&PT_REGS_PARM2(regs); + int *flags = (int *)&PT_REGS_SYSCALL_PARM4(regs); + ksu_handle_stat(dfd, filename_user, flags); + return; + } + + // Handle faccessat + if (id == __NR_faccessat) { + int *dfd = (int *)&PT_REGS_PARM1(regs); + const char __user **filename_user = + (const char __user **)&PT_REGS_PARM2(regs); + int *mode = (int *)&PT_REGS_PARM3(regs); + ksu_handle_faccessat(dfd, filename_user, mode, NULL); + return; + } + + // Handle execve + if (id == __NR_execve) { + const char __user **filename_user = + (const char __user **)&PT_REGS_PARM1(regs); + if (current->pid != 1 && is_init(get_current_cred())) { + ksu_handle_init_mark_tracker(filename_user); + } else { + ksu_handle_execve_sucompat(filename_user, NULL, NULL, NULL); + } + return; + } + } +#endif + + // Handle setresuid + if (id == __NR_setresuid) { + uid_t ruid = (uid_t)PT_REGS_PARM1(regs); + uid_t euid = (uid_t)PT_REGS_PARM2(regs); + uid_t suid = (uid_t)PT_REGS_PARM3(regs); + ksu_handle_setresuid(ruid, euid, suid); + return; + } + +#ifdef CONFIG_KSU_MANUAL_SU + // Handle task_alloc via clone/fork + if (id == __NR_clone || id == __NR_clone3) + return ksu_handle_task_alloc(regs); +#endif + } +} +#endif + +void ksu_syscall_hook_manager_init(void) +{ + int ret; + pr_info("hook_manager: ksu_hook_manager_init called\n"); + +#ifdef CONFIG_KRETPROBES + // Register kretprobe for syscall_regfunc + syscall_regfunc_rp = init_kretprobe("syscall_regfunc", syscall_regfunc_handler); + // Register kretprobe for syscall_unregfunc + syscall_unregfunc_rp = init_kretprobe("syscall_unregfunc", syscall_unregfunc_handler); +#endif + +#ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS + ret = register_trace_sys_enter(ksu_sys_enter_handler, NULL); +#ifndef CONFIG_KRETPROBES + ksu_mark_running_process_locked(); +#endif + if (ret) { + pr_err("hook_manager: failed to register sys_enter tracepoint: %d\n", ret); + } else { + pr_info("hook_manager: sys_enter tracepoint registered\n"); + } +#endif + + ksu_setuid_hook_init(); + ksu_sucompat_init(); +} + +void ksu_syscall_hook_manager_exit(void) +{ + pr_info("hook_manager: ksu_hook_manager_exit called\n"); +#ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS + unregister_trace_sys_enter(ksu_sys_enter_handler, NULL); + tracepoint_synchronize_unregister(); + pr_info("hook_manager: sys_enter tracepoint unregistered\n"); +#endif + +#ifdef CONFIG_KRETPROBES + destroy_kretprobe(&syscall_regfunc_rp); + destroy_kretprobe(&syscall_unregfunc_rp); +#endif + + ksu_sucompat_exit(); + ksu_setuid_hook_exit(); +} diff --git a/kernel/syscall_hook_manager.h b/kernel/syscall_hook_manager.h new file mode 100644 index 0000000..90245c2 --- /dev/null +++ b/kernel/syscall_hook_manager.h @@ -0,0 +1,47 @@ +#ifndef __KSU_H_HOOK_MANAGER +#define __KSU_H_HOOK_MANAGER + +#include +#include +#include +#include +#include +#include +#include +#include "selinux/selinux.h" + +// Hook manager initialization and cleanup +void ksu_syscall_hook_manager_init(void); +void ksu_syscall_hook_manager_exit(void); + +// Process marking for tracepoint +void ksu_mark_all_process(void); +void ksu_unmark_all_process(void); +void ksu_mark_running_process(void); + +// Per-task mark operations +int ksu_get_task_mark(pid_t pid); +int ksu_set_task_mark(pid_t pid, bool mark); + + +static inline void ksu_set_task_tracepoint_flag(struct task_struct *t) +{ +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) + set_task_syscall_work(t, SYSCALL_TRACEPOINT); +#else + set_tsk_thread_flag(t, TIF_SYSCALL_TRACEPOINT); +#endif +} + +static inline void ksu_clear_task_tracepoint_flag(struct task_struct *t) +{ +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) + clear_task_syscall_work(t, SYSCALL_TRACEPOINT); +#else + clear_tsk_thread_flag(t, TIF_SYSCALL_TRACEPOINT); +#endif +} + +void ksu_clear_task_tracepoint_flag_if_needed(struct task_struct *t); + +#endif diff --git a/kernel/throne_comm.c b/kernel/throne_comm.c index 97d8b18..90067dc 100644 --- a/kernel/throne_comm.c +++ b/kernel/throne_comm.c @@ -7,119 +7,208 @@ #include "klog.h" #include "throne_comm.h" +#include "ksu.h" #define PROC_UID_SCANNER "ksu_uid_scanner" +#define UID_SCANNER_STATE_FILE "/data/adb/ksu/.uid_scanner" static struct proc_dir_entry *proc_entry = NULL; static struct workqueue_struct *scanner_wq = NULL; static struct work_struct scan_work; +static struct work_struct ksu_state_save_work; +static struct work_struct ksu_state_load_work; + // Signal userspace to rescan static bool need_rescan = false; static void rescan_work_fn(struct work_struct *work) { - // Signal userspace through proc interface - need_rescan = true; - pr_info("requested userspace uid rescan\n"); + // Signal userspace through proc interface + need_rescan = true; + pr_info("requested userspace uid rescan\n"); } void ksu_request_userspace_scan(void) { - if (scanner_wq) { - queue_work(scanner_wq, &scan_work); - } + if (scanner_wq) { + queue_work(scanner_wq, &scan_work); + } } void ksu_handle_userspace_update(void) { - // Called when userspace notifies update complete - need_rescan = false; - pr_info("userspace uid list updated\n"); + // Called when userspace notifies update complete + need_rescan = false; + pr_info("userspace uid list updated\n"); +} + +static void do_save_throne_state(struct work_struct *work) +{ + struct file *fp; + char state_char = ksu_uid_scanner_enabled ? '1' : '0'; + loff_t off = 0; + + fp = filp_open(UID_SCANNER_STATE_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (IS_ERR(fp)) { + pr_err("save_throne_state create file failed: %ld\n", PTR_ERR(fp)); + return; + } + + if (kernel_write(fp, &state_char, sizeof(state_char), &off) != sizeof(state_char)) { + pr_err("save_throne_state write failed\n"); + goto exit; + } + + pr_info("throne state saved: %s\n", ksu_uid_scanner_enabled ? "enabled" : "disabled"); + +exit: + filp_close(fp, 0); +} + +void do_load_throne_state(struct work_struct *work) +{ + struct file *fp; + char state_char; + loff_t off = 0; + ssize_t ret; + + fp = filp_open(UID_SCANNER_STATE_FILE, O_RDONLY, 0); + if (IS_ERR(fp)) { + pr_info("throne state file not found, using default: disabled\n"); + ksu_uid_scanner_enabled = false; + return; + } + + ret = kernel_read(fp, &state_char, sizeof(state_char), &off); + if (ret != sizeof(state_char)) { + pr_err("load_throne_state read err: %zd\n", ret); + ksu_uid_scanner_enabled = false; + goto exit; + } + + ksu_uid_scanner_enabled = (state_char == '1'); + pr_info("throne state loaded: %s\n", ksu_uid_scanner_enabled ? "enabled" : "disabled"); + +exit: + filp_close(fp, 0); +} + +bool ksu_throne_comm_load_state(void) +{ + return ksu_queue_work(&ksu_state_load_work); +} + +void ksu_throne_comm_save_state(void) +{ + ksu_queue_work(&ksu_state_save_work); } static int uid_scanner_show(struct seq_file *m, void *v) { - if (need_rescan) { - seq_puts(m, "RESCAN\n"); - } else { - seq_puts(m, "OK\n"); - } - return 0; + if (need_rescan) { + seq_puts(m, "RESCAN\n"); + } else { + seq_puts(m, "OK\n"); + } + return 0; } static int uid_scanner_open(struct inode *inode, struct file *file) { - return single_open(file, uid_scanner_show, NULL); + return single_open(file, uid_scanner_show, NULL); } static ssize_t uid_scanner_write(struct file *file, const char __user *buffer, size_t count, loff_t *pos) { - char cmd[16]; - - if (count >= sizeof(cmd)) - return -EINVAL; - - if (copy_from_user(cmd, buffer, count)) - return -EFAULT; - - cmd[count] = '\0'; - - // Remove newline if present - if (count > 0 && cmd[count-1] == '\n') - cmd[count-1] = '\0'; - - if (strcmp(cmd, "UPDATED") == 0) { - ksu_handle_userspace_update(); - pr_info("received userspace update notification\n"); - } - - return count; + char cmd[16]; + + if (count >= sizeof(cmd)) + return -EINVAL; + + if (copy_from_user(cmd, buffer, count)) + return -EFAULT; + + cmd[count] = '\0'; + + // Remove newline if present + if (count > 0 && cmd[count-1] == '\n') + cmd[count-1] = '\0'; + + if (strcmp(cmd, "UPDATED") == 0) { + ksu_handle_userspace_update(); + pr_info("received userspace update notification\n"); + } + + return count; } +#ifdef KSU_COMPAT_HAS_PROC_OPS static const struct proc_ops uid_scanner_proc_ops = { .proc_open = uid_scanner_open, .proc_read = seq_read, - .proc_write = uid_scanner_write, + .proc_write = uid_scanner_write, .proc_lseek = seq_lseek, .proc_release = single_release, }; +#else +static const struct file_operations uid_scanner_proc_ops = { + .owner = THIS_MODULE, + .open = uid_scanner_open, + .read = seq_read, + .write = uid_scanner_write, + .llseek = seq_lseek, + .release = single_release, +}; +#endif int ksu_throne_comm_init(void) { - // Create workqueue - scanner_wq = alloc_workqueue("ksu_scanner", WQ_UNBOUND, 1); - if (!scanner_wq) { - pr_err("failed to create scanner workqueue\n"); - return -ENOMEM; - } - - INIT_WORK(&scan_work, rescan_work_fn); - - // Create proc entry - proc_entry = proc_create(PROC_UID_SCANNER, 0600, NULL, &uid_scanner_proc_ops); - if (!proc_entry) { - pr_err("failed to create proc entry\n"); - destroy_workqueue(scanner_wq); - return -ENOMEM; - } - - pr_info("throne communication initialized\n"); - return 0; + // Create workqueue + scanner_wq = alloc_workqueue("ksu_scanner", WQ_UNBOUND, 1); + if (!scanner_wq) { + pr_err("failed to create scanner workqueue\n"); + return -ENOMEM; + } + + INIT_WORK(&scan_work, rescan_work_fn); + + // Create proc entry + proc_entry = proc_create(PROC_UID_SCANNER, 0600, NULL, &uid_scanner_proc_ops); + if (!proc_entry) { + pr_err("failed to create proc entry\n"); + destroy_workqueue(scanner_wq); + return -ENOMEM; + } + + pr_info("throne communication initialized\n"); + return 0; } void ksu_throne_comm_exit(void) { - if (proc_entry) { - proc_remove(proc_entry); - proc_entry = NULL; - } - - if (scanner_wq) { - destroy_workqueue(scanner_wq); - scanner_wq = NULL; - } - - pr_info("throne communication cleaned up\n"); + if (proc_entry) { + proc_remove(proc_entry); + proc_entry = NULL; + } + + if (scanner_wq) { + destroy_workqueue(scanner_wq); + scanner_wq = NULL; + } + + pr_info("throne communication cleaned up\n"); +} + +int ksu_uid_init(void) +{ + INIT_WORK(&ksu_state_save_work, do_save_throne_state); + INIT_WORK(&ksu_state_load_work, do_load_throne_state); + return 0; +} + +void ksu_uid_exit(void) +{ + do_save_throne_state(NULL); } \ No newline at end of file diff --git a/kernel/throne_comm.h b/kernel/throne_comm.h index eedf8c1..4deba2a 100644 --- a/kernel/throne_comm.h +++ b/kernel/throne_comm.h @@ -9,4 +9,14 @@ int ksu_throne_comm_init(void); void ksu_throne_comm_exit(void); +int ksu_uid_init(void); + +void ksu_uid_exit(void); + +bool ksu_throne_comm_load_state(void); + +void ksu_throne_comm_save_state(void); + +void do_load_throne_state(struct work_struct *work); + #endif \ No newline at end of file diff --git a/kernel/throne_tracker.c b/kernel/throne_tracker.c index 45e07ad..eb8a0b3 100644 --- a/kernel/throne_tracker.c +++ b/kernel/throne_tracker.c @@ -10,232 +10,217 @@ #include "allowlist.h" #include "klog.h" // IWYU pragma: keep -#include "ksu.h" #include "manager.h" #include "throne_tracker.h" -#include "kernel_compat.h" +#include "apk_sign.h" #include "dynamic_manager.h" #include "throne_comm.h" uid_t ksu_manager_uid = KSU_INVALID_UID; +static uid_t locked_manager_uid = KSU_INVALID_UID; +static uid_t locked_dynamic_manager_uid = KSU_INVALID_UID; #define KSU_UID_LIST_PATH "/data/misc/user_uid/uid_list" -#define USER_DATA_PATH "/data/user_de/0" -#define USER_DATA_PATH_LEN 256 +#define SYSTEM_PACKAGES_LIST_PATH "/data/system/packages.list" struct uid_data { - struct list_head list; - u32 uid; - char package[KSU_MAX_PACKAGE_NAME]; + struct list_head list; + u32 uid; + char package[KSU_MAX_PACKAGE_NAME]; }; -// Try read whitelist first, fallback if failed -static int read_uid_whitelist(struct list_head *uid_list) +// Try read /data/misc/user_uid/uid_list +static int uid_from_um_list(struct list_head *uid_list) { - struct file *fp; - char *file_content = NULL; - char *line, *next_line; - loff_t file_size; - loff_t pos = 0; - int count = 0; - ssize_t bytes_read; - - fp = ksu_filp_open_compat(KSU_UID_LIST_PATH, O_RDONLY, 0); - if (IS_ERR(fp)) { - pr_info("whitelist not found, fallback needed\n"); - return -ENOENT; - } + struct file *fp; + char *buf = NULL; + loff_t size, pos = 0; + ssize_t nr; + int cnt = 0; - file_size = fp->f_inode->i_size; - if (file_size <= 0) { - pr_info("whitelist file is empty\n"); - filp_close(fp, NULL); - return -ENODATA; - } + fp = filp_open(KSU_UID_LIST_PATH, O_RDONLY, 0); + if (IS_ERR(fp)) + return -ENOENT; - file_content = kzalloc(file_size + 1, GFP_ATOMIC); - if (!file_content) { - pr_err("failed to allocate memory for whitelist file (%lld bytes)\n", file_size); - filp_close(fp, NULL); - return -ENOMEM; - } + size = fp->f_inode->i_size; + if (size <= 0) { + filp_close(fp, NULL); + return -ENODATA; + } - bytes_read = ksu_kernel_read_compat(fp, file_content, file_size, &pos); - if (bytes_read != file_size) { - pr_err("failed to read whitelist file: read %zd bytes, expected %lld bytes\n", - bytes_read, file_size); - kfree(file_content); - filp_close(fp, NULL); - return -EIO; - } + buf = kzalloc(size + 1, GFP_ATOMIC); + if (!buf) { + pr_err("uid_list: OOM %lld B\n", size); + filp_close(fp, NULL); + return -ENOMEM; + } - file_content[file_size] = '\0'; - filp_close(fp, NULL); + nr = kernel_read(fp, buf, size, &pos); + filp_close(fp, NULL); + if (nr != size) { + pr_err("uid_list: short read %zd/%lld\n", nr, size); + kfree(buf); + return -EIO; + } + buf[size] = '\0'; - pr_info("successfully read whitelist file (%lld bytes), parsing lines...\n", file_size); + for (char *line = buf, *next; line; line = next) { + next = strchr(line, '\n'); + if (next) *next++ = '\0'; - line = file_content; - while (line && *line) { - next_line = strchr(line, '\n'); - if (next_line) { - *next_line = '\0'; - next_line++; - } + while (*line == ' ' || *line == '\t' || *line == '\r') ++line; + if (!*line) continue; - char *trimmed_line = line; - while (*trimmed_line == ' ' || *trimmed_line == '\t' || *trimmed_line == '\r') { - trimmed_line++; - } + char *uid_str = strsep(&line, " \t"); + char *pkg = line; + if (!pkg) continue; + while (*pkg == ' ' || *pkg == '\t') ++pkg; + if (!*pkg) continue; - if (strlen(trimmed_line) > 0) { - char *line_copy = trimmed_line; - char *uid_str = strsep(&line_copy, " \t"); - char *package_name = line_copy; - - if (package_name) { - while (*package_name == ' ' || *package_name == '\t') { - package_name++; - } - } - - if (uid_str && package_name && strlen(package_name) > 0) { - u32 uid; - if (!kstrtou32(uid_str, 10, &uid)) { - struct uid_data *data = kzalloc(sizeof(struct uid_data), GFP_ATOMIC); - if (data) { - data->uid = uid; - size_t pkg_len = strlen(package_name); - size_t copy_len = min(pkg_len, (size_t)(KSU_MAX_PACKAGE_NAME - 1)); - strncpy(data->package, package_name, copy_len); - data->package[copy_len] = '\0'; - - list_add_tail(&data->list, uid_list); - count++; - - if (count % 100 == 0) { - pr_info("parsed %d packages so far...\n", count); - } - } else { - pr_err("failed to allocate memory for uid_data\n"); - } - } else { - pr_warn("invalid uid format in line: %s\n", trimmed_line); - } - } else { - pr_warn("invalid line format: %s\n", trimmed_line); - } - } + u32 uid; + if (kstrtou32(uid_str, 10, &uid)) { + pr_warn_once("uid_list: bad uid <%s>\n", uid_str); + continue; + } - line = next_line; - } - - kfree(file_content); - pr_info("successfully loaded %d uids from whitelist\n", count); - return count > 0 ? 0 : -ENODATA; + struct uid_data *d = kzalloc(sizeof(*d), GFP_ATOMIC); + if (unlikely(!d)) { + pr_err("uid_list: OOM uid=%u\n", uid); + continue; + } + + d->uid = uid; + strscpy(d->package, pkg, KSU_MAX_PACKAGE_NAME); + list_add_tail(&d->list, uid_list); + ++cnt; + } + + kfree(buf); + pr_info("uid_list: loaded %d entries\n", cnt); + return cnt > 0 ? 0 : -ENODATA; } static int get_pkg_from_apk_path(char *pkg, const char *path) { - int len = strlen(path); - if (len >= KSU_MAX_PACKAGE_NAME || len < 1) - return -1; + int len = strlen(path); + if (len >= KSU_MAX_PACKAGE_NAME || len < 1) + return -1; - const char *last_slash = NULL; - const char *second_last_slash = NULL; + const char *last_slash = NULL; + const char *second_last_slash = NULL; - int i; - for (i = len - 1; i >= 0; i--) { - if (path[i] == '/') { - if (!last_slash) { - last_slash = &path[i]; - } else { - second_last_slash = &path[i]; - break; - } - } - } + int i; + for (i = len - 1; i >= 0; i--) { + if (path[i] == '/') { + if (!last_slash) { + last_slash = &path[i]; + } else { + second_last_slash = &path[i]; + break; + } + } + } - if (!last_slash || !second_last_slash) - return -1; + if (!last_slash || !second_last_slash) + return -1; - const char *last_hyphen = strchr(second_last_slash, '-'); - if (!last_hyphen || last_hyphen > last_slash) - return -1; + const char *last_hyphen = strchr(second_last_slash, '-'); + if (!last_hyphen || last_hyphen > last_slash) + return -1; - int pkg_len = last_hyphen - second_last_slash - 1; - if (pkg_len >= KSU_MAX_PACKAGE_NAME || pkg_len <= 0) - return -1; + int pkg_len = last_hyphen - second_last_slash - 1; + if (pkg_len >= KSU_MAX_PACKAGE_NAME || pkg_len <= 0) + return -1; - // Copying the package name - strncpy(pkg, second_last_slash + 1, pkg_len); - pkg[pkg_len] = '\0'; + // Copying the package name + strncpy(pkg, second_last_slash + 1, pkg_len); + pkg[pkg_len] = '\0'; - return 0; + return 0; } static void crown_manager(const char *apk, struct list_head *uid_data, int signature_index) { - char pkg[KSU_MAX_PACKAGE_NAME]; - if (get_pkg_from_apk_path(pkg, apk) < 0) { - pr_err("Failed to get package name from apk path: %s\n", apk); - return; - } + char pkg[KSU_MAX_PACKAGE_NAME]; + if (get_pkg_from_apk_path(pkg, apk) < 0) { + pr_err("Failed to get package name from apk path: %s\n", apk); + return; + } - pr_info("manager pkg: %s, signature_index: %d\n", pkg, signature_index); + pr_info("manager pkg: %s, signature_index: %d\n", pkg, signature_index); #ifdef KSU_MANAGER_PACKAGE - // pkg is `/` - if (strncmp(pkg, KSU_MANAGER_PACKAGE, sizeof(KSU_MANAGER_PACKAGE))) { - pr_info("manager package is inconsistent with kernel build: %s\n", - KSU_MANAGER_PACKAGE); - return; - } + // pkg is `/` + if (strncmp(pkg, KSU_MANAGER_PACKAGE, sizeof(KSU_MANAGER_PACKAGE))) { + pr_info("manager package is inconsistent with kernel build: %s\n", + KSU_MANAGER_PACKAGE); + return; + } #endif - struct list_head *list = (struct list_head *)uid_data; - struct uid_data *np; + struct uid_data *np; - list_for_each_entry (np, list, list) { - if (strncmp(np->package, pkg, KSU_MAX_PACKAGE_NAME) == 0) { - pr_info("Crowning manager: %s(uid=%d, signature_index=%d)\n", pkg, np->uid, signature_index); - - // Dynamic Sign index (1) or multi-manager signatures (2+) - if (signature_index == DYNAMIC_SIGN_INDEX || signature_index >= 2) { - ksu_add_manager(np->uid, signature_index); - - if (!ksu_is_manager_uid_valid()) { - ksu_set_manager_uid(np->uid); - } - } else { - ksu_set_manager_uid(np->uid); - } - break; - } - } + list_for_each_entry(np, uid_data, list) { + if (strncmp(np->package, pkg, KSU_MAX_PACKAGE_NAME) == 0) { + bool is_dynamic = (signature_index == DYNAMIC_SIGN_INDEX || signature_index >= 2); + + if (is_dynamic) { + if (locked_dynamic_manager_uid != KSU_INVALID_UID && locked_dynamic_manager_uid != np->uid) { + pr_info("Unlocking previous dynamic manager UID: %d\n", locked_dynamic_manager_uid); + ksu_remove_manager(locked_dynamic_manager_uid); + locked_dynamic_manager_uid = KSU_INVALID_UID; + } + } else { + if (locked_manager_uid != KSU_INVALID_UID && locked_manager_uid != np->uid) { + pr_info("Unlocking previous manager UID: %d\n", locked_manager_uid); + ksu_invalidate_manager_uid(); // unlock old one + locked_manager_uid = KSU_INVALID_UID; + } + } + + pr_info("Crowning %s manager: %s (uid=%d, signature_index=%d)\n", + is_dynamic ? "dynamic" : "traditional", pkg, np->uid, signature_index); + + if (is_dynamic) { + ksu_add_manager(np->uid, signature_index); + locked_dynamic_manager_uid = np->uid; + + // If there is no traditional manager, set it to the current UID + if (!ksu_is_manager_uid_valid()) { + ksu_set_manager_uid(np->uid); + locked_manager_uid = np->uid; + } + } else { + ksu_set_manager_uid(np->uid); // throne new UID + locked_manager_uid = np->uid; // store locked UID + } + break; + } + } } #define DATA_PATH_LEN 384 // 384 is enough for /data/app//base.apk struct data_path { - char dirpath[DATA_PATH_LEN]; - int depth; - struct list_head list; + char dirpath[DATA_PATH_LEN]; + int depth; + struct list_head list; }; struct apk_path_hash { - unsigned int hash; - bool exists; - struct list_head list; + unsigned int hash; + bool exists; + struct list_head list; }; static struct list_head apk_path_hash_list = LIST_HEAD_INIT(apk_path_hash_list); struct my_dir_context { - struct dir_context ctx; - struct list_head *data_path_list; - char *parent_dir; - void *private_data; - int depth; - int *stop; + struct dir_context ctx; + struct list_head *data_path_list; + char *parent_dir; + void *private_data; + int depth; + int *stop; }; // https://docs.kernel.org/filesystems/porting.html // filldir_t (readdir callbacks) calling conventions have changed. Instead of returning 0 or -E... it returns bool now. false means "no more" (as -E... used to) and true - "keep going" (as 0 in old calling conventions). Rationale: callers never looked at specific -E... values anyway. -> iterate_shared() instances require no changes at all, all filldir_t ones in the tree converted. @@ -248,414 +233,335 @@ struct my_dir_context { #define FILLDIR_ACTOR_CONTINUE 0 #define FILLDIR_ACTOR_STOP -EINVAL #endif - -struct uid_scan_stats { - size_t total_found; - size_t errors_encountered; -}; - -struct user_data_context { - struct dir_context ctx; - struct list_head *uid_list; - struct uid_scan_stats *stats; -}; - -FILLDIR_RETURN_TYPE user_data_actor(struct dir_context *ctx, const char *name, - int namelen, loff_t off, u64 ino, - unsigned int d_type) -{ - struct user_data_context *my_ctx = - container_of(ctx, struct user_data_context, ctx); - - if (!my_ctx || !my_ctx->uid_list) { - return FILLDIR_ACTOR_STOP; - } - - if (!strncmp(name, "..", namelen) || !strncmp(name, ".", namelen)) - return FILLDIR_ACTOR_CONTINUE; - - if (d_type != DT_DIR) - return FILLDIR_ACTOR_CONTINUE; - - if (namelen >= KSU_MAX_PACKAGE_NAME) { - pr_warn("Package name too long: %.*s\n", namelen, name); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - char package_path[USER_DATA_PATH_LEN]; - if (snprintf(package_path, sizeof(package_path), "%s/%.*s", - USER_DATA_PATH, namelen, name) >= sizeof(package_path)) { - pr_err("Path too long for package: %.*s\n", namelen, name); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - struct path path; - int err = kern_path(package_path, LOOKUP_FOLLOW, &path); - if (err) { - pr_debug("Package path lookup failed: %s (err: %d)\n", package_path, err); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - struct kstat stat; - err = vfs_getattr(&path, &stat, STATX_UID, AT_STATX_SYNC_AS_STAT); - path_put(&path); - - if (err) { - pr_debug("Failed to get attributes for: %s (err: %d)\n", package_path, err); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - uid_t uid = from_kuid(&init_user_ns, stat.uid); - if (uid == (uid_t)-1) { - pr_warn("Invalid UID for package: %.*s\n", namelen, name); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - struct uid_data *data = kzalloc(sizeof(struct uid_data), GFP_ATOMIC); - if (!data) { - pr_err("Failed to allocate memory for package: %.*s\n", namelen, name); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - data->uid = uid; - size_t copy_len = min(namelen, KSU_MAX_PACKAGE_NAME - 1); - strncpy(data->package, name, copy_len); - data->package[copy_len] = '\0'; - - list_add_tail(&data->list, my_ctx->uid_list); - - if (my_ctx->stats) - my_ctx->stats->total_found++; - - pr_info("UserDE UID: Found package: %s, uid: %u\n", data->package, data->uid); - - return FILLDIR_ACTOR_CONTINUE; -} - -int scan_user_data_for_uids(struct list_head *uid_list) -{ - struct file *dir_file; - struct uid_scan_stats stats = {0}; - int ret = 0; - - if (!uid_list) { - return -EINVAL; - } - - dir_file = ksu_filp_open_compat(USER_DATA_PATH, O_RDONLY, 0); - if (IS_ERR(dir_file)) { - pr_err("UserDE UID: Failed to open %s: %ld\n", USER_DATA_PATH, PTR_ERR(dir_file)); - return PTR_ERR(dir_file); - } - - struct user_data_context ctx = { - .ctx.actor = user_data_actor, - .uid_list = uid_list, - .stats = &stats - }; - - ret = iterate_dir(dir_file, &ctx.ctx); - filp_close(dir_file, NULL); - - if (stats.errors_encountered > 0) { - pr_warn("Encountered %zu errors while scanning user data directory\n", - stats.errors_encountered); - } - - pr_info("UserDE UID: Scanned user data directory, found %zu packages with %zu errors\n", - stats.total_found, stats.errors_encountered); - - return ret; -} - FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name, - int namelen, loff_t off, u64 ino, - unsigned int d_type) + int namelen, loff_t off, u64 ino, + unsigned int d_type) { - struct my_dir_context *my_ctx = - container_of(ctx, struct my_dir_context, ctx); - char dirpath[DATA_PATH_LEN]; + struct my_dir_context *my_ctx = + container_of(ctx, struct my_dir_context, ctx); + char dirpath[DATA_PATH_LEN]; - if (!my_ctx) { - pr_err("Invalid context\n"); - return FILLDIR_ACTOR_STOP; - } - if (my_ctx->stop && *my_ctx->stop) { - pr_info("Stop searching\n"); - return FILLDIR_ACTOR_STOP; - } + if (!my_ctx) { + pr_err("Invalid context\n"); + return FILLDIR_ACTOR_STOP; + } + if (my_ctx->stop && *my_ctx->stop) { + pr_info("Stop searching\n"); + return FILLDIR_ACTOR_STOP; + } - if (!strncmp(name, "..", namelen) || !strncmp(name, ".", namelen)) - return FILLDIR_ACTOR_CONTINUE; // Skip "." and ".." + if (!strncmp(name, "..", namelen) || !strncmp(name, ".", namelen)) + return FILLDIR_ACTOR_CONTINUE; // Skip "." and ".." - if (d_type == DT_DIR && namelen >= 8 && !strncmp(name, "vmdl", 4) && - !strncmp(name + namelen - 4, ".tmp", 4)) { - pr_info("Skipping directory: %.*s\n", namelen, name); - return FILLDIR_ACTOR_CONTINUE; // Skip staging package - } - + if (d_type == DT_DIR && namelen >= 8 && !strncmp(name, "vmdl", 4) && + !strncmp(name + namelen - 4, ".tmp", 4)) { + pr_info("Skipping directory: %.*s\n", namelen, name); + return FILLDIR_ACTOR_CONTINUE; // Skip staging package + } + + if (snprintf(dirpath, DATA_PATH_LEN, "%s/%.*s", my_ctx->parent_dir, + namelen, name) >= DATA_PATH_LEN) { + pr_err("Path too long: %s/%.*s\n", my_ctx->parent_dir, namelen, + name); + return FILLDIR_ACTOR_CONTINUE; + } - if (snprintf(dirpath, DATA_PATH_LEN, "%s/%.*s", my_ctx->parent_dir, - namelen, name) >= DATA_PATH_LEN) { - pr_err("Path too long: %s/%.*s\n", my_ctx->parent_dir, namelen, - name); - return FILLDIR_ACTOR_CONTINUE; - } + if (d_type == DT_DIR && my_ctx->depth > 0 && + (my_ctx->stop && !*my_ctx->stop)) { + struct data_path *data = kzalloc(sizeof(struct data_path), GFP_ATOMIC); - if (d_type == DT_DIR && my_ctx->depth > 0 && - (my_ctx->stop && !*my_ctx->stop)) { - struct data_path *data = kmalloc(sizeof(struct data_path), GFP_ATOMIC); + if (!data) { + pr_err("Failed to allocate memory for %s\n", dirpath); + return FILLDIR_ACTOR_CONTINUE; + } - if (!data) { - pr_err("Failed to allocate memory for %s\n", dirpath); - return FILLDIR_ACTOR_CONTINUE; - } + strscpy(data->dirpath, dirpath, DATA_PATH_LEN); + data->depth = my_ctx->depth - 1; + list_add_tail(&data->list, my_ctx->data_path_list); + } else { + if ((namelen == 8) && (strncmp(name, "base.apk", namelen) == 0)) { + struct apk_path_hash *pos, *n; +#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 8, 0) + unsigned int hash = full_name_hash(dirpath, strlen(dirpath)); +#else + unsigned int hash = full_name_hash(NULL, dirpath, strlen(dirpath)); +#endif + list_for_each_entry(pos, &apk_path_hash_list, list) { + if (hash == pos->hash) { + pos->exists = true; + return FILLDIR_ACTOR_CONTINUE; + } + } - strscpy(data->dirpath, dirpath, DATA_PATH_LEN); - data->depth = my_ctx->depth - 1; - list_add_tail(&data->list, my_ctx->data_path_list); - } else { - if ((namelen == 8) && (strncmp(name, "base.apk", namelen) == 0)) { - struct apk_path_hash *pos, *n; - unsigned int hash = full_name_hash(NULL, dirpath, strlen(dirpath)); - list_for_each_entry(pos, &apk_path_hash_list, list) { - if (hash == pos->hash) { - pos->exists = true; - return FILLDIR_ACTOR_CONTINUE; - } - } + int signature_index = -1; + bool is_multi_manager = is_dynamic_manager_apk( + dirpath, &signature_index); - int signature_index = -1; - bool is_multi_manager = is_dynamic_manager_apk( - dirpath, &signature_index); + pr_info("Found new base.apk at path: %s, is_multi_manager: %d, signature_index: %d\n", + dirpath, is_multi_manager, signature_index); + + // Check for dynamic sign or multi-manager signatures + if (is_multi_manager && (signature_index == DYNAMIC_SIGN_INDEX || signature_index >= 2)) { + crown_manager(dirpath, my_ctx->private_data, signature_index); + } else if (is_manager_apk(dirpath)) { + crown_manager(dirpath, my_ctx->private_data, 0); + *my_ctx->stop = 1; + } - pr_info("Found new base.apk at path: %s, is_multi_manager: %d, signature_index: %d\n", - dirpath, is_multi_manager, signature_index); - - // Check for dynamic sign or multi-manager signatures - if (is_multi_manager && (signature_index == DYNAMIC_SIGN_INDEX || signature_index >= 2)) { - crown_manager(dirpath, my_ctx->private_data, signature_index); - - struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC); - if (apk_data) { - apk_data->hash = hash; - apk_data->exists = true; - list_add_tail(&apk_data->list, &apk_path_hash_list); - } + struct apk_path_hash *apk_data = kzalloc(sizeof(*apk_data), GFP_ATOMIC); + if (apk_data) { + apk_data->hash = hash; + apk_data->exists = true; + list_add_tail(&apk_data->list, &apk_path_hash_list); + } - } else if (is_manager_apk(dirpath)) { - crown_manager(dirpath, my_ctx->private_data, 0); - *my_ctx->stop = 1; + if (is_manager_apk(dirpath)) { + // Manager found, clear APK cache list + list_for_each_entry_safe(pos, n, &apk_path_hash_list, list) { + list_del(&pos->list); + kfree(pos); + } + } + } + } - // Manager found, clear APK cache list - list_for_each_entry_safe(pos, n, &apk_path_hash_list, list) { - list_del(&pos->list); - kfree(pos); - } - } else { - struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC); - if (apk_data) { - apk_data->hash = hash; - apk_data->exists = true; - list_add_tail(&apk_data->list, &apk_path_hash_list); - } - } - } - } - - return FILLDIR_ACTOR_CONTINUE; + return FILLDIR_ACTOR_CONTINUE; } void search_manager(const char *path, int depth, struct list_head *uid_data) { - int i, stop = 0; - struct list_head data_path_list; - INIT_LIST_HEAD(&data_path_list); - unsigned long data_app_magic = 0; - - // Initialize APK cache list - struct apk_path_hash *pos, *n; - list_for_each_entry(pos, &apk_path_hash_list, list) { - pos->exists = false; - } + int i, stop = 0; + struct list_head data_path_list; + INIT_LIST_HEAD(&data_path_list); + unsigned long data_app_magic = 0; + + // Initialize APK cache list + struct apk_path_hash *pos, *n; + list_for_each_entry (pos, &apk_path_hash_list, list) { + pos->exists = false; + } - // First depth - struct data_path data; - strscpy(data.dirpath, path, DATA_PATH_LEN); - data.depth = depth; - list_add_tail(&data.list, &data_path_list); + // First depth + struct data_path data; + strscpy(data.dirpath, path, DATA_PATH_LEN); + data.depth = depth; + list_add_tail(&data.list, &data_path_list); - for (i = depth; i >= 0; i--) { - struct data_path *pos, *n; + for (i = depth; i >= 0; i--) { + struct data_path *pos, *n; - list_for_each_entry_safe(pos, n, &data_path_list, list) { - struct my_dir_context ctx = { .ctx.actor = my_actor, - .data_path_list = &data_path_list, - .parent_dir = pos->dirpath, - .private_data = uid_data, - .depth = pos->depth, - .stop = &stop }; - struct file *file; + list_for_each_entry_safe (pos, n, &data_path_list, list) { + struct my_dir_context ctx = { .ctx.actor = my_actor, + .data_path_list = &data_path_list, + .parent_dir = pos->dirpath, + .private_data = uid_data, + .depth = pos->depth, + .stop = &stop }; + struct file *file; - if (!stop) { - file = ksu_filp_open_compat(pos->dirpath, O_RDONLY | O_NOFOLLOW, 0); - if (IS_ERR(file)) { - pr_err("Failed to open directory: %s, err: %ld\n", pos->dirpath, PTR_ERR(file)); - goto skip_iterate; - } - - // grab magic on first folder, which is /data/app - if (!data_app_magic) { - if (file->f_inode->i_sb->s_magic) { - data_app_magic = file->f_inode->i_sb->s_magic; - pr_info("%s: dir: %s got magic! 0x%lx\n", __func__, pos->dirpath, data_app_magic); - } else { - filp_close(file, NULL); - goto skip_iterate; - } - } - - if (file->f_inode->i_sb->s_magic != data_app_magic) { - pr_info("%s: skip: %s magic: 0x%lx expected: 0x%lx\n", __func__, pos->dirpath, - file->f_inode->i_sb->s_magic, data_app_magic); - filp_close(file, NULL); - goto skip_iterate; - } + if (!stop) { + file = filp_open(pos->dirpath, O_RDONLY | O_NOFOLLOW, 0); + if (IS_ERR(file)) { + pr_err("Failed to open directory: %s, err: %ld\n", + pos->dirpath, PTR_ERR(file)); + goto skip_iterate; + } - iterate_dir(file, &ctx.ctx); - filp_close(file, NULL); - } -skip_iterate: - list_del(&pos->list); - if (pos != &data) - kfree(pos); - } - } + // grab magic on first folder, which is /data/app + if (!data_app_magic) { + if (file->f_inode->i_sb->s_magic) { + data_app_magic = file->f_inode->i_sb->s_magic; + pr_info("%s: dir: %s got magic! 0x%lx\n", __func__, + pos->dirpath, data_app_magic); + } else { + filp_close(file, NULL); + goto skip_iterate; + } + } - // Remove stale cached APK entries - list_for_each_entry_safe(pos, n, &apk_path_hash_list, list) { - if (!pos->exists) { - list_del(&pos->list); - kfree(pos); - } - } + if (file->f_inode->i_sb->s_magic != data_app_magic) { + pr_info("%s: skip: %s magic: 0x%lx expected: 0x%lx\n", + __func__, pos->dirpath, + file->f_inode->i_sb->s_magic, data_app_magic); + filp_close(file, NULL); + goto skip_iterate; + } + + iterate_dir(file, &ctx.ctx); + filp_close(file, NULL); + } + skip_iterate: + list_del(&pos->list); + if (pos != &data) + kfree(pos); + } + } + + // Remove stale cached APK entries + list_for_each_entry_safe (pos, n, &apk_path_hash_list, list) { + if (!pos->exists) { + list_del(&pos->list); + kfree(pos); + } + } } static bool is_uid_exist(uid_t uid, char *package, void *data) { - struct list_head *list = (struct list_head *)data; - struct uid_data *np; + struct list_head *list = (struct list_head *)data; + struct uid_data *np; - bool exist = false; - list_for_each_entry (np, list, list) { - if (np->uid == uid % 100000 && - strncmp(np->package, package, KSU_MAX_PACKAGE_NAME) == 0) { - exist = true; - break; - } - } - return exist; + bool exist = false; + list_for_each_entry (np, list, list) { + if (np->uid == uid % 100000 && + strncmp(np->package, package, KSU_MAX_PACKAGE_NAME) == 0) { + exist = true; + break; + } + } + return exist; } -void track_throne() +void track_throne(bool prune_only) { - struct list_head uid_list; - INIT_LIST_HEAD(&uid_list); + struct list_head uid_list; + struct uid_data *np, *n; + struct file *fp; + char chr = 0; + loff_t pos = 0; + loff_t line_start = 0; + char buf[KSU_MAX_PACKAGE_NAME]; + static bool manager_exist = false; + static bool dynamic_manager_exist = false; + int current_manager_uid = ksu_get_manager_uid() % 100000; - pr_info("track_throne triggered, attempting whitelist read\n"); - - // Try read whitelist first - int ret = read_uid_whitelist(&uid_list); - - if (ret < 0) { - pr_info("whitelist read failed (%d), request userspace scan, falling back to user_de \n", ret); + // init uid list head + INIT_LIST_HEAD(&uid_list); - int ret_user = scan_user_data_for_uids(&uid_list); - - if (ret_user < 0) { - goto out; - } else { - pr_info("UserDE UID: Successfully loaded %zu packages from user data directory\n", list_count_nodes(&uid_list)); - } - - } else { - pr_info("loaded uids from whitelist successfully\n"); - } + if (ksu_uid_scanner_enabled) { + pr_info("Scanning %s directory..\n", KSU_UID_LIST_PATH); - // now update uid list - struct uid_data *np; - struct uid_data *n; + if (uid_from_um_list(&uid_list) == 0) { + pr_info("Loaded UIDs from %s success\n", KSU_UID_LIST_PATH); + goto uid_ready; + } - // first, check if manager_uid exist! - bool manager_exist = false; - bool dynamic_manager_exist = false; - - list_for_each_entry (np, &uid_list, list) { - // if manager is installed in work profile, the uid in packages.list is still equals main profile - // don't delete it in this case! - int manager_uid = ksu_get_manager_uid() % 100000; - if (np->uid == manager_uid) { - manager_exist = true; - break; - } - } - - // Check for dynamic managers - if (!dynamic_manager_exist && ksu_is_dynamic_manager_enabled()) { - list_for_each_entry (np, &uid_list, list) { - // Check if this uid is a dynamic manager (not the traditional manager) - if (ksu_is_any_manager(np->uid) && np->uid != ksu_get_manager_uid()) { - dynamic_manager_exist = true; - break; - } - } - } + pr_warn("%s read failed, fallback to %s\n", + KSU_UID_LIST_PATH, SYSTEM_PACKAGES_LIST_PATH); + } - if (!manager_exist) { - if (ksu_is_manager_uid_valid()) { - pr_info("manager is uninstalled, invalidate it!\n"); - ksu_invalidate_manager_uid(); - goto prune; - } - pr_info("Searching manager...\n"); - search_manager("/data/app", 2, &uid_list); - pr_info("Search manager finished\n"); - } else if (!dynamic_manager_exist && ksu_is_dynamic_manager_enabled()) { - // Always perform search when called from dynamic manager rescan - pr_info("Dynamic sign enabled, Searching manager...\n"); - search_manager("/data/app", 2, &uid_list); - pr_info("Search Dynamic sign manager finished\n"); - } + { + fp = filp_open(SYSTEM_PACKAGES_LIST_PATH, O_RDONLY, 0); + if (IS_ERR(fp)) { + pr_err("%s: open " SYSTEM_PACKAGES_LIST_PATH " failed: %ld\n", __func__, PTR_ERR(fp)); + return; + } + for (;;) { + ssize_t count = + kernel_read(fp, &chr, sizeof(chr), &pos); + if (count != sizeof(chr)) + break; + if (chr != '\n') + continue; + + count = kernel_read(fp, buf, sizeof(buf), + &line_start); + struct uid_data *data = + kzalloc(sizeof(struct uid_data), GFP_ATOMIC); + if (!data) { + filp_close(fp, 0); + goto out; + } + + char *tmp = buf; + const char *delim = " "; + char *package = strsep(&tmp, delim); + char *uid = strsep(&tmp, delim); + if (!uid || !package) { + pr_err("update_uid: package or uid is NULL!\n"); + break; + } + + u32 res; + if (kstrtou32(uid, 10, &res)) { + pr_err("update_uid: uid parse err\n"); + break; + } + data->uid = res; + strncpy(data->package, package, KSU_MAX_PACKAGE_NAME); + list_add_tail(&data->list, &uid_list); + // reset line start + line_start = pos; + } + + filp_close(fp, 0); + } + +uid_ready: + if (prune_only) + goto prune; + + // first, check if manager_uid exist! + list_for_each_entry(np, &uid_list, list) { + if (np->uid == current_manager_uid) { + manager_exist = true; + break; + } + } + + if (!manager_exist && locked_manager_uid != KSU_INVALID_UID) { + pr_info("Manager APK removed, unlock previous UID: %d\n", + locked_manager_uid); + ksu_invalidate_manager_uid(); + locked_manager_uid = KSU_INVALID_UID; + } + + // Check if the Dynamic Manager exists (only check locked UIDs) + if (ksu_is_dynamic_manager_enabled() && + locked_dynamic_manager_uid != KSU_INVALID_UID) { + list_for_each_entry(np, &uid_list, list) { + if (np->uid == locked_dynamic_manager_uid) { + dynamic_manager_exist = true; + break; + } + } + + if (!dynamic_manager_exist) { + pr_info("Dynamic manager APK removed, unlock previous UID: %d\n", + locked_dynamic_manager_uid); + ksu_remove_manager(locked_dynamic_manager_uid); + locked_dynamic_manager_uid = KSU_INVALID_UID; + } + } + + bool need_search = !manager_exist; + if (ksu_is_dynamic_manager_enabled() && !dynamic_manager_exist) + need_search = true; + + if (need_search) { + pr_info("Searching for manager(s)...\n"); + search_manager("/data/app", 2, &uid_list); + pr_info("Manager search finished\n"); + } + prune: - // then prune the allowlist - ksu_prune_allowlist(is_uid_exist, &uid_list); + // then prune the allowlist + ksu_prune_allowlist(is_uid_exist, &uid_list); out: - // free uid_list - list_for_each_entry_safe (np, n, &uid_list, list) { - list_del(&np->list); - kfree(np); - } + // free uid_list + list_for_each_entry_safe(np, n, &uid_list, list) { + list_del(&np->list); + kfree(np); + } } -void ksu_throne_tracker_init() +void ksu_throne_tracker_init(void) { - // nothing to do + // nothing to do } -void ksu_throne_tracker_exit() +void ksu_throne_tracker_exit(void) { - // nothing to do -} + // nothing to do +} \ No newline at end of file diff --git a/kernel/throne_tracker.h b/kernel/throne_tracker.h index 5d7f477..6be7d5f 100644 --- a/kernel/throne_tracker.h +++ b/kernel/throne_tracker.h @@ -5,6 +5,6 @@ void ksu_throne_tracker_init(); void ksu_throne_tracker_exit(); -void track_throne(); +void track_throne(bool prune_only); #endif diff --git a/kernel/umount_manager.c b/kernel/umount_manager.c new file mode 100644 index 0000000..31e45b1 --- /dev/null +++ b/kernel/umount_manager.c @@ -0,0 +1,242 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "klog.h" +#include "kernel_umount.h" +#include "umount_manager.h" + +static struct umount_manager g_umount_mgr = { + .entry_count = 0, + .max_entries = 64, +}; + +static void try_umount_path(struct umount_entry *entry) +{ + try_umount(entry->path, entry->flags); +} + +static struct umount_entry *find_entry_locked(const char *path) +{ + struct umount_entry *entry; + + list_for_each_entry(entry, &g_umount_mgr.entry_list, list) { + if (strcmp(entry->path, path) == 0) { + return entry; + } + } + + return NULL; +} + +int ksu_umount_manager_init(void) +{ + INIT_LIST_HEAD(&g_umount_mgr.entry_list); + spin_lock_init(&g_umount_mgr.lock); + + return 0; +} + +void ksu_umount_manager_exit(void) +{ + struct umount_entry *entry, *tmp; + unsigned long flags; + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + list_for_each_entry_safe(entry, tmp, &g_umount_mgr.entry_list, list) { + list_del(&entry->list); + kfree(entry); + g_umount_mgr.entry_count--; + } + + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + + pr_info("Umount manager cleaned up\n"); +} + +int ksu_umount_manager_add(const char *path, int flags, bool is_default) +{ + struct umount_entry *entry; + unsigned long irqflags; + int ret = 0; + + if (flags == -1) + flags = MNT_DETACH; + + if (!path || strlen(path) == 0 || strlen(path) >= 256) { + return -EINVAL; + } + + spin_lock_irqsave(&g_umount_mgr.lock, irqflags); + + if (g_umount_mgr.entry_count >= g_umount_mgr.max_entries) { + pr_err("Umount manager: max entries reached\n"); + ret = -ENOMEM; + goto out; + } + + if (find_entry_locked(path)) { + pr_warn("Umount manager: path already exists: %s\n", path); + ret = -EEXIST; + goto out; + } + + entry = kzalloc(sizeof(*entry), GFP_ATOMIC); + if (!entry) { + ret = -ENOMEM; + goto out; + } + + strncpy(entry->path, path, sizeof(entry->path) - 1); + entry->flags = flags; + entry->state = UMOUNT_STATE_IDLE; + entry->is_default = is_default; + entry->ref_count = 0; + + list_add_tail(&entry->list, &g_umount_mgr.entry_list); + g_umount_mgr.entry_count++; + + pr_info("Umount manager: added %s entry: %s\n", + is_default ? "default" : "custom", path); + +out: + spin_unlock_irqrestore(&g_umount_mgr.lock, irqflags); + return ret; +} + +int ksu_umount_manager_remove(const char *path) +{ + struct umount_entry *entry; + unsigned long flags; + int ret = 0; + + if (!path) { + return -EINVAL; + } + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + entry = find_entry_locked(path); + if (!entry) { + ret = -ENOENT; + goto out; + } + + if (entry->is_default) { + pr_err("Umount manager: cannot remove default entry: %s\n", path); + ret = -EPERM; + goto out; + } + + if (entry->state == UMOUNT_STATE_BUSY || entry->ref_count > 0) { + pr_err("Umount manager: entry is busy: %s\n", path); + ret = -EBUSY; + goto out; + } + + list_del(&entry->list); + g_umount_mgr.entry_count--; + kfree(entry); + + pr_info("Umount manager: removed entry: %s\n", path); + +out: + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + return ret; +} + +void ksu_umount_manager_execute_all(const struct cred *cred) +{ + struct umount_entry *entry; + unsigned long flags; + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + list_for_each_entry(entry, &g_umount_mgr.entry_list, list) { + if (entry->state == UMOUNT_STATE_IDLE) { + entry->ref_count++; + } + } + + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + + list_for_each_entry(entry, &g_umount_mgr.entry_list, list) { + if (entry->ref_count > 0 && entry->state == UMOUNT_STATE_IDLE) { + try_umount_path(entry); + } + } + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + list_for_each_entry(entry, &g_umount_mgr.entry_list, list) { + if (entry->ref_count > 0) { + entry->ref_count--; + } + } + + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); +} + +int ksu_umount_manager_get_entries(struct ksu_umount_entry_info __user *entries, u32 *count) +{ + struct umount_entry *entry; + struct ksu_umount_entry_info info; + unsigned long flags; + u32 idx = 0; + u32 max_count = *count; + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + list_for_each_entry(entry, &g_umount_mgr.entry_list, list) { + if (idx >= max_count) { + break; + } + + memset(&info, 0, sizeof(info)); + strncpy(info.path, entry->path, sizeof(info.path) - 1); + info.flags = entry->flags; + info.is_default = entry->is_default; + info.state = entry->state; + info.ref_count = entry->ref_count; + + if (copy_to_user(&entries[idx], &info, sizeof(info))) { + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + return -EFAULT; + } + + idx++; + } + + *count = idx; + + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + return 0; +} + +int ksu_umount_manager_clear_custom(void) +{ + struct umount_entry *entry, *tmp; + unsigned long flags; + u32 cleared = 0; + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + list_for_each_entry_safe(entry, tmp, &g_umount_mgr.entry_list, list) { + if (!entry->is_default && entry->state == UMOUNT_STATE_IDLE && entry->ref_count == 0) { + list_del(&entry->list); + kfree(entry); + g_umount_mgr.entry_count--; + cleared++; + } + } + + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + + pr_info("Umount manager: cleared %u custom entries\n", cleared); + return 0; +} diff --git a/kernel/umount_manager.h b/kernel/umount_manager.h new file mode 100644 index 0000000..41c9888 --- /dev/null +++ b/kernel/umount_manager.h @@ -0,0 +1,63 @@ +#ifndef __KSU_H_UMOUNT_MANAGER +#define __KSU_H_UMOUNT_MANAGER + +#include +#include +#include + +struct cred; + +enum umount_entry_state { + UMOUNT_STATE_IDLE = 0, + UMOUNT_STATE_ACTIVE = 1, + UMOUNT_STATE_BUSY = 2, +}; + +struct umount_entry { + struct list_head list; + char path[256]; + int flags; + enum umount_entry_state state; + bool is_default; + u32 ref_count; +}; + +struct umount_manager { + struct list_head entry_list; + spinlock_t lock; + u32 entry_count; + u32 max_entries; +}; + +enum umount_manager_op { + UMOUNT_OP_ADD = 0, + UMOUNT_OP_REMOVE = 1, + UMOUNT_OP_LIST = 2, + UMOUNT_OP_CLEAR_CUSTOM = 3, +}; + +struct ksu_umount_manager_cmd { + __u32 operation; + char path[256]; + __s32 flags; + __u32 count; + __aligned_u64 entries_ptr; +}; + +struct ksu_umount_entry_info { + char path[256]; + __s32 flags; + __u8 is_default; + __u32 state; + __u32 ref_count; +}; + +int ksu_umount_manager_init(void); +void ksu_umount_manager_exit(void); +int ksu_umount_manager_add(const char *path, int flags, bool is_default); +int ksu_umount_manager_remove(const char *path); +void ksu_umount_manager_execute_all(const struct cred *cred); +int ksu_umount_manager_get_entries(struct ksu_umount_entry_info __user *entries, u32 *count); +int ksu_umount_manager_clear_custom(void); + +#endif // __KSU_H_UMOUNT_MANAGER diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index ee3b591..d51afa9 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -2,7 +2,6 @@ import com.android.build.gradle.internal.api.BaseVariantOutputImpl import com.android.build.gradle.tasks.PackageAndroidArtifact -import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.agp.app) @@ -17,6 +16,7 @@ plugins { val managerVersionCode: Int by rootProject.extra val managerVersionName: String by rootProject.extra +val androidCmakeVersion: String by rootProject.extra apksign { storeFileProperty = "KEYSTORE_FILE" @@ -51,15 +51,12 @@ android { } buildFeatures { + aidl = true buildConfig = true compose = true prefab = true } - kotlin { - jvmToolchain(21) - } - packaging { jniLibs { useLegacyPackaging = true @@ -77,7 +74,8 @@ android { externalNativeBuild { cmake { - path("src/main/cpp/CMakeLists.txt") + path = file("src/main/cpp/CMakeLists.txt") + version = androidCmakeVersion } } @@ -125,6 +123,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.foundation) implementation(libs.androidx.documentfile) + implementation(libs.androidx.compose.foundation) debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.tooling) diff --git a/manager/app/proguard-rules.pro b/manager/app/proguard-rules.pro index 8563805..18c49c1 100644 --- a/manager/app/proguard-rules.pro +++ b/manager/app/proguard-rules.pro @@ -43,4 +43,6 @@ -keep class com.dergoogler.mmrl.webui.interfaces.** { *; } -keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; } --keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; } \ No newline at end of file +-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; } + +-keep interface com.sukisu.zako.** { *; } \ No newline at end of file diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml index 62fd030..d4f5d53 100644 --- a/manager/app/src/main/AndroidManifest.xml +++ b/manager/app/src/main/AndroidManifest.xml @@ -18,31 +18,82 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" + android:requestLegacyExternalStorage="true" android:supportsRtl="true" android:theme="@style/Theme.KernelSU" - android:requestLegacyExternalStorage="true" tools:targetApi="34"> + + + + + + + + + + + + + + + + + + + + + + + - + + android:roundIcon="@mipmap/ic_launcher_alt_round"> + + + + + + + + + + + + + + + + + + + + getPackages(int start, int maxCount); +} \ No newline at end of file diff --git a/manager/app/src/main/assets/kpimg b/manager/app/src/main/assets/kpimg index 80125ec..e64eb85 100644 Binary files a/manager/app/src/main/assets/kpimg and b/manager/app/src/main/assets/kpimg differ diff --git a/manager/app/src/main/assets/kptools b/manager/app/src/main/assets/kptools index e7434c9..f1a2a57 100644 Binary files a/manager/app/src/main/assets/kptools and b/manager/app/src/main/assets/kptools differ diff --git a/manager/app/src/main/assets/ksu_susfs_1.5.7 b/manager/app/src/main/assets/ksu_susfs_1.5.7 deleted file mode 100644 index 2369a06..0000000 Binary files a/manager/app/src/main/assets/ksu_susfs_1.5.7 and /dev/null differ diff --git a/manager/app/src/main/assets/ksu_susfs_1.5.8 b/manager/app/src/main/assets/ksu_susfs_1.5.8 deleted file mode 100644 index a588f43..0000000 Binary files a/manager/app/src/main/assets/ksu_susfs_1.5.8 and /dev/null differ diff --git a/manager/app/src/main/assets/ksu_susfs_1.5.9 b/manager/app/src/main/assets/ksu_susfs_1.5.9 deleted file mode 100644 index d72c0a9..0000000 Binary files a/manager/app/src/main/assets/ksu_susfs_1.5.9 and /dev/null differ diff --git a/manager/app/src/main/assets/ksu_susfs_2.0.0 b/manager/app/src/main/assets/ksu_susfs_2.0.0 new file mode 100644 index 0000000..dbb0b6b Binary files /dev/null and b/manager/app/src/main/assets/ksu_susfs_2.0.0 differ diff --git a/manager/app/src/main/cpp/CMakeLists.txt b/manager/app/src/main/cpp/CMakeLists.txt index abeb419..7fc4fdc 100644 --- a/manager/app/src/main/cpp/CMakeLists.txt +++ b/manager/app/src/main/cpp/CMakeLists.txt @@ -6,10 +6,11 @@ cmake_minimum_required(VERSION 3.18.1) project("kernelsu") -add_library(zako +add_library(kernelsu SHARED jni.c ksu.c + legacy.c ) find_library(log-lib log) @@ -21,7 +22,7 @@ elseif(ANDROID_ABI STREQUAL "armeabi-v7a") endif() if(ANDROID_ABI STREQUAL "arm64-v8a" OR ANDROID_ABI STREQUAL "armeabi-v7a") - target_link_libraries(zako ${log-lib} ${zakosign-lib}) + target_link_libraries(kernelsu ${log-lib} ${zakosign-lib}) else() - target_link_libraries(zako ${log-lib}) + target_link_libraries(kernelsu ${log-lib}) endif() diff --git a/manager/app/src/main/cpp/jni.c b/manager/app/src/main/cpp/jni.c index 2862763..95818d6 100644 --- a/manager/app/src/main/cpp/jni.c +++ b/manager/app/src/main/cpp/jni.c @@ -5,430 +5,448 @@ #include #include #include - - -NativeBridge(becomeManager, jboolean, jstring pkg) { - const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, JNI_FALSE); - bool result = become_manager(cpkg); - - GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg); - return result; -} +#include +#include NativeBridgeNP(getVersion, jint) { - return get_version(); + uint32_t version = get_version(); + if (version > 0) { + return (jint)version; + } + // try legacy method as fallback + return legacy_get_info().version; } // get VERSION FULL NativeBridgeNP(getFullVersion, jstring) { - char buff[255] = { 0 }; - get_full_version((char *) &buff); - return GetEnvironment()->NewStringUTF(env, buff); + char buff[255] = { 0 }; + get_full_version((char *) &buff); + return GetEnvironment()->NewStringUTF(env, buff); } NativeBridgeNP(getAllowList, jintArray) { - int uids[1024]; - int size = 0; - bool result = get_allow_list(uids, &size); + struct ksu_get_allow_list_cmd cmd = {}; + bool result = get_allow_list(&cmd); - LogDebug("getAllowList: %d, size: %d", result, size); + if (result) { + jsize array_size = (jsize)cmd.count; + if (array_size < 0 || (unsigned int)array_size != cmd.count) { + LogDebug("Invalid array size: %u", cmd.count); + return GetEnvironment()->NewIntArray(env, 0); + } - if (result) { - jintArray array = GetEnvironment()->NewIntArray(env, size); - GetEnvironment()->SetIntArrayRegion(env, array, 0, size, uids); + jintArray array = GetEnvironment()->NewIntArray(env, array_size); + GetEnvironment()->SetIntArrayRegion(env, array, 0, array_size, (const jint *)(cmd.uids)); - return array; - } + return array; + } - return GetEnvironment()->NewIntArray(env, 0); + return GetEnvironment()->NewIntArray(env, 0); } NativeBridgeNP(isSafeMode, jboolean) { - return is_safe_mode(); + return is_safe_mode(); } NativeBridgeNP(isLkmMode, jboolean) { - return is_lkm_mode(); + return is_lkm_mode(); +} + +NativeBridgeNP(isManager, jboolean) { + return is_manager(); } static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) { - jclass cls = GetEnvironment()->GetObjectClass(env, list); - jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); - jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); - jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "", "(I)V"); - for (int i = 0; i < count; ++i) { - jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, data[i]); - GetEnvironment()->CallBooleanMethod(env, list, add, integer); - } + jclass cls = GetEnvironment()->GetObjectClass(env, list); + jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); + jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); + jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "", "(I)V"); + for (int i = 0; i < count; ++i) { + jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, data[i]); + GetEnvironment()->CallBooleanMethod(env, list, add, integer); + } } static void addIntToList(JNIEnv *env, jobject list, int ele) { - jclass cls = GetEnvironment()->GetObjectClass(env, list); - jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); - jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); - jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "", "(I)V"); - jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, ele); - GetEnvironment()->CallBooleanMethod(env, list, add, integer); + jclass cls = GetEnvironment()->GetObjectClass(env, list); + jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); + jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); + jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "", "(I)V"); + jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, ele); + GetEnvironment()->CallBooleanMethod(env, list, add, integer); } static uint64_t capListToBits(JNIEnv *env, jobject list) { - jclass cls = GetEnvironment()->GetObjectClass(env, list); - jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;"); - jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I"); - jint listSize = GetEnvironment()->CallIntMethod(env, list, size); - jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); - jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I"); - uint64_t result = 0; - for (int i = 0; i < listSize; ++i) { - jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i); - int data = GetEnvironment()->CallIntMethod(env, integer, intValue); + jclass cls = GetEnvironment()->GetObjectClass(env, list); + jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;"); + jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I"); + jint listSize = GetEnvironment()->CallIntMethod(env, list, size); + jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); + jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I"); + uint64_t result = 0; + for (int i = 0; i < listSize; ++i) { + jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i); + int data = GetEnvironment()->CallIntMethod(env, integer, intValue); - if (cap_valid(data)) { - result |= (1ULL << data); - } - } + if (cap_valid(data)) { + result |= (1ULL << data); + } + } - return result; + return result; } static int getListSize(JNIEnv *env, jobject list) { - jclass cls = GetEnvironment()->GetObjectClass(env, list); - jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I"); - return GetEnvironment()->CallIntMethod(env, list, size); + jclass cls = GetEnvironment()->GetObjectClass(env, list); + jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I"); + return GetEnvironment()->CallIntMethod(env, list, size); } static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) { - jclass cls = GetEnvironment()->GetObjectClass(env, list); - jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;"); - jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); - jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I"); - for (int i = 0; i < count; ++i) { - jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i); - data[i] = GetEnvironment()->CallIntMethod(env, integer, intValue); - } + jclass cls = GetEnvironment()->GetObjectClass(env, list); + jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;"); + jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); + jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I"); + for (int i = 0; i < count; ++i) { + jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i); + data[i] = GetEnvironment()->CallIntMethod(env, integer, intValue); + } } NativeBridge(getAppProfile, jobject, jstring pkg, jint uid) { - if (GetEnvironment()->GetStringLength(env, pkg) > KSU_MAX_PACKAGE_NAME) { - return NULL; - } + if (GetEnvironment()->GetStringLength(env, pkg) > KSU_MAX_PACKAGE_NAME) { + return NULL; + } - char key[KSU_MAX_PACKAGE_NAME] = { 0 }; - const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, nullptr); - strcpy(key, cpkg); - GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg); + char key[KSU_MAX_PACKAGE_NAME] = { 0 }; + const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, nullptr); + strcpy(key, cpkg); + GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg); - struct app_profile profile = { 0 }; - profile.version = KSU_APP_PROFILE_VER; + struct app_profile profile = { 0 }; + profile.version = KSU_APP_PROFILE_VER; - strcpy(profile.key, key); - profile.current_uid = uid; + strcpy(profile.key, key); + profile.current_uid = uid; - bool useDefaultProfile = !get_app_profile(key, &profile); + bool useDefaultProfile = get_app_profile(&profile) != 0; - jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile"); - jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); - jobject obj = GetEnvironment()->NewObject(env, cls, constructor); - jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;"); - jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I"); - jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z"); + jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile"); + jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); + jobject obj = GetEnvironment()->NewObject(env, cls, constructor); + jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;"); + jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I"); + jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z"); - jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z"); - jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;"); + jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z"); + jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;"); - jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I"); - jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I"); - jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;"); - jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;"); - jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;"); - jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I"); + jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I"); + jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I"); + jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;"); + jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;"); + jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;"); + jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I"); - jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z"); - jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z"); + jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z"); + jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z"); - GetEnvironment()->SetObjectField(env, obj, keyField, GetEnvironment()->NewStringUTF(env, profile.key)); - GetEnvironment()->SetIntField(env, obj, currentUidField, profile.current_uid); + GetEnvironment()->SetObjectField(env, obj, keyField, GetEnvironment()->NewStringUTF(env, profile.key)); + GetEnvironment()->SetIntField(env, obj, currentUidField, profile.current_uid); - if (useDefaultProfile) { - // no profile found, so just use default profile: - // don't allow root and use default profile! - LogDebug("use default profile for: %s, %d", key, uid); + if (useDefaultProfile) { + // no profile found, so just use default profile: + // don't allow root and use default profile! + LogDebug("use default profile for: %s, %d", key, uid); - // allow_su = false - // non root use default = true - GetEnvironment()->SetBooleanField(env, obj, allowSuField, false); - GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, true); + // allow_su = false + // non root use default = true + GetEnvironment()->SetBooleanField(env, obj, allowSuField, false); + GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, true); - return obj; - } + return obj; + } - bool allowSu = profile.allow_su; + bool allowSu = profile.allow_su; - if (allowSu) { - GetEnvironment()->SetBooleanField(env, obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default); - if (strlen(profile.rp_config.template_name) > 0) { - GetEnvironment()->SetObjectField(env, obj, rootTemplateField, - GetEnvironment()->NewStringUTF(env, profile.rp_config.template_name)); - } + if (allowSu) { + GetEnvironment()->SetBooleanField(env, obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default); + if (strlen(profile.rp_config.template_name) > 0) { + GetEnvironment()->SetObjectField(env, obj, rootTemplateField, + GetEnvironment()->NewStringUTF(env, profile.rp_config.template_name)); + } - GetEnvironment()->SetIntField(env, obj, uidField, profile.rp_config.profile.uid); - GetEnvironment()->SetIntField(env, obj, gidField, profile.rp_config.profile.gid); + GetEnvironment()->SetIntField(env, obj, uidField, profile.rp_config.profile.uid); + GetEnvironment()->SetIntField(env, obj, gidField, profile.rp_config.profile.gid); - jobject groupList = GetEnvironment()->GetObjectField(env, obj, groupsField); - int groupCount = profile.rp_config.profile.groups_count; - if (groupCount > KSU_MAX_GROUPS) { - LogDebug("kernel group count too large: %d???", groupCount); - groupCount = KSU_MAX_GROUPS; - } - fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount); + jobject groupList = GetEnvironment()->GetObjectField(env, obj, groupsField); + int groupCount = profile.rp_config.profile.groups_count; + if (groupCount > KSU_MAX_GROUPS) { + LogDebug("kernel group count too large: %d???", groupCount); + groupCount = KSU_MAX_GROUPS; + } + fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount); - jobject capList = GetEnvironment()->GetObjectField(env, obj, capabilitiesField); - for (int i = 0; i <= CAP_LAST_CAP; i++) { - if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) { - addIntToList(env, capList, i); - } - } + jobject capList = GetEnvironment()->GetObjectField(env, obj, capabilitiesField); + for (int i = 0; i <= CAP_LAST_CAP; i++) { + if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) { + addIntToList(env, capList, i); + } + } - GetEnvironment()->SetObjectField(env, obj, domainField, - GetEnvironment()->NewStringUTF(env, profile.rp_config.profile.selinux_domain)); - GetEnvironment()->SetIntField(env, obj, namespacesField, profile.rp_config.profile.namespaces); - GetEnvironment()->SetBooleanField(env, obj, allowSuField, profile.allow_su); - } else { - GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, profile.nrp_config.use_default); - GetEnvironment()->SetBooleanField(env, obj, umountModulesField, profile.nrp_config.profile.umount_modules); - } + GetEnvironment()->SetObjectField(env, obj, domainField, + GetEnvironment()->NewStringUTF(env, profile.rp_config.profile.selinux_domain)); + GetEnvironment()->SetIntField(env, obj, namespacesField, profile.rp_config.profile.namespaces); + GetEnvironment()->SetBooleanField(env, obj, allowSuField, profile.allow_su); + } else { + GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, profile.nrp_config.use_default); + GetEnvironment()->SetBooleanField(env, obj, umountModulesField, profile.nrp_config.profile.umount_modules); + } - return obj; + return obj; } NativeBridge(setAppProfile, jboolean, jobject profile) { - jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile"); + jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile"); - jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;"); - jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I"); - jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z"); + jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;"); + jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I"); + jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z"); - jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z"); - jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;"); + jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z"); + jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;"); - jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I"); - jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I"); - jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;"); - jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;"); - jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;"); - jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I"); + jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I"); + jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I"); + jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;"); + jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;"); + jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;"); + jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I"); - jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z"); - jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z"); + jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z"); + jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z"); - jobject key = GetEnvironment()->GetObjectField(env, profile, keyField); - if (!key) { - return false; - } - if (GetEnvironment()->GetStringLength(env, (jstring) key) > KSU_MAX_PACKAGE_NAME) { - return false; - } + jobject key = GetEnvironment()->GetObjectField(env, profile, keyField); + if (!key) { + return false; + } + if (GetEnvironment()->GetStringLength(env, (jstring) key) > KSU_MAX_PACKAGE_NAME) { + return false; + } - const char* cpkg = GetEnvironment()->GetStringUTFChars(env, (jstring) key, nullptr); - char p_key[KSU_MAX_PACKAGE_NAME] = { 0 }; - strcpy(p_key, cpkg); - GetEnvironment()->ReleaseStringUTFChars(env, (jstring) key, cpkg); + const char* cpkg = GetEnvironment()->GetStringUTFChars(env, (jstring) key, nullptr); + char p_key[KSU_MAX_PACKAGE_NAME] = { 0 }; + strcpy(p_key, cpkg); + GetEnvironment()->ReleaseStringUTFChars(env, (jstring) key, cpkg); - jint currentUid = GetEnvironment()->GetIntField(env, profile, currentUidField); + jint currentUid = GetEnvironment()->GetIntField(env, profile, currentUidField); - jint uid = GetEnvironment()->GetIntField(env, profile, uidField); - jint gid = GetEnvironment()->GetIntField(env, profile, gidField); - jobject groups = GetEnvironment()->GetObjectField(env, profile, groupsField); - jobject capabilities = GetEnvironment()->GetObjectField(env, profile, capabilitiesField); - jobject domain = GetEnvironment()->GetObjectField(env, profile, domainField); - jboolean allowSu = GetEnvironment()->GetBooleanField(env, profile, allowSuField); - jboolean umountModules = GetEnvironment()->GetBooleanField(env, profile, umountModulesField); + jint uid = GetEnvironment()->GetIntField(env, profile, uidField); + jint gid = GetEnvironment()->GetIntField(env, profile, gidField); + jobject groups = GetEnvironment()->GetObjectField(env, profile, groupsField); + jobject capabilities = GetEnvironment()->GetObjectField(env, profile, capabilitiesField); + jobject domain = GetEnvironment()->GetObjectField(env, profile, domainField); + jboolean allowSu = GetEnvironment()->GetBooleanField(env, profile, allowSuField); + jboolean umountModules = GetEnvironment()->GetBooleanField(env, profile, umountModulesField); - struct app_profile p = { 0 }; - p.version = KSU_APP_PROFILE_VER; + struct app_profile p = { 0 }; + p.version = KSU_APP_PROFILE_VER; - strcpy(p.key, p_key); - p.allow_su = allowSu; - p.current_uid = currentUid; + strcpy(p.key, p_key); + p.allow_su = allowSu; + p.current_uid = currentUid; - if (allowSu) { - p.rp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, rootUseDefaultField); - jobject templateName = GetEnvironment()->GetObjectField(env, profile, rootTemplateField); - if (templateName) { - const char* ctemplateName = GetEnvironment()->GetStringUTFChars(env, (jstring) templateName, nullptr); - strcpy(p.rp_config.template_name, ctemplateName); - GetEnvironment()->ReleaseStringUTFChars(env, (jstring) templateName, ctemplateName); - } + if (allowSu) { + p.rp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, rootUseDefaultField); + jobject templateName = GetEnvironment()->GetObjectField(env, profile, rootTemplateField); + if (templateName) { + const char* ctemplateName = GetEnvironment()->GetStringUTFChars(env, (jstring) templateName, nullptr); + strcpy(p.rp_config.template_name, ctemplateName); + GetEnvironment()->ReleaseStringUTFChars(env, (jstring) templateName, ctemplateName); + } - p.rp_config.profile.uid = uid; - p.rp_config.profile.gid = gid; + p.rp_config.profile.uid = uid; + p.rp_config.profile.gid = gid; - int groups_count = getListSize(env, groups); - if (groups_count > KSU_MAX_GROUPS) { - LogDebug("groups count too large: %d", groups_count); - return false; - } - p.rp_config.profile.groups_count = groups_count; - fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count); + int groups_count = getListSize(env, groups); + if (groups_count > KSU_MAX_GROUPS) { + LogDebug("groups count too large: %d", groups_count); + return false; + } + p.rp_config.profile.groups_count = groups_count; + fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count); - p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities); + p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities); - const char* cdomain = GetEnvironment()->GetStringUTFChars(env, (jstring) domain, nullptr); - strcpy(p.rp_config.profile.selinux_domain, cdomain); - GetEnvironment()->ReleaseStringUTFChars(env, (jstring) domain, cdomain); + const char* cdomain = GetEnvironment()->GetStringUTFChars(env, (jstring) domain, nullptr); + strcpy(p.rp_config.profile.selinux_domain, cdomain); + GetEnvironment()->ReleaseStringUTFChars(env, (jstring) domain, cdomain); - p.rp_config.profile.namespaces = GetEnvironment()->GetIntField(env, profile, namespacesField); - } else { - p.nrp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, nonRootUseDefaultField); - p.nrp_config.profile.umount_modules = umountModules; - } + p.rp_config.profile.namespaces = GetEnvironment()->GetIntField(env, profile, namespacesField); + } else { + p.nrp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, nonRootUseDefaultField); + p.nrp_config.profile.umount_modules = umountModules; + } - return set_app_profile(&p); + return set_app_profile(&p); } NativeBridge(uidShouldUmount, jboolean, jint uid) { - return uid_should_umount(uid); + return uid_should_umount(uid); } NativeBridgeNP(isSuEnabled, jboolean) { - return is_su_enabled(); + return is_su_enabled(); } NativeBridge(setSuEnabled, jboolean, jboolean enabled) { - return set_su_enabled(enabled); + return set_su_enabled(enabled); +} + +NativeBridgeNP(isKernelUmountEnabled, jboolean) { + return is_kernel_umount_enabled(); +} + +NativeBridge(setKernelUmountEnabled, jboolean, jboolean enabled) { + return set_kernel_umount_enabled(enabled); +} + +NativeBridgeNP(isEnhancedSecurityEnabled, jboolean) { + return is_enhanced_security_enabled(); +} + +NativeBridge(setEnhancedSecurityEnabled, jboolean, jboolean enabled) { + return set_enhanced_security_enabled(enabled); +} + +NativeBridgeNP(isSuLogEnabled, jboolean) { + return is_sulog_enabled(); +} + +NativeBridge(setSuLogEnabled, jboolean, jboolean enabled) { + return set_sulog_enabled(enabled); +} + +NativeBridge(getUserName, jstring, jint uid) { + struct passwd *pw = getpwuid((uid_t) uid); + if (pw && pw->pw_name && pw->pw_name[0] != '\0') { + return GetEnvironment()->NewStringUTF(env, pw->pw_name); + } + return NULL; } // Check if KPM is enabled NativeBridgeNP(isKPMEnabled, jboolean) { - return is_KPM_enable(); + return is_KPM_enable(); } // Get HOOK type NativeBridgeNP(getHookType, jstring) { - char hook_type[16]; - get_hook_type(hook_type, sizeof(hook_type)); - return GetEnvironment()->NewStringUTF(env, hook_type); -} - -// SuSFS Related Function Status -NativeBridgeNP(getSusfsFeatureStatus, jobject) { - struct susfs_feature_status status; - bool result = get_susfs_feature_status(&status); - - if (!result) { - return NULL; - } - - jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$SusfsFeatureStatus"); - jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); - jobject obj = GetEnvironment()->NewObject(env, cls, constructor); - - SET_BOOLEAN_FIELD(obj, cls, statusSusPath, status.status_sus_path); - SET_BOOLEAN_FIELD(obj, cls, statusSusMount, status.status_sus_mount); - SET_BOOLEAN_FIELD(obj, cls, statusAutoDefaultMount, status.status_auto_default_mount); - SET_BOOLEAN_FIELD(obj, cls, statusAutoBindMount, status.status_auto_bind_mount); - SET_BOOLEAN_FIELD(obj, cls, statusSusKstat, status.status_sus_kstat); - SET_BOOLEAN_FIELD(obj, cls, statusTryUmount, status.status_try_umount); - SET_BOOLEAN_FIELD(obj, cls, statusAutoTryUmountBind, status.status_auto_try_umount_bind); - SET_BOOLEAN_FIELD(obj, cls, statusSpoofUname, status.status_spoof_uname); - SET_BOOLEAN_FIELD(obj, cls, statusEnableLog, status.status_enable_log); - SET_BOOLEAN_FIELD(obj, cls, statusHideSymbols, status.status_hide_symbols); - SET_BOOLEAN_FIELD(obj, cls, statusSpoofCmdline, status.status_spoof_cmdline); - SET_BOOLEAN_FIELD(obj, cls, statusOpenRedirect, status.status_open_redirect); - SET_BOOLEAN_FIELD(obj, cls, statusMagicMount, status.status_magic_mount); - SET_BOOLEAN_FIELD(obj, cls, statusSusSu, status.status_sus_su); - - return obj; + char hook_type[32] = { 0 }; + get_hook_type((char *) &hook_type); + return GetEnvironment()->NewStringUTF(env, hook_type); } // dynamic manager NativeBridge(setDynamicManager, jboolean, jint size, jstring hash) { - if (!hash) { - LogDebug("setDynamicManager: hash is null"); - return false; - } + if (!hash) { + LogDebug("setDynamicManager: hash is null"); + return false; + } - const char* chash = GetEnvironment()->GetStringUTFChars(env, hash, nullptr); - bool result = set_dynamic_manager((unsigned int)size, chash); - GetEnvironment()->ReleaseStringUTFChars(env, hash, chash); + const char* chash = GetEnvironment()->GetStringUTFChars(env, hash, nullptr); + bool result = set_dynamic_manager((unsigned int)size, chash); + GetEnvironment()->ReleaseStringUTFChars(env, hash, chash); - LogDebug("setDynamicManager: size=0x%x, result=%d", size, result); - return result; + LogDebug("setDynamicManager: size=0x%x, result=%d", size, result); + return result; } NativeBridgeNP(getDynamicManager, jobject) { - struct dynamic_manager_user_config config; - bool result = get_dynamic_manager(&config); + struct dynamic_manager_user_config config; + bool result = get_dynamic_manager(&config); - if (!result) { - LogDebug("getDynamicManager: failed to get dynamic manager config"); - return NULL; - } + if (!result) { + LogDebug("getDynamicManager: failed to get dynamic manager config"); + return NULL; + } - jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$DynamicManagerConfig"); - jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$DynamicManagerConfig"); + jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$DynamicManagerConfig"); + jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$DynamicManagerConfig"); - SET_INT_FIELD(obj, cls, size, (jint)config.size); - SET_STRING_FIELD(obj, cls, hash, config.hash); + SET_INT_FIELD(obj, cls, size, (jint)config.size); + SET_STRING_FIELD(obj, cls, hash, config.hash); - LogDebug("getDynamicManager: size=0x%x, hash=%.16s...", config.size, config.hash); - return obj; + LogDebug("getDynamicManager: size=0x%x, hash=%.16s...", config.size, config.hash); + return obj; } NativeBridgeNP(clearDynamicManager, jboolean) { - bool result = clear_dynamic_manager(); - LogDebug("clearDynamicManager: result=%d", result); - return result; + bool result = clear_dynamic_manager(); + LogDebug("clearDynamicManager: result=%d", result); + return result; } // Get a list of active managers NativeBridgeNP(getManagersList, jobject) { - struct manager_list_info managerListInfo; - bool result = get_managers_list(&managerListInfo); + struct manager_list_info managerListInfo; + bool result = get_managers_list(&managerListInfo); - if (!result) { - LogDebug("getManagersList: failed to get active managers list"); - return NULL; - } + if (!result) { + LogDebug("getManagersList: failed to get active managers list"); + return NULL; + } - jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$ManagersList"); - jclass managerListCls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$ManagersList"); + jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$ManagersList"); + jclass managerListCls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$ManagersList"); - SET_INT_FIELD(obj, managerListCls, count, (jint)managerListInfo.count); + SET_INT_FIELD(obj, managerListCls, count, (jint)managerListInfo.count); - jobject managersList = CREATE_ARRAYLIST(); + jobject managersList = CREATE_ARRAYLIST(); - for (int i = 0; i < managerListInfo.count; i++) { - jobject managerInfo = CREATE_JAVA_OBJECT_WITH_PARAMS( - "com/sukisu/ultra/Natives$ManagerInfo", - "(II)V", - (jint)managerListInfo.managers[i].uid, - (jint)managerListInfo.managers[i].signature_index - ); - ADD_TO_LIST(managersList, managerInfo); - } + for (int i = 0; i < managerListInfo.count; i++) { + jobject managerInfo = CREATE_JAVA_OBJECT_WITH_PARAMS( + "com/sukisu/ultra/Natives$ManagerInfo", + "(II)V", + (jint)managerListInfo.managers[i].uid, + (jint)managerListInfo.managers[i].signature_index + ); + ADD_TO_LIST(managersList, managerInfo); + } - SET_OBJECT_FIELD(obj, managerListCls, managers, managersList); + SET_OBJECT_FIELD(obj, managerListCls, managers, managersList); - LogDebug("getManagersList: count=%d", managerListInfo.count); - return obj; + LogDebug("getManagersList: count=%d", managerListInfo.count); + return obj; } NativeBridge(verifyModuleSignature, jboolean, jstring modulePath) { #if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) - if (!modulePath) { - LogDebug("verifyModuleSignature: modulePath is null"); - return false; - } + if (!modulePath) { + LogDebug("verifyModuleSignature: modulePath is null"); + return false; + } - const char* cModulePath = GetEnvironment()->GetStringUTFChars(env, modulePath, nullptr); - bool result = verify_module_signature(cModulePath); - GetEnvironment()->ReleaseStringUTFChars(env, modulePath, cModulePath); + const char* cModulePath = GetEnvironment()->GetStringUTFChars(env, modulePath, nullptr); + bool result = verify_module_signature(cModulePath); + GetEnvironment()->ReleaseStringUTFChars(env, modulePath, cModulePath); - LogDebug("verifyModuleSignature: path=%s, result=%d", cModulePath, result); - return result; + LogDebug("verifyModuleSignature: path=%s, result=%d", cModulePath, result); + return result; #else - LogDebug("verifyModuleSignature: not supported on non-ARM architecture"); - return false; + LogDebug("verifyModuleSignature: not supported on non-ARM architecture"); + return false; #endif +} + +NativeBridgeNP(isUidScannerEnabled, jboolean) { + return is_uid_scanner_enabled(); +} + +NativeBridge(setUidScannerEnabled, jboolean, jboolean enabled) { + return set_uid_scanner_enabled(enabled); +} + +NativeBridgeNP(clearUidScannerEnvironment, jboolean) { + return clear_uid_scanner_environment(); } \ No newline at end of file diff --git a/manager/app/src/main/cpp/ksu.c b/manager/app/src/main/cpp/ksu.c index 7c5a047..0fc8866 100644 --- a/manager/app/src/main/cpp/ksu.c +++ b/manager/app/src/main/cpp/ksu.c @@ -2,11 +2,14 @@ // Created by weishu on 2022/12/9. // -#include #include #include #include #include +#include +#include +#include +#include #include "prelude.h" #include "ksu.h" @@ -21,232 +24,383 @@ extern const char* zako_file_verrcidx2str(uint8_t index); #endif // __aarch64__ || _M_ARM64 || __arm__ || _M_ARM -#define KERNEL_SU_OPTION 0xDEADBEEF +static int fd = -1; -#define CMD_GRANT_ROOT 0 +static inline int scan_driver_fd() { + const char *kName = "[ksu_driver]"; + DIR *fd_dir = opendir("/proc/self/fd"); + if (!fd_dir) { + return -1; + } -#define CMD_BECOME_MANAGER 1 -#define CMD_GET_VERSION 2 -#define CMD_ALLOW_SU 3 -#define CMD_DENY_SU 4 -#define CMD_GET_SU_LIST 5 -#define CMD_GET_DENY_LIST 6 -#define CMD_CHECK_SAFEMODE 9 + int found = -1; + struct dirent *de; + char path[64]; + char target[PATH_MAX]; -#define CMD_GET_APP_PROFILE 10 -#define CMD_SET_APP_PROFILE 11 + while ((de = readdir(fd_dir)) != NULL) { + if (de->d_name[0] == '.') { + continue; + } -#define CMD_IS_UID_GRANTED_ROOT 12 -#define CMD_IS_UID_SHOULD_UMOUNT 13 -#define CMD_IS_SU_ENABLED 14 -#define CMD_ENABLE_SU 15 + char *endptr = nullptr; + long fd_long = strtol(de->d_name, &endptr, 10); + if (!de->d_name[0] || *endptr != '\0' || fd_long < 0 || fd_long > INT_MAX) { + continue; + } -#define CMD_GET_VERSION_FULL 0xC0FFEE1A + snprintf(path, sizeof(path), "/proc/self/fd/%s", de->d_name); + ssize_t n = readlink(path, target, sizeof(target) - 1); + if (n < 0) { + continue; + } + target[n] = '\0'; -#define CMD_ENABLE_KPM 100 -#define CMD_HOOK_TYPE 101 -#define CMD_GET_SUSFS_FEATURE_STATUS 102 -#define CMD_DYNAMIC_MANAGER 103 -#define CMD_GET_MANAGERS 104 + const char *base = strrchr(target, '/'); + base = base ? base + 1 : target; -#define DYNAMIC_MANAGER_OP_SET 0 -#define DYNAMIC_MANAGER_OP_GET 1 -#define DYNAMIC_MANAGER_OP_CLEAR 2 + if (strstr(base, kName)) { + found = (int)fd_long; + break; + } + } -static bool ksuctl(int cmd, void* arg1, void* arg2) { - int32_t result = 0; - int32_t rtn = prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result); - - return result == KERNEL_SU_OPTION && rtn == -1; + closedir(fd_dir); + return found; } -bool become_manager(const char* pkg) { - char param[128]; - uid_t uid = getuid(); - uint32_t userId = uid / 100000; - if (userId == 0) { - sprintf(param, "/data/data/%s", pkg); - } else { - snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg); +static int ksuctl(unsigned long op, void* arg) { + if (fd < 0) { + fd = scan_driver_fd(); + } + return ioctl(fd, op, arg); +} + +static struct ksu_get_info_cmd g_version = {0}; + +struct ksu_get_info_cmd get_info() { + if (!g_version.version) { + ksuctl(KSU_IOCTL_GET_INFO, &g_version); + } + return g_version; +} + +uint32_t get_version() { + auto info = get_info(); + return info.version; +} + +bool get_allow_list(struct ksu_get_allow_list_cmd *cmd) { + if (ksuctl(KSU_IOCTL_GET_ALLOW_LIST, cmd) == 0) { + return true; } - return ksuctl(CMD_BECOME_MANAGER, param, NULL); -} - -// cache the result to avoid unnecessary syscall -static bool is_lkm; -int get_version() { - int32_t version = -1; - int32_t flags = 0; - ksuctl(CMD_GET_VERSION, &version, &flags); - if (!is_lkm && (flags & 0x1)) { - is_lkm = true; + // fallback to legacy + int size = 0; + int uids[1024]; + if (legacy_get_allow_list(uids, &size)) { + cmd->count = size; + memcpy(cmd->uids, uids, sizeof(int) * size); + return true; } - return version; -} -void get_full_version(char* buff) { - ksuctl(CMD_GET_VERSION_FULL, buff, NULL); -} - -bool get_allow_list(int *uids, int *size) { - return ksuctl(CMD_GET_SU_LIST, uids, size); + return false; } bool is_safe_mode() { - return ksuctl(CMD_CHECK_SAFEMODE, NULL, NULL); + struct ksu_check_safemode_cmd cmd = {}; + if (ksuctl(KSU_IOCTL_CHECK_SAFEMODE, &cmd) == 0) { + return cmd.in_safe_mode; + } + // fallback + return legacy_is_safe_mode(); } bool is_lkm_mode() { - // you should call get_version first! - return is_lkm; + auto info = get_info(); + if (info.version > 0) { + return (info.flags & 0x1) != 0; + } + // Legacy Compatible + return (legacy_get_info().flags & 0x1) != 0; +} + +bool is_manager() { + auto info = get_info(); + if (info.version > 0) { + return (info.flags & 0x2) != 0; + } + // Legacy Compatible + return legacy_get_info().version > 0; } bool uid_should_umount(int uid) { - int should; - return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, (void*) ((size_t) uid), &should) && should; + struct ksu_uid_should_umount_cmd cmd = {}; + cmd.uid = uid; + if (ksuctl(KSU_IOCTL_UID_SHOULD_UMOUNT, &cmd) == 0) { + return cmd.should_umount; + } + return legacy_uid_should_umount(uid); } -bool set_app_profile(const struct app_profile* profile) { - return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, NULL); +bool set_app_profile(const struct app_profile *profile) { + struct ksu_set_app_profile_cmd cmd = {}; + cmd.profile = *profile; + if (ksuctl(KSU_IOCTL_SET_APP_PROFILE, &cmd) == 0) { + return true; + } + return legacy_set_app_profile(profile); } -bool get_app_profile(char* key, struct app_profile* profile) { - return ksuctl(CMD_GET_APP_PROFILE, profile, NULL); +int get_app_profile(struct app_profile *profile) { + struct ksu_get_app_profile_cmd cmd = {.profile = *profile}; + int ret = ksuctl(KSU_IOCTL_GET_APP_PROFILE, &cmd); + if (ret == 0) { + *profile = cmd.profile; + return 0; + } + return legacy_get_app_profile(profile->key, profile) ? 0 : -1; } bool set_su_enabled(bool enabled) { - return ksuctl(CMD_ENABLE_SU, (void*) enabled, NULL); + struct ksu_set_feature_cmd cmd = {}; + cmd.feature_id = KSU_FEATURE_SU_COMPAT; + cmd.value = enabled ? 1 : 0; + if (ksuctl(KSU_IOCTL_SET_FEATURE, &cmd) == 0) { + return true; + } + return legacy_set_su_enabled(enabled); } bool is_su_enabled() { - int enabled = true; - // if ksuctl failed, we assume su is enabled, and it cannot be disabled. - ksuctl(CMD_IS_SU_ENABLED, &enabled, NULL); - return enabled; + struct ksu_get_feature_cmd cmd = {}; + cmd.feature_id = KSU_FEATURE_SU_COMPAT; + if (ksuctl(KSU_IOCTL_GET_FEATURE, &cmd) == 0 && cmd.supported) { + return cmd.value != 0; + } + return legacy_is_su_enabled(); } -bool is_KPM_enable() { - int enabled = false; - ksuctl(CMD_ENABLE_KPM, &enabled, NULL); - return enabled; -} - -bool get_hook_type(char* hook_type, size_t size) { - if (hook_type == NULL || size == 0) { +static inline bool get_feature(uint32_t feature_id, uint64_t *out_value, bool *out_supported) { + struct ksu_get_feature_cmd cmd = {}; + cmd.feature_id = feature_id; + if (ksuctl(KSU_IOCTL_GET_FEATURE, &cmd) != 0) { return false; } - - static char cached_hook_type[16] = {0}; - if (cached_hook_type[0] == '\0') { - if (!ksuctl(CMD_HOOK_TYPE, cached_hook_type, NULL)) { - strcpy(cached_hook_type, "Unknown"); - } - } - - strncpy(hook_type, cached_hook_type, size); - hook_type[size - 1] = '\0'; + if (out_value) *out_value = cmd.value; + if (out_supported) *out_supported = cmd.supported; return true; } -bool get_susfs_feature_status(struct susfs_feature_status* status) { - if (status == NULL) { - return false; - } - - return ksuctl(CMD_GET_SUSFS_FEATURE_STATUS, status, NULL); +static inline bool set_feature(uint32_t feature_id, uint64_t value) { + struct ksu_set_feature_cmd cmd = {}; + cmd.feature_id = feature_id; + cmd.value = value; + return ksuctl(KSU_IOCTL_SET_FEATURE, &cmd) == 0; } -bool set_dynamic_manager(unsigned int size, const char* hash) { - if (hash == NULL) { - return false; - } - - struct dynamic_manager_user_config config; - config.operation = DYNAMIC_MANAGER_OP_SET; - config.size = size; - strncpy(config.hash, hash, sizeof(config.hash) - 1); - config.hash[sizeof(config.hash) - 1] = '\0'; - - return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL); +bool set_kernel_umount_enabled(bool enabled) { + return set_feature(KSU_FEATURE_KERNEL_UMOUNT, enabled ? 1 : 0); } -bool get_dynamic_manager(struct dynamic_manager_user_config* config) { - if (config == NULL) { +bool is_kernel_umount_enabled() { + uint64_t value = 0; + bool supported = false; + if (!get_feature(KSU_FEATURE_KERNEL_UMOUNT, &value, &supported)) { return false; } - - config->operation = DYNAMIC_MANAGER_OP_GET; - return ksuctl(CMD_DYNAMIC_MANAGER, config, NULL); -} - -bool clear_dynamic_manager() { - struct dynamic_manager_user_config config; - config.operation = DYNAMIC_MANAGER_OP_CLEAR; - return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL); -} - -bool get_managers_list(struct manager_list_info* info) { - if (info == NULL) { + if (!supported) { return false; } + return value != 0; +} - return ksuctl(CMD_GET_MANAGERS, info, NULL); +bool set_enhanced_security_enabled(bool enabled) { + return set_feature(KSU_FEATURE_ENHANCED_SECURITY, enabled ? 1 : 0); +} + +bool is_enhanced_security_enabled() { + uint64_t value = 0; + bool supported = false; + if (!get_feature(KSU_FEATURE_ENHANCED_SECURITY, &value, &supported)) { + return false; + } + if (!supported) { + return false; + } + return value != 0; +} + +bool set_sulog_enabled(bool enabled) { + return set_feature(KSU_FEATURE_SULOG, enabled ? 1 : 0); +} + +bool is_sulog_enabled() { + uint64_t value = 0; + bool supported = false; + if (!get_feature(KSU_FEATURE_SULOG, &value, &supported)) { + return false; + } + if (!supported) { + return false; + } + return value != 0; +} + +void get_full_version(char* buff) { + struct ksu_get_full_version_cmd cmd = {0}; + if (ksuctl(KSU_IOCTL_GET_FULL_VERSION, &cmd) == 0) { + strncpy(buff, cmd.version_full, KSU_FULL_VERSION_STRING - 1); + buff[KSU_FULL_VERSION_STRING - 1] = '\0'; + } else { + return legacy_get_full_version(buff); + } +} + +bool is_KPM_enable(void) { + struct ksu_enable_kpm_cmd cmd = {}; + if (ksuctl(KSU_IOCTL_ENABLE_KPM, &cmd) == 0 && cmd.enabled) { + return true; + } + return legacy_is_KPM_enable(); +} + +void get_hook_type(char *buff) { + struct ksu_hook_type_cmd cmd = {0}; + if (ksuctl(KSU_IOCTL_HOOK_TYPE, &cmd) == 0) { + strncpy(buff, cmd.hook_type, 32 - 1); + buff[32 - 1] = '\0'; + } else { + legacy_get_hook_type(buff, 32); + } +} + +bool set_dynamic_manager(unsigned int size, const char *hash) +{ + struct ksu_dynamic_manager_cmd cmd = {0}; + cmd.config.operation = DYNAMIC_MANAGER_OP_SET; + cmd.config.size = size; + strlcpy(cmd.config.hash, hash, sizeof(cmd.config.hash)); + + return ksuctl(KSU_IOCTL_DYNAMIC_MANAGER, &cmd) == 0; +} + +bool get_dynamic_manager(struct dynamic_manager_user_config *cfg) +{ + if (!cfg) + return false; + + struct ksu_dynamic_manager_cmd cmd = {0}; + cmd.config.operation = DYNAMIC_MANAGER_OP_GET; + + if (ksuctl(KSU_IOCTL_DYNAMIC_MANAGER, &cmd) != 0) + return false; + + *cfg = cmd.config; + return true; +} + +bool clear_dynamic_manager(void) +{ + struct ksu_dynamic_manager_cmd cmd = {0}; + cmd.config.operation = DYNAMIC_MANAGER_OP_CLEAR; + return ksuctl(KSU_IOCTL_DYNAMIC_MANAGER, &cmd) == 0; +} + +bool get_managers_list(struct manager_list_info *info) +{ + if (!info) + return false; + struct ksu_get_managers_cmd cmd = {0}; + if (ksuctl(KSU_IOCTL_GET_MANAGERS, &cmd) != 0) + return false; + + *info = cmd.manager_info; + return true; +} + +bool is_uid_scanner_enabled(void) +{ + bool status = false; + + struct ksu_enable_uid_scanner_cmd cmd = { + .operation = UID_SCANNER_OP_GET_STATUS, + .status_ptr = (__u64)(uintptr_t)&status + }; + + return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd) == 0 != 0 && status; +} + +bool set_uid_scanner_enabled(bool enabled) +{ + struct ksu_enable_uid_scanner_cmd cmd = { + .operation = UID_SCANNER_OP_TOGGLE, + .enabled = enabled + }; + return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd); +} + +bool clear_uid_scanner_environment(void) +{ + struct ksu_enable_uid_scanner_cmd cmd = { + .operation = UID_SCANNER_OP_CLEAR_ENV + }; + return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd); } bool verify_module_signature(const char* input) { #if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) - if (input == NULL) { - LogDebug("verify_module_signature: input path is null"); - return false; - } + if (input == NULL) { + LogDebug("verify_module_signature: input path is null"); + return false; + } - int fd = zako_sys_file_open(input); - if (fd < 0) { - LogDebug("verify_module_signature: failed to open file: %s", input); - return false; - } + int file_fd = zako_sys_file_open(input); + if (file_fd < 0) { + LogDebug("verify_module_signature: failed to open file: %s", input); + return false; + } - uint32_t results = zako_file_verify_esig(fd, 0); + uint32_t results = zako_file_verify_esig(file_fd, 0); - if (results != 0) { - /* If important error occured, verification process should - be considered as failed due to unexpected modification - potentially happened. */ - if ((results & ZAKO_ESV_IMPORTANT_ERROR) != 0) { - LogDebug("verify_module_signature: Verification failed! (important error)"); - } else { - /* This is for manager that doesn't want to do certificate checks */ - LogDebug("verify_module_signature: Verification partially passed"); - } - } else { - LogDebug("verify_module_signature: Verification passed!"); - goto exit; - } + if (results != 0) { + /* If important error occured, verification process should + be considered as failed due to unexpected modification + potentially happened. */ + if ((results & ZAKO_ESV_IMPORTANT_ERROR) != 0) { + LogDebug("verify_module_signature: Verification failed! (important error)"); + } else { + /* This is for manager that doesn't want to do certificate checks */ + LogDebug("verify_module_signature: Verification partially passed"); + } + } else { + LogDebug("verify_module_signature: Verification passed!"); + goto exit; + } - /* Go through all bit fields */ - for (size_t i = 0; i < sizeof(uint32_t) * 8; i++) { - if ((results & (1 << i)) == 0) { - continue; - } + /* Go through all bit fields */ + for (size_t i = 0; i < sizeof(uint32_t) * 8; i++) { + if ((results & (1 << i)) == 0) { + continue; + } - /* Convert error bit field index into human readable string */ - const char* message = zako_file_verrcidx2str((uint8_t)i); - // Error message: message - if (message != NULL) { - LogDebug("verify_module_signature: Error bit %zu: %s", i, message); - } else { - LogDebug("verify_module_signature: Error bit %zu: Unknown error", i); - } - } + /* Convert error bit field index into human readable string */ + const char* message = zako_file_verrcidx2str((uint8_t)i); + // Error message: message + if (message != NULL) { + LogDebug("verify_module_signature: Error bit %zu: %s", i, message); + } else { + LogDebug("verify_module_signature: Error bit %zu: Unknown error", i); + } + } - exit: - close(fd); - LogDebug("verify_module_signature: path=%s, results=0x%x, success=%s", - input, results, (results == 0) ? "true" : "false"); - return results == 0; + exit: + close(file_fd); + LogDebug("verify_module_signature: path=%s, results=0x%x, success=%s", + input, results, (results == 0) ? "true" : "false"); + return results == 0; #else - LogDebug("verify_module_signature: not supported on non-ARM architecture, path=%s", input ? input : "null"); - return false; + LogDebug("verify_module_signature: not supported on non-ARM architecture, path=%s", input ? input : "null"); + return false; #endif -} \ No newline at end of file +} diff --git a/manager/app/src/main/cpp/ksu.h b/manager/app/src/main/cpp/ksu.h index 8df709d..efcaa05 100644 --- a/manager/app/src/main/cpp/ksu.h +++ b/manager/app/src/main/cpp/ksu.h @@ -6,16 +6,15 @@ #define KERNELSU_KSU_H #include "prelude.h" -#include +#include #include +#include +#include +#include -bool become_manager(const char *); +#define KSU_FULL_VERSION_STRING 255 -void get_full_version(char* buff); - -int get_version(); - -bool get_allow_list(int *uids, int *size); +uint32_t get_version(); bool uid_should_umount(int uid); @@ -23,6 +22,10 @@ bool is_safe_mode(); bool is_lkm_mode(); +bool is_manager(); + +void get_full_version(char* buff); + #define KSU_APP_PROFILE_VER 2 #define KSU_MAX_PACKAGE_NAME 256 // NGROUPS_MAX for Linux is 65535 generally, but we only supports 32 groups. @@ -33,99 +36,80 @@ bool is_lkm_mode(); #define DYNAMIC_MANAGER_OP_GET 1 #define DYNAMIC_MANAGER_OP_CLEAR 2 +#define UID_SCANNER_OP_GET_STATUS 0 +#define UID_SCANNER_OP_TOGGLE 1 +#define UID_SCANNER_OP_CLEAR_ENV 2 + struct dynamic_manager_user_config { - unsigned int operation; - unsigned int size; - char hash[65]; + unsigned int operation; + unsigned int size; + char hash[65]; }; -// SUSFS Functional State Structures -struct susfs_feature_status { - bool status_sus_path; - bool status_sus_mount; - bool status_auto_default_mount; - bool status_auto_bind_mount; - bool status_sus_kstat; - bool status_try_umount; - bool status_auto_try_umount_bind; - bool status_spoof_uname; - bool status_enable_log; - bool status_hide_symbols; - bool status_spoof_cmdline; - bool status_open_redirect; - bool status_magic_mount; - bool status_sus_su; -}; struct root_profile { - int32_t uid; - int32_t gid; + int32_t uid; + int32_t gid; - int32_t groups_count; - int32_t groups[KSU_MAX_GROUPS]; + int32_t groups_count; + int32_t groups[KSU_MAX_GROUPS]; - // kernel_cap_t is u32[2] for capabilities v3 - struct { - uint64_t effective; - uint64_t permitted; - uint64_t inheritable; - } capabilities; + // kernel_cap_t is u32[2] for capabilities v3 + struct { + uint64_t effective; + uint64_t permitted; + uint64_t inheritable; + } capabilities; - char selinux_domain[KSU_SELINUX_DOMAIN]; + char selinux_domain[KSU_SELINUX_DOMAIN]; - int32_t namespaces; + int32_t namespaces; }; struct non_root_profile { - bool umount_modules; + bool umount_modules; }; struct app_profile { - // It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this. - uint32_t version; + // It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this. + uint32_t version; - // this is usually the package of the app, but can be other value for special apps - char key[KSU_MAX_PACKAGE_NAME]; - int32_t current_uid; - bool allow_su; + // this is usually the package of the app, but can be other value for special apps + char key[KSU_MAX_PACKAGE_NAME]; + int32_t current_uid; + bool allow_su; - union { - struct { - bool use_default; - char template_name[KSU_MAX_PACKAGE_NAME]; + union { + struct { + bool use_default; + char template_name[KSU_MAX_PACKAGE_NAME]; - struct root_profile profile; - } rp_config; + struct root_profile profile; + } rp_config; - struct { - bool use_default; + struct { + bool use_default; - struct non_root_profile profile; - } nrp_config; - }; + struct non_root_profile profile; + } nrp_config; + }; }; struct manager_list_info { - int count; - struct { - uid_t uid; - int signature_index; - } managers[2]; + int count; + struct { + uid_t uid; + int signature_index; + } managers[2]; }; bool set_app_profile(const struct app_profile* profile); -bool get_app_profile(char* key, struct app_profile* profile); - -bool set_su_enabled(bool enabled); - -bool is_su_enabled(); +int get_app_profile(struct app_profile* profile); bool is_KPM_enable(); -bool get_hook_type(char* hook_type, size_t size); - -bool get_susfs_feature_status(struct susfs_feature_status* status); +void get_hook_type(char* hook_type); bool set_dynamic_manager(unsigned int size, const char* hash); @@ -137,4 +121,180 @@ bool get_managers_list(struct manager_list_info* info); bool verify_module_signature(const char* input); +bool is_uid_scanner_enabled(); + +bool set_uid_scanner_enabled(bool enabled); + +bool clear_uid_scanner_environment(); + +// Feature IDs +enum ksu_feature_id { + KSU_FEATURE_SU_COMPAT = 0, + KSU_FEATURE_KERNEL_UMOUNT = 1, + KSU_FEATURE_ENHANCED_SECURITY = 2, + KSU_FEATURE_SULOG = 3, +}; + +// Generic feature API +struct ksu_get_feature_cmd { + uint32_t feature_id; // Input: feature ID + uint64_t value; // Output: feature value/state + uint8_t supported; // Output: whether the feature is supported +}; + +struct ksu_set_feature_cmd { + uint32_t feature_id; // Input: feature ID + uint64_t value; // Input: feature value/state to set +}; + +struct ksu_become_daemon_cmd { + uint8_t token[65]; // Input: daemon token (null-terminated) +}; + +struct ksu_get_info_cmd { + uint32_t version; // Output: KERNEL_SU_VERSION + uint32_t flags; // Output: flags (bit 0: MODULE mode) + uint32_t features; // Output: max feature ID supported (KSU_FEATURE_MAX) +}; + +struct ksu_report_event_cmd { + uint32_t event; // Input: EVENT_POST_FS_DATA, EVENT_BOOT_COMPLETED, etc. +}; + +struct ksu_set_sepolicy_cmd { + uint64_t cmd; // Input: sepolicy command + uint64_t arg; // Input: sepolicy argument pointer +}; + +struct ksu_check_safemode_cmd { + uint8_t in_safe_mode; // Output: true if in safe mode, false otherwise +}; + +struct ksu_get_allow_list_cmd { + uint32_t uids[128]; // Output: array of allowed/denied UIDs + uint32_t count; // Output: number of UIDs in array + uint8_t allow; // Input: true for allow list, false for deny list +}; + +struct ksu_uid_granted_root_cmd { + uint32_t uid; // Input: target UID to check + uint8_t granted; // Output: true if granted, false otherwise +}; + +struct ksu_uid_should_umount_cmd { + uint32_t uid; // Input: target UID to check + uint8_t should_umount; // Output: true if should umount, false otherwise +}; + +struct ksu_get_manager_uid_cmd { + uint32_t uid; // Output: manager UID +}; + +struct ksu_set_manager_uid_cmd { + uint32_t uid; // Input: new manager UID +}; + +struct ksu_get_app_profile_cmd { + struct app_profile profile; // Input/Output: app profile structure +}; + +struct ksu_set_app_profile_cmd { + struct app_profile profile; // Input: app profile structure +}; + +// Su compat +bool set_su_enabled(bool enabled); +bool is_su_enabled(); + +// Kernel umount +bool set_kernel_umount_enabled(bool enabled); +bool is_kernel_umount_enabled(); + +// Enhanced security +bool set_enhanced_security_enabled(bool enabled); +bool is_enhanced_security_enabled(); + +// Su log +bool set_sulog_enabled(bool enabled); +bool is_sulog_enabled(); + +// Other command structures +struct ksu_get_full_version_cmd { + char version_full[KSU_FULL_VERSION_STRING]; // Output: full version string +}; + +struct ksu_hook_type_cmd { + char hook_type[32]; // Output: hook type string +}; + +struct ksu_enable_kpm_cmd { + uint8_t enabled; // Output: true if KPM is enabled +}; + +struct ksu_dynamic_manager_cmd { + struct dynamic_manager_user_config config; // Input/Output: dynamic manager config +}; + +struct ksu_get_managers_cmd { + struct manager_list_info manager_info; // Output: manager list information +}; + +struct ksu_enable_uid_scanner_cmd { + uint32_t operation; // Input: operation type (UID_SCANNER_OP_GET_STATUS, UID_SCANNER_OP_TOGGLE, UID_SCANNER_OP_CLEAR_ENV) + uint32_t enabled; // Input: enable or disable (for UID_SCANNER_OP_TOGGLE) + uint64_t status_ptr; // Input: pointer to store status (for UID_SCANNER_OP_GET_STATUS) +}; + +// IOCTL command definitions +#define KSU_IOCTL_GRANT_ROOT _IOC(_IOC_NONE, 'K', 1, 0) +#define KSU_IOCTL_GET_INFO _IOC(_IOC_READ, 'K', 2, 0) +#define KSU_IOCTL_REPORT_EVENT _IOC(_IOC_WRITE, 'K', 3, 0) +#define KSU_IOCTL_SET_SEPOLICY _IOC(_IOC_READ|_IOC_WRITE, 'K', 4, 0) +#define KSU_IOCTL_CHECK_SAFEMODE _IOC(_IOC_READ, 'K', 5, 0) +#define KSU_IOCTL_GET_ALLOW_LIST _IOC(_IOC_READ|_IOC_WRITE, 'K', 6, 0) +#define KSU_IOCTL_GET_DENY_LIST _IOC(_IOC_READ|_IOC_WRITE, 'K', 7, 0) +#define KSU_IOCTL_UID_GRANTED_ROOT _IOC(_IOC_READ|_IOC_WRITE, 'K', 8, 0) +#define KSU_IOCTL_UID_SHOULD_UMOUNT _IOC(_IOC_READ|_IOC_WRITE, 'K', 9, 0) +#define KSU_IOCTL_GET_MANAGER_UID _IOC(_IOC_READ, 'K', 10, 0) +#define KSU_IOCTL_GET_APP_PROFILE _IOC(_IOC_READ|_IOC_WRITE, 'K', 11, 0) +#define KSU_IOCTL_SET_APP_PROFILE _IOC(_IOC_WRITE, 'K', 12, 0) +#define KSU_IOCTL_GET_FEATURE _IOC(_IOC_READ|_IOC_WRITE, 'K', 13, 0) +#define KSU_IOCTL_SET_FEATURE _IOC(_IOC_WRITE, 'K', 14, 0) + +// Other IOCTL command definitions +#define KSU_IOCTL_GET_FULL_VERSION _IOC(_IOC_READ, 'K', 100, 0) +#define KSU_IOCTL_HOOK_TYPE _IOC(_IOC_READ, 'K', 101, 0) +#define KSU_IOCTL_ENABLE_KPM _IOC(_IOC_READ, 'K', 102, 0) +#define KSU_IOCTL_DYNAMIC_MANAGER _IOC(_IOC_READ|_IOC_WRITE, 'K', 103, 0) +#define KSU_IOCTL_GET_MANAGERS _IOC(_IOC_READ|_IOC_WRITE, 'K', 104, 0) +#define KSU_IOCTL_ENABLE_UID_SCANNER _IOC(_IOC_READ|_IOC_WRITE, 'K', 105, 0) + +bool get_allow_list(struct ksu_get_allow_list_cmd *); + +// Legacy Compatible +struct ksu_version_info legacy_get_info(); + +struct ksu_version_info { + int32_t version; + int32_t flags; +}; + +bool legacy_get_allow_list(int *uids, int *size); +bool legacy_is_safe_mode(); +bool legacy_uid_should_umount(int uid); +bool legacy_set_app_profile(const struct app_profile* profile); +bool legacy_get_app_profile(char* key, struct app_profile* profile); +bool legacy_set_su_enabled(bool enabled); +bool legacy_is_su_enabled(); +bool legacy_is_KPM_enable(); +bool legacy_get_hook_type(char* hook_type, size_t size); +void legacy_get_full_version(char* buff); +bool legacy_set_dynamic_manager(unsigned int size, const char* hash); +bool legacy_get_dynamic_manager(struct dynamic_manager_user_config* config); +bool legacy_clear_dynamic_manager(); +bool legacy_get_managers_list(struct manager_list_info* info); +bool legacy_is_uid_scanner_enabled(); +bool legacy_set_uid_scanner_enabled(bool enabled); +bool legacy_clear_uid_scanner_environment(); + #endif //KERNELSU_KSU_H \ No newline at end of file diff --git a/manager/app/src/main/cpp/legacy.c b/manager/app/src/main/cpp/legacy.c new file mode 100644 index 0000000..de72a32 --- /dev/null +++ b/manager/app/src/main/cpp/legacy.c @@ -0,0 +1,163 @@ +// +// Created by shirkneko on 2025/11/3. +// +// Legacy Compatible +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prelude.h" +#include "ksu.h" + +#define KERNEL_SU_OPTION 0xDEADBEEF + +#define CMD_GRANT_ROOT 0 + +#define CMD_BECOME_MANAGER 1 +#define CMD_GET_VERSION 2 +#define CMD_ALLOW_SU 3 +#define CMD_DENY_SU 4 +#define CMD_GET_SU_LIST 5 +#define CMD_GET_DENY_LIST 6 +#define CMD_CHECK_SAFEMODE 9 + +#define CMD_GET_APP_PROFILE 10 +#define CMD_SET_APP_PROFILE 11 + +#define CMD_IS_UID_GRANTED_ROOT 12 +#define CMD_IS_UID_SHOULD_UMOUNT 13 +#define CMD_IS_SU_ENABLED 14 +#define CMD_ENABLE_SU 15 + +#define CMD_GET_VERSION_FULL 0xC0FFEE1A + +#define CMD_ENABLE_KPM 100 +#define CMD_HOOK_TYPE 101 +#define CMD_DYNAMIC_MANAGER 103 +#define CMD_GET_MANAGERS 104 +#define CMD_ENABLE_UID_SCANNER 105 + +static bool ksuctl(int cmd, void* arg1, void* arg2) { + int32_t result = 0; + int32_t rtn = prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result); + return result == KERNEL_SU_OPTION && rtn == -1; +} + +struct ksu_version_info legacy_get_info() +{ + int32_t version = -1; + int32_t flags = 0; + ksuctl(CMD_GET_VERSION, &version, &flags); + return (struct ksu_version_info){version, flags}; +} + +bool legacy_get_allow_list(int *uids, int *size) { + return ksuctl(CMD_GET_SU_LIST, uids, size); +} + +bool legacy_is_safe_mode() { + return ksuctl(CMD_CHECK_SAFEMODE, NULL, NULL); +} + +bool legacy_uid_should_umount(int uid) { + int should; + return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, (void*) ((size_t) uid), &should) && should; +} + +bool legacy_set_app_profile(const struct app_profile* profile) { + return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, NULL); +} + +bool legacy_get_app_profile(char* key, struct app_profile* profile) { + return ksuctl(CMD_GET_APP_PROFILE, profile, NULL); +} + +bool legacy_set_su_enabled(bool enabled) { + return ksuctl(CMD_ENABLE_SU, (void*) enabled, NULL); +} + +bool legacy_is_su_enabled() { + int enabled = true; + // if ksuctl failed, we assume su is enabled, and it cannot be disabled. + ksuctl(CMD_IS_SU_ENABLED, &enabled, NULL); + return enabled; +} + +bool legacy_is_KPM_enable() { + int enabled = false; + ksuctl(CMD_ENABLE_KPM, &enabled, NULL); + return enabled; +} + +bool legacy_get_hook_type(char* hook_type, size_t size) { + if (hook_type == NULL || size == 0) { + return false; + } + + static char cached_hook_type[16] = {0}; + if (cached_hook_type[0] == '\0') { + if (!ksuctl(CMD_HOOK_TYPE, cached_hook_type, NULL)) { + strcpy(cached_hook_type, "Unknown"); + } + } + + strncpy(hook_type, cached_hook_type, size - 1); + hook_type[size - 1] = '\0'; + return true; +} + +void legacy_get_full_version(char* buff) { + ksuctl(CMD_GET_VERSION_FULL, buff, NULL); +} + +bool legacy_set_dynamic_manager(unsigned int size, const char* hash) { + if (hash == NULL) { + return false; + } + struct dynamic_manager_user_config config; + config.operation = DYNAMIC_MANAGER_OP_SET; + config.size = size; + strncpy(config.hash, hash, sizeof(config.hash) - 1); + config.hash[sizeof(config.hash) - 1] = '\0'; + return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL); +} + +bool legacy_get_dynamic_manager(struct dynamic_manager_user_config* config) { + if (config == NULL) { + return false; + } + config->operation = DYNAMIC_MANAGER_OP_GET; + return ksuctl(CMD_DYNAMIC_MANAGER, config, NULL); +} + +bool legacy_clear_dynamic_manager() { + struct dynamic_manager_user_config config; + config.operation = DYNAMIC_MANAGER_OP_CLEAR; + return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL); +} + +bool legacy_get_managers_list(struct manager_list_info* info) { + if (info == NULL) { + return false; + } + return ksuctl(CMD_GET_MANAGERS, info, NULL); +} + +bool legacy_is_uid_scanner_enabled() { + bool status = false; + ksuctl(CMD_ENABLE_UID_SCANNER, (void*)0, &status); + return status; +} + +bool legacy_set_uid_scanner_enabled(bool enabled) { + return ksuctl(CMD_ENABLE_UID_SCANNER, (void*)1, (void*)enabled); +} + +bool legacy_clear_uid_scanner_environment() { + return ksuctl(CMD_ENABLE_UID_SCANNER, (void*)2, NULL); +} \ No newline at end of file diff --git a/manager/app/src/main/cpp/prelude.h b/manager/app/src/main/cpp/prelude.h index 780d841..18e19fa 100644 --- a/manager/app/src/main/cpp/prelude.h +++ b/manager/app/src/main/cpp/prelude.h @@ -14,51 +14,51 @@ // Macros to simplify field setup #define SET_BOOLEAN_FIELD(obj, cls, fieldName, value) do { \ - jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Z"); \ - GetEnvironment()->SetBooleanField(env, obj, field, value); \ + jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Z"); \ + GetEnvironment()->SetBooleanField(env, obj, field, value); \ } while(0) #define SET_INT_FIELD(obj, cls, fieldName, value) do { \ - jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "I"); \ - GetEnvironment()->SetIntField(env, obj, field, value); \ + jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "I"); \ + GetEnvironment()->SetIntField(env, obj, field, value); \ } while(0) #define SET_STRING_FIELD(obj, cls, fieldName, value) do { \ - jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/lang/String;"); \ - GetEnvironment()->SetObjectField(env, obj, field, GetEnvironment()->NewStringUTF(env, value)); \ + jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/lang/String;"); \ + GetEnvironment()->SetObjectField(env, obj, field, GetEnvironment()->NewStringUTF(env, value)); \ } while(0) #define SET_OBJECT_FIELD(obj, cls, fieldName, value) do { \ - jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/util/List;"); \ - GetEnvironment()->SetObjectField(env, obj, field, value); \ + jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/util/List;"); \ + GetEnvironment()->SetObjectField(env, obj, field, value); \ } while(0) // Macros for creating Java objects #define CREATE_JAVA_OBJECT(className) ({ \ - jclass cls = GetEnvironment()->FindClass(env, className); \ - jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); \ - GetEnvironment()->NewObject(env, cls, constructor); \ + jclass cls = GetEnvironment()->FindClass(env, className); \ + jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); \ + GetEnvironment()->NewObject(env, cls, constructor); \ }) // Macros for creating ArrayList #define CREATE_ARRAYLIST() ({ \ - jclass arrayListCls = GetEnvironment()->FindClass(env, "java/util/ArrayList"); \ - jmethodID constructor = GetEnvironment()->GetMethodID(env, arrayListCls, "", "()V"); \ - GetEnvironment()->NewObject(env, arrayListCls, constructor); \ + jclass arrayListCls = GetEnvironment()->FindClass(env, "java/util/ArrayList"); \ + jmethodID constructor = GetEnvironment()->GetMethodID(env, arrayListCls, "", "()V"); \ + GetEnvironment()->NewObject(env, arrayListCls, constructor); \ }) // Macros for adding elements to an ArrayList #define ADD_TO_LIST(list, item) do { \ - jclass cls = GetEnvironment()->GetObjectClass(env, list); \ - jmethodID addMethod = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); \ - GetEnvironment()->CallBooleanMethod(env, list, addMethod, item); \ + jclass cls = GetEnvironment()->GetObjectClass(env, list); \ + jmethodID addMethod = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); \ + GetEnvironment()->CallBooleanMethod(env, list, addMethod, item); \ } while(0) // Macros for creating Java objects with parameter constructors #define CREATE_JAVA_OBJECT_WITH_PARAMS(className, signature, ...) ({ \ - jclass cls = GetEnvironment()->FindClass(env, className); \ - jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", signature); \ - GetEnvironment()->NewObject(env, cls, constructor, __VA_ARGS__); \ + jclass cls = GetEnvironment()->FindClass(env, className); \ + jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", signature); \ + GetEnvironment()->NewObject(env, cls, constructor, __VA_ARGS__); \ }) #ifdef NDEBUG diff --git a/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt b/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt index 73de13d..2f587d1 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt @@ -1,94 +1,40 @@ package com.sukisu.ultra -import android.annotation.SuppressLint -import android.app.Activity -import android.app.ActivityOptions import android.app.Application -import android.content.Context -import android.content.res.Configuration -import android.content.res.Resources -import android.os.Build -import android.os.Bundle +import android.system.Os +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import coil.Coil import coil.ImageLoader import com.dergoogler.mmrl.platform.Platform import me.zhanghai.android.appiconloader.coil.AppIconFetcher import me.zhanghai.android.appiconloader.coil.AppIconKeyer +import okhttp3.Cache +import okhttp3.OkHttpClient import java.io.File -import java.util.* +import java.util.Locale -@SuppressLint("StaticFieldLeak") lateinit var ksuApp: KernelSUApplication -class KernelSUApplication : Application() { - private var currentActivity: Activity? = null +class KernelSUApplication : Application(), ViewModelStoreOwner { - private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks { - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - currentActivity = activity - } - override fun onActivityStarted(activity: Activity) { - currentActivity = activity - } - override fun onActivityResumed(activity: Activity) { - currentActivity = activity - } - override fun onActivityPaused(activity: Activity) {} - override fun onActivityStopped(activity: Activity) {} - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} - override fun onActivityDestroyed(activity: Activity) { - if (currentActivity == activity) { - currentActivity = null - } - } - } - - override fun attachBaseContext(base: Context) { - val prefs = base.getSharedPreferences("settings", MODE_PRIVATE) - val languageCode = prefs.getString("app_language", "") ?: "" - - var context = base - if (languageCode.isNotEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - Locale.setDefault(locale) - - val config = Configuration(base.resources.configuration) - config.setLocale(locale) - - context = base.createConfigurationContext(config) - } - - super.attachBaseContext(context) - } - - @SuppressLint("ObsoleteSdkInt") - override fun getResources(): Resources { - val resources = super.getResources() - val prefs = getSharedPreferences("settings", MODE_PRIVATE) - val languageCode = prefs.getString("app_language", "") ?: "" - - if (languageCode.isNotEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - val config = Configuration(resources.configuration) - config.setLocale(locale) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return createConfigurationContext(config).resources - } else { - @Suppress("DEPRECATION") - resources.updateConfiguration(config, resources.displayMetrics) - } - } - - return resources - } + lateinit var okhttpClient: OkHttpClient + private val appViewModelStore by lazy { ViewModelStore() } override fun onCreate() { super.onCreate() ksuApp = this - // 注册Activity生命周期回调 - registerActivityLifecycleCallbacks(activityLifecycleCallbacks) + // For faster response when first entering superuser or webui activity + val superUserViewModel = ViewModelProvider(this)[SuperUserViewModel::class.java] + CoroutineScope(Dispatchers.Main).launch { + superUserViewModel.fetchAppList() + } Platform.setHiddenApiExemptions() @@ -107,45 +53,20 @@ class KernelSUApplication : Application() { if (!webroot.exists()) { webroot.mkdir() } + + // Provide working env for rust's temp_dir() + Os.setenv("TMPDIR", cacheDir.absolutePath, true) + + okhttpClient = + OkHttpClient.Builder().cache(Cache(File(cacheDir, "okhttp"), 10 * 1024 * 1024)) + .addInterceptor { block -> + block.proceed( + block.request().newBuilder() + .header("User-Agent", "SukiSU/${BuildConfig.VERSION_CODE}") + .header("Accept-Language", Locale.getDefault().toLanguageTag()).build() + ) + }.build() } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - applyLanguageSetting() - } - - @SuppressLint("ObsoleteSdkInt") - private fun applyLanguageSetting() { - val prefs = getSharedPreferences("settings", MODE_PRIVATE) - val languageCode = prefs.getString("app_language", "") ?: "" - - if (languageCode.isNotEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - Locale.setDefault(locale) - - val resources = resources - val config = Configuration(resources.configuration) - config.setLocale(locale) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - createConfigurationContext(config) - } else { - @Suppress("DEPRECATION") - resources.updateConfiguration(config, resources.displayMetrics) - } - } - } - - // 添加刷新当前Activity的方法 - fun refreshCurrentActivity() { - currentActivity?.let { activity -> - val intent = activity.intent - activity.finish() - - val options = ActivityOptions.makeCustomAnimation( - activity, android.R.anim.fade_in, android.R.anim.fade_out - ) - activity.startActivity(intent, options.toBundle()) - } - } -} + override val viewModelStore: ViewModelStore + get() = appViewModelStore +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt index 222df27..db891da 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt @@ -16,23 +16,22 @@ object Natives { // 10946: add capabilities // 10977: change groups_count and groups to avoid overflow write // 11071: Fix the issue of failing to set a custom SELinux type. - const val MINIMAL_SUPPORTED_KERNEL = 11071 - const val MINIMAL_SUPPORTED_KERNEL_FULL = "v3.1.5" - - // 11640: Support query working mode, LKM or GKI - // when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant. - const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648 + // 12143: breaking: new supercall impl + const val MINIMAL_SUPPORTED_KERNEL = 12143 // 12040: Support disable sucompat mode - const val MINIMAL_SUPPORTED_SU_COMPAT = 12040 const val KERNEL_SU_DOMAIN = "u:r:su:s0" + const val MINIMAL_SUPPORTED_KERNEL_FULL = "v3.1.8" + const val MINIMAL_SUPPORTED_KPM = 12800 const val MINIMAL_SUPPORTED_DYNAMIC_MANAGER = 13215 const val MINIMAL_SUPPORTED_UID_SCANNER = 13347 + const val MINIMAL_NEW_IOCTL_KERNEL = 13490 + const val ROOT_UID = 0 const val ROOT_GID = 0 @@ -63,11 +62,9 @@ object Natives { init { System.loadLibrary("zakosign") - System.loadLibrary("zako") + System.loadLibrary("kernelsu") } - // become root manager, return true if success. - external fun becomeManager(pkg: String?): Boolean val version: Int external get @@ -81,6 +78,9 @@ object Natives { val isLkmMode: Boolean external get + val isManager: Boolean + external get + external fun uidShouldUmount(uid: Int): Boolean /** @@ -99,6 +99,34 @@ object Natives { */ external fun isSuEnabled(): Boolean external fun setSuEnabled(enabled: Boolean): Boolean + + /** + * Kernel module umount can be disabled temporarily. + * 0: disabled + * 1: enabled + * negative : error + */ + external fun isKernelUmountEnabled(): Boolean + external fun setKernelUmountEnabled(enabled: Boolean): Boolean + + /** + * Enhanced security can be enabled/disabled. + * 0: disabled + * 1: enabled + * negative : error + */ + external fun isEnhancedSecurityEnabled(): Boolean + external fun setEnhancedSecurityEnabled(enabled: Boolean): Boolean + + /** + * Su Log can be enabled/disabled. + * 0: disabled + * 1: enabled + * negative : error + */ + external fun isSuLogEnabled(): Boolean + external fun setSuLogEnabled(enabled: Boolean): Boolean + external fun isKPMEnabled(): Boolean external fun getHookType(): String @@ -106,7 +134,6 @@ object Natives { * Get SUSFS feature status from kernel * @return SusfsFeatureStatus object containing all feature states, or null if failed */ - external fun getSusfsFeatureStatus(): SusfsFeatureStatus? /** * Set dynamic managerature configuration @@ -138,6 +165,28 @@ object Natives { // 模块签名验证 external fun verifyModuleSignature(modulePath: String): Boolean + /** + * Check if UID scanner is currently enabled + * @return true if UID scanner is enabled, false otherwise + */ + external fun isUidScannerEnabled(): Boolean + + /** + * Enable or disable UID scanner + * @param enabled true to enable, false to disable + * @return true if operation was successful, false otherwise + */ + external fun setUidScannerEnabled(enabled: Boolean): Boolean + + /** + * Clear UID scanner environment (force exit) + * This will forcefully stop all UID scanner operations and clear the environment + * @return true if operation was successful, false otherwise + */ + external fun clearUidScannerEnvironment(): Boolean + + external fun getUserName(uid: Int): String? + private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$" private const val NOBODY_UID = 9999 @@ -159,31 +208,10 @@ object Natives { } fun requireNewKernel(): Boolean { - if (version < MINIMAL_SUPPORTED_KERNEL) return true + if (version != -1 && version < MINIMAL_SUPPORTED_KERNEL) return true return isVersionLessThan(getFullVersion(), MINIMAL_SUPPORTED_KERNEL_FULL) } - @Immutable - @Parcelize - @Keep - data class SusfsFeatureStatus( - val statusSusPath: Boolean = false, - val statusSusMount: Boolean = false, - val statusAutoDefaultMount: Boolean = false, - val statusAutoBindMount: Boolean = false, - val statusSusKstat: Boolean = false, - val statusTryUmount: Boolean = false, - val statusAutoTryUmountBind: Boolean = false, - val statusSpoofUname: Boolean = false, - val statusEnableLog: Boolean = false, - val statusHideSymbols: Boolean = false, - val statusSpoofCmdline: Boolean = false, - val statusOpenRedirect: Boolean = false, - val statusMagicMount: Boolean = false, - val statusOverlayfsAutoKstat: Boolean = false, - val statusSusSu: Boolean = false - ) : Parcelable - @Immutable @Parcelize @Keep diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt index 41d0f43..39201e5 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt @@ -1,137 +1,75 @@ package com.sukisu.ultra.ui +import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageInfo import android.os.* import android.util.Log import com.topjohnwu.superuser.ipc.RootService -import rikka.parcelablelist.ParcelableListSlice -import java.lang.reflect.Method +import com.sukisu.zako.IKsuInterface /** * @author ShirkNeko - * @date 2025/7/2. + * @date 2025/10/17. */ class KsuService : RootService() { - companion object { - private const val TAG = "KsuService" - private const val DESCRIPTOR = "com.sukisu.ultra.IKsuInterface" - private const val TRANSACTION_GET_PACKAGES = IBinder.FIRST_CALL_TRANSACTION + 0 + private val TAG = "KsuService" + + private val cacheLock = Object() + private var _all: List? = null + private val allPackages: List + get() = synchronized(cacheLock) { + _all ?: loadAllPackages().also { _all = it } + } + + private fun loadAllPackages(): List { + val tmp = arrayListOf() + for (user in (getSystemService(USER_SERVICE) as UserManager).userProfiles) { + val userId = user.getUserIdCompat() + tmp += getInstalledPackagesAsUser(userId) + } + return tmp } - interface IKsuInterface : IInterface { - fun getPackages(flags: Int): ParcelableListSlice - } + internal inner class Stub : IKsuInterface.Stub() { + override fun getPackageCount(): Int = allPackages.size - abstract class Stub : Binder(), IKsuInterface { - init { - attachInterface(this, DESCRIPTOR) - } - - companion object { - fun asInterface(obj: IBinder?): IKsuInterface? { - if (obj == null) return null - val iin = obj.queryLocalInterface(DESCRIPTOR) - return if (iin != null && iin is IKsuInterface) { - iin - } else { - Proxy(obj) - } - } - } - - override fun asBinder(): IBinder = this - - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - val descriptor = DESCRIPTOR - when (code) { - INTERFACE_TRANSACTION -> { - reply?.writeString(descriptor) - return true - } - TRANSACTION_GET_PACKAGES -> { - data.enforceInterface(descriptor) - val flagsArg = data.readInt() - val result = getPackages(flagsArg) - reply?.writeNoException() - reply?.writeInt(1) - result.writeToParcel(reply!!, Parcelable.PARCELABLE_WRITE_RETURN_VALUE) - return true - } - } - return super.onTransact(code, data, reply, flags) - } - - private class Proxy(private val mRemote: IBinder) : IKsuInterface { - override fun getPackages(flags: Int): ParcelableListSlice { - val data = Parcel.obtain() - val reply = Parcel.obtain() - return try { - data.writeInterfaceToken(DESCRIPTOR) - data.writeInt(flags) - mRemote.transact(TRANSACTION_GET_PACKAGES, data, reply, 0) - reply.readException() - if (reply.readInt() != 0) { - @Suppress("UNCHECKED_CAST") - ParcelableListSlice.CREATOR.createFromParcel(reply) as ParcelableListSlice - } else { - ParcelableListSlice(emptyList()) - } - } finally { - reply.recycle() - data.recycle() - } - } - - override fun asBinder(): IBinder = mRemote + override fun getPackages(start: Int, maxCount: Int): List { + val list = allPackages + val end = (start + maxCount).coerceAtMost(list.size) + return if (start >= list.size) emptyList() + else list.subList(start, end) } } - inner class KsuInterfaceImpl : Stub() { - override fun getPackages(flags: Int): ParcelableListSlice { - val list = getInstalledPackagesAll(flags) - Log.i(TAG, "getPackages: ${list.size}") - return ParcelableListSlice(list) - } - } + override fun onBind(intent: Intent): IBinder = Stub() - override fun onBind(intent: Intent): IBinder { - return KsuInterfaceImpl() - } - - private fun getUserIds(): List { - val result = mutableListOf() - val um = getSystemService(USER_SERVICE) as UserManager - val userProfiles = um.userProfiles - for (userProfile in userProfiles) { - result.add(userProfile.hashCode()) - } - return result - } - - private fun getInstalledPackagesAll(flags: Int): ArrayList { - val packages = ArrayList() - for (userId in getUserIds()) { - Log.i(TAG, "getInstalledPackagesAll: $userId") - packages.addAll(getInstalledPackagesAsUser(flags, userId)) - } - return packages - } - - private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List { + @SuppressLint("PrivateApi") + private fun getInstalledPackagesAsUser(userId: Int): List { return try { val pm = packageManager - val getInstalledPackagesAsUser: Method = pm.javaClass.getDeclaredMethod( + val m = pm.javaClass.getDeclaredMethod( "getInstalledPackagesAsUser", Int::class.java, Int::class.java ) @Suppress("UNCHECKED_CAST") - getInstalledPackagesAsUser.invoke(pm, flags, userId) as List + m.invoke(pm, 0, userId) as List } catch (e: Throwable) { - Log.e(TAG, "err", e) - ArrayList() + Log.e(TAG, "getInstalledPackagesAsUser", e) + emptyList() + } + } + + private fun UserHandle.getUserIdCompat(): Int { + return try { + javaClass.getDeclaredField("identifier").apply { isAccessible = true }.getInt(this) + } catch (_: NoSuchFieldException) { + javaClass.getDeclaredMethod("getIdentifier").invoke(this) as Int + } catch (e: Throwable) { + Log.e("KsuService", "getUserIdCompat", e) + 0 } } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt index ca0b995..033ee44 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt @@ -1,7 +1,8 @@ package com.sukisu.ultra.ui import android.content.Context -import android.content.res.Configuration +import android.content.Intent +import android.net.Uri import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity @@ -9,13 +10,10 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.* import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import androidx.navigation.NavBackStackEntry @@ -26,6 +24,8 @@ import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationSty import com.ramcosta.composedestinations.generated.NavGraphs import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination import com.ramcosta.composedestinations.spec.NavHostGraphSpec +import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator +import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper import com.sukisu.ultra.Natives import com.sukisu.ultra.ui.screen.BottomBarDestination import com.sukisu.ultra.ui.theme.KernelSUTheme @@ -34,11 +34,11 @@ import com.sukisu.ultra.ui.util.install import com.sukisu.ultra.ui.viewmodel.HomeViewModel import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import com.sukisu.ultra.ui.webui.initPlatform -import io.sukisu.ultra.UltraToolInstall +import com.sukisu.ultra.ui.component.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import zako.zako.zako.zakoui.activity.component.BottomBar -import zako.zako.zako.zakoui.activity.util.* +import com.sukisu.ultra.ui.activity.component.BottomBar +import com.sukisu.ultra.ui.activity.util.* class MainActivity : ComponentActivity() { private lateinit var superUserViewModel: SuperUserViewModel @@ -50,21 +50,18 @@ class MainActivity : ComponentActivity() { val showKpmInfo: Boolean = false ) - private lateinit var themeChangeObserver: ThemeChangeContentObserver + private var showConfirmationDialog = mutableStateOf(false) + private var pendingZipFiles = mutableStateOf>(emptyList()) - // 添加标记避免重复初始化 + private lateinit var themeChangeObserver: ThemeChangeContentObserver private var isInitialized = false - override fun attachBaseContext(newBase: Context) { - val context = LocaleUtils.applyLocale(newBase) - super.attachBaseContext(context) + override fun attachBaseContext(newBase: Context?) { + super.attachBaseContext(newBase?.let { LocaleHelper.applyLanguage(it) }) } override fun onCreate(savedInstanceState: Bundle?) { try { - // 确保应用正确的语言设置 - LocaleUtils.applyLanguageSetting(this) - // 应用自定义 DPI DisplayUtils.applyCustomDpi(this) @@ -77,6 +74,11 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) + val isManager = Natives.isManager + if (isManager && !Natives.requireNewKernel()) { + install() + } + // 使用标记控制初始化流程 if (!isInitialized) { initializeViewModels() @@ -84,6 +86,39 @@ class MainActivity : ComponentActivity() { isInitialized = true } + // Check if launched with a ZIP file + val zipUri: ArrayList? = when (intent?.action) { + Intent.ACTION_SEND -> { + val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(Intent.EXTRA_STREAM) + } + uri?.let { arrayListOf(it) } + } + + Intent.ACTION_SEND_MULTIPLE -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + } + } + + else -> when { + intent?.data != null -> arrayListOf(intent.data!!) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { + intent.getParcelableArrayListExtra("uris", Uri::class.java) + } + else -> { + @Suppress("DEPRECATION") + intent.getParcelableArrayListExtra("uris") + } + } + } + setContent { KernelSUTheme { val navController = rememberNavController() @@ -94,6 +129,38 @@ class MainActivity : ComponentActivity() { BottomBarDestination.entries.map { it.direction.route }.toSet() } + val navigator = navController.rememberDestinationsNavigator() + + InstallConfirmationDialog( + show = showConfirmationDialog.value, + zipFiles = pendingZipFiles.value, + onConfirm = { confirmedFiles -> + showConfirmationDialog.value = false + UltraActivityUtils.navigateToFlashScreen(this, confirmedFiles, navigator) + }, + onDismiss = { + showConfirmationDialog.value = false + pendingZipFiles.value = emptyList() + finish() + } + ) + + LaunchedEffect(zipUri) { + if (!zipUri.isNullOrEmpty()) { + // 检测 ZIP 文件类型并显示确认对话框 + lifecycleScope.launch { + UltraActivityUtils.detectZipTypeAndShowConfirmation(this@MainActivity, zipUri) { infos -> + if (infos.isNotEmpty()) { + pendingZipFiles.value = infos + showConfirmationDialog.value = true + } else { + finish() + } + } + } + } + } + val showBottomBar = when (currentDestination?.route) { ExecuteModuleActionScreenDestination.route -> false else -> true @@ -187,32 +254,17 @@ class MainActivity : ComponentActivity() { } } - lifecycleScope.launch { - try { - homeViewModel.initializeData() - } catch (e: Exception) { - e.printStackTrace() - } - } - // 数据刷新协程 DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope) DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow) // 初始化主题相关设置 ThemeUtils.initializeThemeSettings(this, settingsStateFlow) - - val isManager = Natives.becomeManager(packageName) - if (isManager) { - install() - UltraToolInstall.tryToInstall() - } } override fun onResume() { try { super.onResume() - LocaleUtils.applyLanguageSetting(this) ThemeUtils.onActivityResume() // 仅在需要时刷新数据 @@ -228,7 +280,6 @@ class MainActivity : ComponentActivity() { lifecycleScope.launch { try { superUserViewModel.fetchAppList() - homeViewModel.initializeData() DataRefreshUtils.refreshData(lifecycleScope) } catch (e: Exception) { e.printStackTrace() @@ -253,13 +304,4 @@ class MainActivity : ComponentActivity() { e.printStackTrace() } } - - override fun onConfigurationChanged(newConfig: Configuration) { - try { - super.onConfigurationChanged(newConfig) - LocaleUtils.applyLanguageSetting(this) - } catch (e: Exception) { - e.printStackTrace() - } - } -} +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/component/BottomBar.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/component/BottomBar.kt similarity index 94% rename from manager/app/src/main/java/zako/zako/zako/zakoui/activity/component/BottomBar.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/activity/component/BottomBar.kt index 14e0c11..7efffad 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/component/BottomBar.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/component/BottomBar.kt @@ -1,4 +1,4 @@ -package zako.zako.zako.zakoui.activity.component +package com.sukisu.ultra.ui.activity.component import android.annotation.SuppressLint import androidx.compose.foundation.layout.* @@ -15,21 +15,20 @@ import com.ramcosta.composedestinations.spec.RouteOrDirection import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator import com.sukisu.ultra.Natives -import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ui.MainActivity +import com.sukisu.ultra.ui.activity.util.* +import com.sukisu.ultra.ui.activity.util.AppData.getKpmVersionUse import com.sukisu.ultra.ui.screen.BottomBarDestination import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha import com.sukisu.ultra.ui.theme.CardConfig.cardElevation -import zako.zako.zako.zakoui.activity.util.AppData -import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager -import zako.zako.zako.zakoui.activity.util.AppData.getKpmVersionUse +import com.sukisu.ultra.ui.util.* @SuppressLint("ContextCastToActivity") @OptIn(ExperimentalMaterial3Api::class) @Composable fun BottomBar(navController: NavHostController) { val navigator = navController.rememberDestinationsNavigator() - val isFullFeatured = AppData.isFullFeatured(ksuApp.packageName) + val isFullFeatured = AppData.isFullFeatured() val kpmVersion = getKpmVersionUse() val cardColor = MaterialTheme.colorScheme.surfaceContainer val activity = LocalContext.current as MainActivity @@ -40,9 +39,9 @@ fun BottomBar(navController: NavHostController) { val showKpmInfo = settings.showKpmInfo // 收集计数数据 - val superuserCount by DataRefreshManager.superuserCount.collectAsState() - val moduleCount by DataRefreshManager.moduleCount.collectAsState() - val kpmModuleCount by DataRefreshManager.kpmModuleCount.collectAsState() + val superuserCount by AppData.DataRefreshManager.superuserCount.collectAsState() + val moduleCount by AppData.DataRefreshManager.moduleCount.collectAsState() + val kpmModuleCount by AppData.DataRefreshManager.kpmModuleCount.collectAsState() NavigationBar( diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/ThemeUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/ThemeUtils.kt similarity index 95% rename from manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/ThemeUtils.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/ThemeUtils.kt index 75ce7da..6786917 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/ThemeUtils.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/ThemeUtils.kt @@ -1,8 +1,9 @@ -package zako.zako.zako.zakoui.activity.util +package com.sukisu.ultra.ui.activity.util import android.content.Context import android.database.ContentObserver import android.os.Handler +import android.provider.Settings import androidx.core.content.edit import com.sukisu.ultra.ui.MainActivity import com.sukisu.ultra.ui.theme.CardConfig @@ -56,7 +57,7 @@ object ThemeUtils { } activity.contentResolver.registerContentObserver( - android.provider.Settings.System.getUriFor("ui_night_mode"), + Settings.System.getUriFor("ui_night_mode"), false, contentObserver ) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt new file mode 100644 index 0000000..367e791 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt @@ -0,0 +1,236 @@ +package com.sukisu.ultra.ui.activity.util + +import android.content.Context +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.runtime.Composable +import androidx.lifecycle.LifecycleCoroutineScope +import com.sukisu.ultra.Natives +import com.sukisu.ultra.ui.MainActivity +import com.sukisu.ultra.ui.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.util.* +import android.net.Uri +import androidx.lifecycle.lifecycleScope +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.sukisu.ultra.ui.component.ZipFileDetector +import com.sukisu.ultra.ui.component.ZipFileInfo +import com.sukisu.ultra.ui.component.ZipType +import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination +import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination +import com.sukisu.ultra.ui.screen.FlashIt +import kotlinx.coroutines.withContext +import androidx.core.content.edit + +object AnimatedBottomBar { + @Composable + fun AnimatedBottomBarWrapper( + showBottomBar: Boolean, + content: @Composable () -> Unit + ) { + AnimatedVisibility( + visible = showBottomBar, + enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it }) + fadeOut() + ) { + content() + } + } +} + +object UltraActivityUtils { + + suspend fun detectZipTypeAndShowConfirmation( + activity: MainActivity, + zipUris: ArrayList, + onResult: (List) -> Unit + ) { + val infos = ZipFileDetector.detectAndParseZipFiles(activity, zipUris) + withContext(Dispatchers.Main) { onResult(infos) } + } + + fun navigateToFlashScreen( + activity: MainActivity, + zipFiles: List, + navigator: DestinationsNavigator + ) { + activity.lifecycleScope.launch { + val moduleUris = zipFiles.filter { it.type == ZipType.MODULE }.map { it.uri } + val kernelUris = zipFiles.filter { it.type == ZipType.KERNEL }.map { it.uri } + + when { + kernelUris.isNotEmpty() && moduleUris.isEmpty() -> { + if (kernelUris.size == 1 && rootAvailable()) { + navigator.navigate( + InstallScreenDestination( + preselectedKernelUri = kernelUris.first().toString() + ) + ) + } + setAutoExitAfterFlash(activity) + } + + moduleUris.isNotEmpty() -> { + navigator.navigate( + FlashScreenDestination( + FlashIt.FlashModules(ArrayList(moduleUris)) + ) + ) + setAutoExitAfterFlash(activity) + } + } + } + } + + private fun setAutoExitAfterFlash(activity: Context) { + activity.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + .edit { + putBoolean("auto_exit_after_flash", true) + } + } +} + +object AppData { + object DataRefreshManager { + // 私有状态流 + private val _superuserCount = MutableStateFlow(0) + private val _moduleCount = MutableStateFlow(0) + private val _kpmModuleCount = MutableStateFlow(0) + + // 公开的只读状态流 + val superuserCount: StateFlow = _superuserCount.asStateFlow() + val moduleCount: StateFlow = _moduleCount.asStateFlow() + val kpmModuleCount: StateFlow = _kpmModuleCount.asStateFlow() + + /** + * 刷新所有数据计数 + */ + fun refreshData() { + _superuserCount.value = getSuperuserCountUse() + _moduleCount.value = getModuleCountUse() + _kpmModuleCount.value = getKpmModuleCountUse() + } + } + + /** + * 获取超级用户应用计数 + */ + fun getSuperuserCountUse(): Int { + return try { + if (!rootAvailable()) return 0 + getSuperuserCount() + } catch (_: Exception) { + 0 + } + } + + /** + * 获取模块计数 + */ + fun getModuleCountUse(): Int { + return try { + if (!rootAvailable()) return 0 + getModuleCount() + } catch (_: Exception) { + 0 + } + } + + /** + * 获取KPM模块计数 + */ + fun getKpmModuleCountUse(): Int { + return try { + if (!rootAvailable()) return 0 + val kpmVersion = getKpmVersionUse() + if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) return 0 + getKpmModuleCount() + } catch (_: Exception) { + 0 + } + } + + /** + * 获取KPM版本 + */ + fun getKpmVersionUse(): String { + return try { + if (!rootAvailable()) return "" + val version = getKpmVersion() + version.ifEmpty { "" } + } catch (e: Exception) { + "Error: ${e.message}" + } + } + + /** + * 检查是否是完整功能模式 + */ + fun isFullFeatured(): Boolean { + val isManager = Natives.isManager + return isManager && !Natives.requireNewKernel() && rootAvailable() + } +} + +object DataRefreshUtils { + fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) { + scope.launch(Dispatchers.IO) { + while (isActive) { + AppData.DataRefreshManager.refreshData() + delay(5000) + } + } + } + + fun startSettingsMonitorCoroutine( + scope: LifecycleCoroutineScope, + activity: MainActivity, + settingsStateFlow: MutableStateFlow + ) { + scope.launch(Dispatchers.IO) { + while (isActive) { + val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE) + settingsStateFlow.value = MainActivity.SettingsState( + isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false), + showKpmInfo = prefs.getBoolean("show_kpm_info", false) + ) + delay(1000) + } + } + } + + fun refreshData(scope: LifecycleCoroutineScope) { + scope.launch { + AppData.DataRefreshManager.refreshData() + } + } +} + +object DisplayUtils { + fun applyCustomDpi(context: Context) { + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val customDpi = prefs.getInt("app_dpi", 0) + + if (customDpi > 0) { + try { + val resources = context.resources + val metrics = resources.displayMetrics + metrics.density = customDpi / 160f + @Suppress("DEPRECATION") + metrics.scaledDensity = customDpi / 160f + metrics.densityDpi = customDpi + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt index f398c1b..10c0477 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt @@ -8,11 +8,16 @@ import android.text.method.LinkMovementMethod import android.util.Log import android.view.ViewGroup import android.widget.TextView +import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.Saver @@ -428,27 +433,36 @@ private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, di @Composable private fun MarkdownContent(content: String) { val contentColor = LocalContentColor.current + val scrollState = rememberScrollState() - AndroidView( - factory = { context -> - TextView(context).apply { - movementMethod = LinkMovementMethod.getInstance() - setSpannableFactory(NoCopySpannableFactory.getInstance()) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE - } - hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - }, + Column( modifier = Modifier .fillMaxWidth() - .wrapContentHeight(), - update = { - Markwon.create(it.context).setMarkdown(it, content) - it.setTextColor(contentColor.toArgb()) - } - ) -} \ No newline at end of file + .verticalScroll( + state = scrollState, + flingBehavior = ScrollableDefaults.flingBehavior() + ) + .padding(12.dp) + ) { + AndroidView( + factory = { context -> + TextView(context).apply { + movementMethod = LinkMovementMethod.getInstance() + setSpannableFactory(NoCopySpannableFactory.getInstance()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE + } + hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + }, + update = { + Markwon.create(it.context).setMarkdown(it, content) + it.setTextColor(contentColor.toArgb()) + } + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt deleted file mode 100644 index 4dd8b2e..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt +++ /dev/null @@ -1,223 +0,0 @@ -package com.sukisu.ultra.ui.component - -import android.net.Uri -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTransformGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Fullscreen -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import coil.compose.AsyncImage -import coil.request.ImageRequest -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.util.BackgroundTransformation -import com.sukisu.ultra.ui.util.saveTransformedBackground -import kotlinx.coroutines.launch -import kotlin.math.max - -@Composable -fun ImageEditorDialog( - imageUri: Uri, - onDismiss: () -> Unit, - onConfirm: (Uri) -> Unit -) { - var scale by remember { mutableFloatStateOf(1f) } - var offsetX by remember { mutableFloatStateOf(0f) } - var offsetY by remember { mutableFloatStateOf(0f) } - val context = LocalContext.current - val scope = rememberCoroutineScope() - var lastScale by remember { mutableFloatStateOf(1f) } - var lastOffsetX by remember { mutableFloatStateOf(0f) } - var lastOffsetY by remember { mutableFloatStateOf(0f) } - var imageSize by remember { mutableStateOf(Size.Zero) } - var screenSize by remember { mutableStateOf(Size.Zero) } - val animatedScale by animateFloatAsState( - targetValue = scale, - label = "ScaleAnimation" - ) - val animatedOffsetX by animateFloatAsState( - targetValue = offsetX, - label = "OffsetXAnimation" - ) - val animatedOffsetY by animateFloatAsState( - targetValue = offsetY, - label = "OffsetYAnimation" - ) - val updateTransformation = remember { - { newScale: Float, newOffsetX: Float, newOffsetY: Float -> - val scaleDiff = kotlin.math.abs(newScale - lastScale) - val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX) - val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY) - if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) { - scale = newScale - offsetX = newOffsetX - offsetY = newOffsetY - lastScale = newScale - lastOffsetX = newOffsetX - lastOffsetY = newOffsetY - } - } - } - val scaleToFullScreen = remember { - { - if (imageSize.height > 0 && screenSize.height > 0) { - val newScale = screenSize.height / imageSize.height - updateTransformation(newScale, 0f, 0f) - } - } - } - - Dialog( - onDismissRequest = onDismiss, - properties = DialogProperties( - dismissOnBackPress = true, - dismissOnClickOutside = false, - usePlatformDefaultWidth = false - ) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.9f)) - .onSizeChanged { size -> - screenSize = Size(size.width.toFloat(), size.height.toFloat()) - } - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUri) - .crossfade(true) - .build(), - contentDescription = stringResource(R.string.settings_custom_background), - contentScale = ContentScale.Fit, - modifier = Modifier - .fillMaxSize() - .graphicsLayer( - scaleX = animatedScale, - scaleY = animatedScale, - translationX = animatedOffsetX, - translationY = animatedOffsetY - ) - .pointerInput(Unit) { - detectTransformGestures { _, pan, zoom, _ -> - scope.launch { - try { - val newScale = (scale * zoom).coerceIn(0.5f, 3f) - val maxOffsetX = max(0f, size.width * (newScale - 1) / 2) - val maxOffsetY = max(0f, size.height * (newScale - 1) / 2) - val newOffsetX = if (maxOffsetX > 0) { - (offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX) - } else { - 0f - } - val newOffsetY = if (maxOffsetY > 0) { - (offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY) - } else { - 0f - } - updateTransformation(newScale, newOffsetX, newOffsetY) - } catch (_: Exception) { - updateTransformation(lastScale, lastOffsetX, lastOffsetY) - } - } - } - } - .onSizeChanged { size -> - imageSize = Size(size.width.toFloat(), size.height.toFloat()) - } - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .align(Alignment.TopCenter), - horizontalArrangement = Arrangement.SpaceBetween - ) { - IconButton( - onClick = onDismiss, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.cancel), - tint = Color.White - ) - } - IconButton( - onClick = { scaleToFullScreen() }, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - ) { - Icon( - imageVector = Icons.Default.Fullscreen, - contentDescription = stringResource(R.string.reprovision), - tint = Color.White - ) - } - IconButton( - onClick = { - scope.launch { - try { - val transformation = BackgroundTransformation(scale, offsetX, offsetY) - val savedUri = context.saveTransformedBackground(imageUri, transformation) - savedUri?.let { onConfirm(it) } - } catch (_: Exception) { - "" - } - } - }, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - ) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = stringResource(R.string.confirm), - tint = Color.White - ) - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - .padding(16.dp) - .align(Alignment.BottomCenter) - ) { - Text( - text = stringResource(id = R.string.image_editor_hint), - color = Color.White, - style = MaterialTheme.typography.bodyMedium - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt new file mode 100644 index 0000000..6ae6a47 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt @@ -0,0 +1,441 @@ +package com.sukisu.ultra.ui.component + +import android.content.Context +import android.net.Uri +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.filled.Extension +import androidx.compose.material.icons.filled.GetApp +import androidx.compose.material.icons.filled.Memory +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.sukisu.ultra.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader +import java.util.zip.ZipInputStream + +enum class ZipType { + MODULE, + KERNEL, + UNKNOWN +} + +data class ZipFileInfo( + val uri: Uri, + val type: ZipType, + val name: String = "", + val version: String = "", + val versionCode: String = "", + val author: String = "", + val description: String = "", + val kernelVersion: String = "", + val supported: String = "" +) + +object ZipFileDetector { + + fun detectZipType(context: Context, uri: Uri): ZipType { + return try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + ZipInputStream(inputStream).use { zipStream -> + var hasModuleProp = false + var hasToolsFolder = false + var hasAnykernelSh = false + + var entry = zipStream.nextEntry + while (entry != null) { + val entryName = entry.name.lowercase() + + when { + entryName == "module.prop" || entryName.endsWith("/module.prop") -> { + hasModuleProp = true + } + entryName.startsWith("tools/") || entryName == "tools" -> { + hasToolsFolder = true + } + entryName == "anykernel.sh" || entryName.endsWith("/anykernel.sh") -> { + hasAnykernelSh = true + } + } + + zipStream.closeEntry() + entry = zipStream.nextEntry + } + + when { + hasModuleProp -> ZipType.MODULE + hasToolsFolder && hasAnykernelSh -> ZipType.KERNEL + else -> ZipType.UNKNOWN + } + } + } ?: ZipType.UNKNOWN + } catch (e: IOException) { + e.printStackTrace() + ZipType.UNKNOWN + } + } + + fun parseModuleInfo(context: Context, uri: Uri): ZipFileInfo { + var zipInfo = ZipFileInfo(uri = uri, type = ZipType.MODULE) + + try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + ZipInputStream(inputStream).use { zipStream -> + var entry = zipStream.nextEntry + while (entry != null) { + if (entry.name.lowercase() == "module.prop" || entry.name.endsWith("/module.prop")) { + val reader = BufferedReader(InputStreamReader(zipStream)) + val props = mutableMapOf() + + var line = reader.readLine() + while (line != null) { + if (line.contains("=") && !line.startsWith("#")) { + val parts = line.split("=", limit = 2) + if (parts.size == 2) { + props[parts[0].trim()] = parts[1].trim() + } + } + line = reader.readLine() + } + + zipInfo = zipInfo.copy( + name = props["name"] ?: context.getString(R.string.unknown_module), + version = props["version"] ?: "", + versionCode = props["versionCode"] ?: "", + author = props["author"] ?: "", + description = props["description"] ?: "" + ) + break + } + zipStream.closeEntry() + entry = zipStream.nextEntry + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + + return zipInfo + } + + fun parseKernelInfo(context: Context, uri: Uri): ZipFileInfo { + var zipInfo = ZipFileInfo(uri = uri, type = ZipType.KERNEL) + + try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + ZipInputStream(inputStream).use { zipStream -> + var entry = zipStream.nextEntry + while (entry != null) { + if (entry.name.lowercase() == "anykernel.sh" || entry.name.endsWith("/anykernel.sh")) { + val reader = BufferedReader(InputStreamReader(zipStream)) + val props = mutableMapOf() + + var inPropertiesBlock = false + var line = reader.readLine() + while (line != null) { + if (line.contains("properties()")) { + inPropertiesBlock = true + } else if (inPropertiesBlock && line.contains("'; }")) { + inPropertiesBlock = false + } else if (inPropertiesBlock) { + val propertyLine = line.trim() + if (propertyLine.contains("=") && !propertyLine.startsWith("#")) { + val parts = propertyLine.split("=", limit = 2) + if (parts.size == 2) { + val key = parts[0].trim() + val value = parts[1].trim().removeSurrounding("'").removeSurrounding("\"") + when (key) { + "kernel.string" -> props["name"] = value + "supported.versions" -> props["supported"] = value + } + } + } + } + + // 解析普通变量定义 + if (line.contains("kernel.string=") && !inPropertiesBlock) { + val value = line.substringAfter("kernel.string=").trim().removeSurrounding("\"") + props["name"] = value + } + if (line.contains("supported.versions=") && !inPropertiesBlock) { + val value = line.substringAfter("supported.versions=").trim().removeSurrounding("\"") + props["supported"] = value + } + if (line.contains("kernel.version=") && !inPropertiesBlock) { + val value = line.substringAfter("kernel.version=").trim().removeSurrounding("\"") + props["version"] = value + } + if (line.contains("kernel.author=") && !inPropertiesBlock) { + val value = line.substringAfter("kernel.author=").trim().removeSurrounding("\"") + props["author"] = value + } + + line = reader.readLine() + } + + zipInfo = zipInfo.copy( + name = props["name"] ?: context.getString(R.string.unknown_kernel), + version = props["version"] ?: "", + author = props["author"] ?: "", + supported = props["supported"] ?: "", + kernelVersion = props["version"] ?: "" + ) + break + } + zipStream.closeEntry() + entry = zipStream.nextEntry + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + + return zipInfo + } + + suspend fun detectAndParseZipFiles(context: Context, zipUris: List): List { + return withContext(Dispatchers.IO) { + val zipFileInfos = mutableListOf() + + for (uri in zipUris) { + val zipType = detectZipType(context, uri) + val zipInfo = when (zipType) { + ZipType.MODULE -> parseModuleInfo(context, uri) + ZipType.KERNEL -> parseKernelInfo(context, uri) + ZipType.UNKNOWN -> ZipFileInfo( + uri = uri, + type = ZipType.UNKNOWN, + name = context.getString(R.string.unknown_file) + ) + } + zipFileInfos.add(zipInfo) + } + + zipFileInfos.filter { it.type != ZipType.UNKNOWN } + } + } +} + +@Composable +fun InstallConfirmationDialog( + show: Boolean, + zipFiles: List, + onConfirm: (List) -> Unit, + onDismiss: () -> Unit +) { + if (show && zipFiles.isNotEmpty()) { + val context = LocalContext.current + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = if (zipFiles.any { it.type == ZipType.KERNEL }) + Icons.Default.Memory else Icons.Default.Extension, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = if (zipFiles.size == 1) { + context.getString(R.string.confirm_installation) + } else { + context.getString(R.string.confirm_multiple_installation, zipFiles.size) + }, + style = MaterialTheme.typography.headlineSmall + ) + } + }, + text = { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 400.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(zipFiles.size) { index -> + val zipFile = zipFiles[index] + InstallItemCard(zipFile = zipFile) + } + } + }, + confirmButton = { + Button( + onClick = { onConfirm(zipFiles) }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Icon( + imageVector = Icons.Default.GetApp, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(context.getString(R.string.install_confirm)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text( + context.getString(android.R.string.cancel), + color = MaterialTheme.colorScheme.onSurface + ) + } + }, + modifier = Modifier.widthIn(min = 320.dp, max = 560.dp) + ) + } +} + +@Composable +fun InstallItemCard(zipFile: ZipFileInfo) { + val context = LocalContext.current + + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.elevatedCardColors( + containerColor = when (zipFile.type) { + ZipType.MODULE -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ZipType.KERNEL -> MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f) + else -> MaterialTheme.colorScheme.surfaceVariant + } + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = 0.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = when (zipFile.type) { + ZipType.MODULE -> Icons.Default.Extension + ZipType.KERNEL -> Icons.Default.Memory + else -> Icons.AutoMirrored.Filled.Help + }, + contentDescription = null, + tint = when (zipFile.type) { + ZipType.MODULE -> MaterialTheme.colorScheme.primary + ZipType.KERNEL -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = zipFile.name.ifEmpty { + when (zipFile.type) { + ZipType.MODULE -> context.getString(R.string.unknown_module) + ZipType.KERNEL -> context.getString(R.string.unknown_kernel) + else -> context.getString(R.string.unknown_file) + } + }, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = when (zipFile.type) { + ZipType.MODULE -> context.getString(R.string.module_package) + ZipType.KERNEL -> context.getString(R.string.kernel_package) + else -> context.getString(R.string.unknown_package) + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // 详细信息 + if (zipFile.version.isNotEmpty() || zipFile.author.isNotEmpty() || + zipFile.description.isNotEmpty() || zipFile.supported.isNotEmpty()) { + + Spacer(modifier = Modifier.height(12.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), + thickness = 0.5.dp + ) + Spacer(modifier = Modifier.height(8.dp)) + + // 版本信息 + if (zipFile.version.isNotEmpty()) { + InfoRow( + label = context.getString(R.string.version), + value = zipFile.version + if (zipFile.versionCode.isNotEmpty()) " (${zipFile.versionCode})" else "" + ) + } + + // 作者信息 + if (zipFile.author.isNotEmpty()) { + InfoRow( + label = context.getString(R.string.author), + value = zipFile.author + ) + } + + // 描述信息 (仅模块) + if (zipFile.description.isNotEmpty() && zipFile.type == ZipType.MODULE) { + InfoRow( + label = context.getString(R.string.description), + value = zipFile.description + ) + } + + // 支持设备 (仅内核) + if (zipFile.supported.isNotEmpty() && zipFile.type == ZipType.KERNEL) { + InfoRow( + label = context.getString(R.string.supported_devices), + value = zipFile.supported + ) + } + } + } + } +} + +@Composable +fun InfoRow(label: String, value: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp), + verticalAlignment = Alignment.Top + ) { + Text( + text = "$label:", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.widthIn(min = 60.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = value, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f) + ) + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt index 5adf608..eb3c5db 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt @@ -8,7 +8,7 @@ import com.sukisu.ultra.ksuApp fun KsuIsValid( content: @Composable () -> Unit ) { - val isManager = Natives.becomeManager(ksuApp.packageName) + val isManager = Natives.isManager val ksuVersion = if (isManager) Natives.version else null if (ksuVersion != null) { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt new file mode 100644 index 0000000..9103e55 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt @@ -0,0 +1,250 @@ +package com.sukisu.ultra.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SuperDropdown( + items: List, + selectedIndex: Int, + title: String, + summary: String? = null, + icon: ImageVector? = null, + enabled: Boolean = true, + showValue: Boolean = true, + maxHeight: Dp? = 400.dp, + colors: SuperDropdownColors = SuperDropdownDefaults.colors(), + leftAction: (@Composable () -> Unit)? = null, + onSelectedIndexChange: (Int) -> Unit +) { + var showDialog by remember { mutableStateOf(false) } + val selectedItemText = items.getOrNull(selectedIndex) ?: "" + val itemsNotEmpty = items.isNotEmpty() + val actualEnabled = enabled && itemsNotEmpty + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = actualEnabled) { showDialog = true } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.Top + ) { + if (leftAction != null) { + leftAction() + } else if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (actualEnabled) colors.iconColor else colors.disabledIconColor, + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp) + ) + } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = if (actualEnabled) colors.titleColor else colors.disabledTitleColor + ) + + if (summary != null) { + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = summary, + style = MaterialTheme.typography.bodyMedium, + color = if (actualEnabled) colors.summaryColor else colors.disabledSummaryColor + ) + } + + if (showValue && itemsNotEmpty) { + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = selectedItemText, + style = MaterialTheme.typography.bodyMedium, + color = if (actualEnabled) colors.valueColor else colors.disabledValueColor, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + tint = if (actualEnabled) colors.arrowColor else colors.disabledArrowColor, + modifier = Modifier.size(24.dp) + ) + } + + if (showDialog && itemsNotEmpty) { + AlertDialog( + onDismissRequest = { showDialog = false }, + title = { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall + ) + }, + text = { + val dialogMaxHeight = maxHeight ?: 400.dp + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = dialogMaxHeight), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(items.size) { index -> + DropdownItem( + text = items[index], + isSelected = selectedIndex == index, + colors = colors, + onClick = { + onSelectedIndexChange(index) + showDialog = false + } + ) + } + } + }, + confirmButton = { + TextButton(onClick = { showDialog = false }) { + Text(text = stringResource(id = android.R.string.cancel)) + } + }, + containerColor = colors.dialogBackgroundColor, + shape = MaterialTheme.shapes.extraLarge, + tonalElevation = 4.dp + ) + } +} + +@Composable +private fun DropdownItem( + text: String, + isSelected: Boolean, + colors: SuperDropdownColors, + onClick: () -> Unit +) { + val backgroundColor = if (isSelected) { + colors.selectedBackgroundColor + } else { + Color.Transparent + } + + val contentColor = if (isSelected) { + colors.selectedContentColor + } else { + colors.contentColor + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(backgroundColor) + .clickable(onClick = onClick) + .padding(vertical = 12.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = isSelected, + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = colors.selectedContentColor, + unselectedColor = colors.contentColor + ) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + color = contentColor, + modifier = Modifier.weight(1f) + ) + + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = colors.selectedContentColor, + modifier = Modifier.size(20.dp) + ) + } + } +} + +@Immutable +data class SuperDropdownColors( + val titleColor: Color, + val summaryColor: Color, + val valueColor: Color, + val iconColor: Color, + val arrowColor: Color, + val disabledTitleColor: Color, + val disabledSummaryColor: Color, + val disabledValueColor: Color, + val disabledIconColor: Color, + val disabledArrowColor: Color, + val dialogBackgroundColor: Color, + val contentColor: Color, + val selectedContentColor: Color, + val selectedBackgroundColor: Color +) + +object SuperDropdownDefaults { + @Composable + fun colors( + titleColor: Color = MaterialTheme.colorScheme.onSurface, + summaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + valueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + iconColor: Color = MaterialTheme.colorScheme.primary, + arrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + disabledTitleColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + disabledSummaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + disabledValueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + disabledIconColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + disabledArrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + dialogBackgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor: Color = MaterialTheme.colorScheme.onSurface, + selectedContentColor: Color = MaterialTheme.colorScheme.primary, + selectedBackgroundColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ): SuperDropdownColors { + return SuperDropdownColors( + titleColor = titleColor, + summaryColor = summaryColor, + valueColor = valueColor, + iconColor = iconColor, + arrowColor = arrowColor, + disabledTitleColor = disabledTitleColor, + disabledSummaryColor = disabledSummaryColor, + disabledValueColor = disabledValueColor, + disabledIconColor = disabledIconColor, + disabledArrowColor = disabledArrowColor, + dialogBackgroundColor = dialogBackgroundColor, + contentColor = contentColor, + selectedContentColor = selectedContentColor, + selectedBackgroundColor = selectedBackgroundColor + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt index 914240c..e37cd81 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.sukisu.ultra.R -// 菜单项数据类 data class FabMenuItem( val icon: ImageVector, val labelRes: Int, @@ -29,7 +28,6 @@ data class FabMenuItem( val onClick: () -> Unit ) -// 动画配置 object FabAnimationConfig { const val ANIMATION_DURATION = 300 const val STAGGER_DELAY = 50 @@ -53,23 +51,15 @@ fun VerticalExpandableFab( ) { var isExpanded by remember { mutableStateOf(false) } - // 主按钮旋转动画 val rotationAngle by animateFloatAsState( targetValue = if (isExpanded) 45f else 0f, - animationSpec = tween( - durationMillis = animationDurationMs, - easing = FastOutSlowInEasing - ), + animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing), label = "mainButtonRotation" ) - // 主按钮缩放动画 val mainButtonScale by animateFloatAsState( targetValue = if (isExpanded) 1.1f else 1f, - animationSpec = tween( - durationMillis = animationDurationMs, - easing = FastOutSlowInEasing - ), + animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing), label = "mainButtonScale" ) @@ -77,14 +67,9 @@ fun VerticalExpandableFab( modifier = modifier.wrapContentSize(), contentAlignment = Alignment.BottomEnd ) { - // 子菜单按钮 menuItems.forEachIndexed { index, menuItem -> val animatedOffsetY by animateFloatAsState( - targetValue = if (isExpanded) { - -(buttonSpacing.value * (index + 1)) - } else { - 0f - }, + targetValue = if (isExpanded) -(buttonSpacing.value * (index + 1)) else 0f, animationSpec = tween( durationMillis = animationDurationMs, delayMillis = if (isExpanded) { @@ -125,7 +110,6 @@ fun VerticalExpandableFab( label = "fabAlpha$index" ) - // 子按钮容器(包含标签) Row( modifier = Modifier .offset(y = animatedOffsetY.dp) @@ -134,7 +118,6 @@ fun VerticalExpandableFab( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End ) { - // 标签 AnimatedVisibility( visible = isExpanded && animatedScale > 0.5f, enter = slideInHorizontally( @@ -161,7 +144,6 @@ fun VerticalExpandableFab( } } - // 子按钮 SmallFloatingActionButton( onClick = { menuItem.onClick() @@ -193,15 +175,12 @@ fun VerticalExpandableFab( } } - // 主按钮 FloatingActionButton( onClick = { onMainButtonClick?.invoke() isExpanded = !isExpanded }, - modifier = Modifier - .size(buttonSize) - .scale(mainButtonScale), + modifier = Modifier.size(buttonSize).scale(mainButtonScale), elevation = FloatingActionButtonDefaults.elevation( defaultElevation = 6.dp, pressedElevation = 8.dp, @@ -221,7 +200,6 @@ fun VerticalExpandableFab( } } -// 预设菜单项 object FabMenuPresets { fun getScrollMenuItems( onScrollToTop: () -> Unit, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt index 08c093a..7af311b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt @@ -43,7 +43,7 @@ fun TemplateConfig( ) { OutlinedTextField( modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) .fillMaxWidth(), readOnly = true, label = { Text(stringResource(R.string.profile_template)) }, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt index bb8eca0..7764cb0 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt @@ -248,7 +248,12 @@ private fun AppProfileInner( ProfileBox(mode, true) { // template mode shouldn't change profile here! if (it == Mode.Default || it == Mode.Custom) { - onProfileChange(profile.copy(rootUseDefault = it == Mode.Default)) + onProfileChange( + profile.copy( + rootUseDefault = it == Mode.Default, + rootTemplate = null + ) + ) } mode = it } @@ -479,7 +484,10 @@ private fun ProfileBox( @SuppressLint("UnusedBoxWithConstraintsScope") @Composable -private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) { +private fun AppMenuBox( + packageName: String, + content: @Composable () -> Unit +) { var expanded by remember { mutableStateOf(false) } var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) } val density = LocalDensity.current @@ -499,15 +507,15 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) { content() val (offsetX, offsetY) = with(density) { - (touchPoint.x.toDp()) to (touchPoint.y.toDp()) + (touchPoint.x.toDp()) to (-touchPoint.y.toDp()) } DropdownMenu( expanded = expanded, - offset = DpOffset(offsetX, -offsetY), + offset = DpOffset(offsetX, offsetY), onDismissRequest = { expanded = false - }, + } ) { AppMenuOption( text = stringResource(id = R.string.launch_app), diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt index d401806..e991839 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt @@ -1,5 +1,7 @@ package com.sukisu.ultra.ui.screen +import android.content.Context +import android.content.Intent import android.net.Uri import android.os.Environment import android.os.Parcelable @@ -30,6 +32,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.activity.ComponentActivity import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination @@ -48,6 +51,9 @@ import kotlinx.parcelize.Parcelize import java.io.File import java.text.SimpleDateFormat import java.util.* +import androidx.core.content.edit +import com.sukisu.ultra.ui.util.module.ModuleOperationUtils +import com.sukisu.ultra.ui.util.module.ModuleUtils /** * @author ShirkNeko @@ -119,6 +125,29 @@ fun setModuleVerificationStatus(uri: Uri, isVerified: Boolean) { @Destination fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { val context = LocalContext.current + + val shouldAutoExit = remember { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.getBoolean("auto_exit_after_flash", false) + } + + // 是否通过从外部启动的模块安装 + val isExternalInstall = remember { + when (flashIt) { + is FlashIt.FlashModule -> { + (context as? ComponentActivity)?.intent?.let { intent -> + intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND + } ?: false + } + is FlashIt.FlashModules -> { + (context as? ComponentActivity)?.intent?.let { intent -> + intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND + } ?: false + } + else -> false + } + } + var text by rememberSaveable { mutableStateOf("") } var tempText: String val logContent = rememberSaveable { StringBuilder() } @@ -203,8 +232,25 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { if (showReboot) { text += "\n\n\n" showFloatAction = true + + // 如果是内部安装,显示重启按钮后不自动返回 + if (isExternalInstall) { + return@flashModuleUpdate + } } hasUpdateCompleted = true + + // 如果是外部安装或需要自动退出的模块更新且不需要重启,延迟后自动返回 + if (isExternalInstall || shouldAutoExit) { + scope.launch { + kotlinx.coroutines.delay(1000) + if (shouldAutoExit) { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + } + (context as? ComponentActivity)?.finish() + } + } }, onStdout = { tempText = "$it\n" if (tempText.startsWith("")) { // clear command @@ -297,6 +343,26 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { kotlinx.coroutines.delay(500) navigator.navigate(FlashScreenDestination(nextFlashIt)) } + } else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModules && flashIt.currentIndex >= flashIt.uris.size - 1) { + // 如果是外部安装或需要自动退出且是最后一个模块,安装完成后自动返回 + scope.launch { + kotlinx.coroutines.delay(1000) + if (shouldAutoExit) { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + } + (context as? ComponentActivity)?.finish() + } + } else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModule) { + // 如果是外部安装或需要自动退出的单个模块,安装完成后自动返回 + scope.launch { + kotlinx.coroutines.delay(1000) + if (shouldAutoExit) { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + } + (context as? ComponentActivity)?.finish() + } } }, onStdout = { tempText = "$it\n" @@ -319,14 +385,18 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { } if (canGoBack) { - if (flashIt is FlashIt.FlashModules || flashIt is FlashIt.FlashModuleUpdate) { - viewModel.markNeedRefresh() - viewModel.fetchModuleList() - navigator.navigate(ModuleScreenDestination) + if (isExternalInstall) { + (context as? ComponentActivity)?.finish() } else { - viewModel.markNeedRefresh() - viewModel.fetchModuleList() - navigator.popBackStack() + if (flashIt is FlashIt.FlashModules || flashIt is FlashIt.FlashModuleUpdate) { + viewModel.markNeedRefresh() + viewModel.fetchModuleList() + navigator.navigate(ModuleScreenDestination) + } else { + viewModel.markNeedRefresh() + viewModel.fetchModuleList() + navigator.popBackStack() + } } } } @@ -619,7 +689,7 @@ private fun TopBar( ) } -suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): String { +suspend fun getModuleNameFromUri(context: Context, uri: Uri): String { return withContext(Dispatchers.IO) { try { if (uri == Uri.EMPTY) { @@ -637,7 +707,7 @@ suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): St @Parcelize sealed class FlashIt : Parcelable { - data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt() + data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean, val partition: String? = null) : FlashIt() data class FlashModule(val uri: Uri) : FlashIt() data class FlashModules(val uris: List, val currentIndex: Int = 0) : FlashIt() data class FlashModuleUpdate(val uri: Uri) : FlashIt() // 模块更新 @@ -666,6 +736,7 @@ fun flashIt( flashIt.boot, flashIt.lkm, flashIt.ota, + flashIt.partition, onFinish, onStdout, onStderr diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt index 954044e..b6ef712 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt @@ -50,20 +50,21 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation -import com.sukisu.ultra.ui.util.SuSFSManager +import com.sukisu.ultra.ui.susfs.util.SuSFSManager import com.sukisu.ultra.ui.util.checkNewVersion -import com.sukisu.ultra.ui.util.getSuSFS +import com.sukisu.ultra.ui.util.getSuSFSVersion import com.sukisu.ultra.ui.util.module.LatestVersionInfo import com.sukisu.ultra.ui.util.reboot import com.sukisu.ultra.ui.viewmodel.HomeViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.random.Random /** * @author ShirkNeko - * @date 2025/5/31. + * @date 2025/9/29. */ @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Destination(start = true) @@ -73,16 +74,35 @@ fun HomeScreen(navigator: DestinationsNavigator) { val viewModel = viewModel() val coroutineScope = rememberCoroutineScope() + val pullRefreshState = rememberPullRefreshState( + refreshing = viewModel.isRefreshing, + onRefresh = { + viewModel.onPullRefresh(context) + } + ) + LaunchedEffect(key1 = navigator) { + viewModel.loadUserSettings(context) coroutineScope.launch { - viewModel.refreshAllData(context) + viewModel.loadCoreData() + delay(100) + viewModel.loadExtendedData(context) + } + + // 启动数据变化监听 + coroutineScope.launch { + while (true) { + delay(5000) // 每5秒检查一次 + viewModel.autoRefreshIfNeeded(context) + } } } - LaunchedEffect(Unit) { - viewModel.loadUserSettings(context) - viewModel.initializeData() - viewModel.checkForUpdates(context) + // 监听数据刷新状态流 + LaunchedEffect(viewModel.dataRefreshTrigger) { + viewModel.dataRefreshTrigger.collect { _ -> + // 数据刷新时的额外处理可以在这里添加 + } } val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) @@ -92,22 +112,14 @@ fun HomeScreen(navigator: DestinationsNavigator) { topBar = { TopBar( scrollBehavior = scrollBehavior, - navigator = navigator + navigator = navigator, + isDataLoaded = viewModel.isCoreDataLoaded ) }, contentWindowInsets = WindowInsets.safeDrawing.only( WindowInsetsSides.Top + WindowInsetsSides.Horizontal ) ) { innerPadding -> - val pullRefreshState = rememberPullRefreshState( - refreshing = false, - onRefresh = { - coroutineScope.launch { - viewModel.refreshAllData(context) - } - } - ) - Box( modifier = Modifier .padding(innerPadding) @@ -121,50 +133,78 @@ fun HomeScreen(navigator: DestinationsNavigator) { .padding(top = 12.dp, start = 16.dp, end = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - StatusCard( - systemStatus = viewModel.systemStatus, - onClickInstall = { - navigator.navigate(InstallScreenDestination) - } - ) + // 状态卡片 + if (viewModel.isCoreDataLoaded) { + StatusCard( + systemStatus = viewModel.systemStatus, + onClickInstall = { + navigator.navigate(InstallScreenDestination(preselectedKernelUri = null)) + } + ) - if (viewModel.systemStatus.requireNewKernel) { - WarningCard( - stringResource(id = R.string.require_kernel_version).format( - Natives.getSimpleVersionFull(), - Natives.MINIMAL_SUPPORTED_KERNEL_FULL + // 警告信息 + if (viewModel.systemStatus.requireNewKernel) { + WarningCard( + stringResource(id = R.string.require_kernel_version).format( + Natives.getSimpleVersionFull(), + Natives.MINIMAL_SUPPORTED_KERNEL_FULL + ) ) + } + + if (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) { + WarningCard( + stringResource(id = R.string.grant_root_failed) + ) + } + + // 只有在没有其他警告信息时才显示不兼容内核警告 + val shouldShowWarnings = viewModel.systemStatus.requireNewKernel || + (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) + + if (Natives.version <= Natives.MINIMAL_NEW_IOCTL_KERNEL && !shouldShowWarnings && viewModel.systemStatus.ksuVersion != null) { + IncompatibleKernelCard() + Spacer(Modifier.height(12.dp)) + } + } + + // 更新检查 + if (viewModel.isExtendedDataLoaded) { + val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + .getBoolean("check_update", true) + if (checkUpdate) { + UpdateCard() + } + + // 信息卡片 + InfoCard( + systemInfo = viewModel.systemInfo, + isSimpleMode = viewModel.isSimpleMode, + isHideSusfsStatus = viewModel.isHideSusfsStatus, + isHideZygiskImplement = viewModel.isHideZygiskImplement, + showKpmInfo = viewModel.showKpmInfo, + lkmMode = viewModel.systemStatus.lkmMode, ) - } - if (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) { - WarningCard( - stringResource(id = R.string.grant_root_failed) - ) - } - - val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("check_update", true) - if (checkUpdate) { - UpdateCard() - } - - InfoCard( - systemInfo = viewModel.systemInfo, - isSimpleMode = viewModel.isSimpleMode, - isHideSusfsStatus = viewModel.isHideSusfsStatus, - isHideZygiskImplement = viewModel.isHideZygiskImplement, - showKpmInfo = viewModel.showKpmInfo, - lkmMode = viewModel.systemStatus.lkmMode, - ) - - if (!viewModel.isSimpleMode) { - if (!viewModel.isHideLinkCard) { + // 链接卡片 + if (!viewModel.isSimpleMode && !viewModel.isHideLinkCard) { ContributionCard() DonateCard() LearnMoreCard() } } + + if (!viewModel.isExtendedDataLoaded) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + Spacer(Modifier.height(16.dp)) } } @@ -231,7 +271,8 @@ fun RebootDropdownItem(@StringRes id: Int, reason: String = "") { @Composable private fun TopBar( scrollBehavior: TopAppBarScrollBehavior? = null, - navigator: DestinationsNavigator + navigator: DestinationsNavigator, + isDataLoaded: Boolean = false ) { val context = LocalContext.current val colorScheme = MaterialTheme.colorScheme @@ -253,44 +294,47 @@ private fun TopBar( scrolledContainerColor = cardColor.copy(alpha = cardAlpha) ), actions = { - // SuSFS 配置按钮 - if (getSuSFS() == "Supported" && SuSFSManager.isBinaryAvailable(context)) { - IconButton(onClick = { - navigator.navigate(SuSFSConfigScreenDestination) - }) { - Icon( - imageVector = Icons.Filled.Tune, - contentDescription = stringResource(R.string.susfs_config_setting_title) - ) - } - } - - // 重启按钮 - var showDropdown by remember { mutableStateOf(false) } - KsuIsValid { - IconButton(onClick = { - showDropdown = true - }) { - Icon( - imageVector = Icons.Filled.PowerSettingsNew, - contentDescription = stringResource(id = R.string.reboot) - ) - - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false + if (isDataLoaded) { + // SuSFS 配置按钮 + val susfsVersion = getSuSFSVersion() + if (susfsVersion.isNotEmpty() && !susfsVersion.startsWith("[-]") && SuSFSManager.isBinaryAvailable(context)) { + IconButton(onClick = { + navigator.navigate(SuSFSConfigScreenDestination) }) { - RebootDropdownItem(id = R.string.reboot) + Icon( + imageVector = Icons.Filled.Tune, + contentDescription = stringResource(R.string.susfs_config_setting_title) + ) + } + } - val pm = - LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? - @Suppress("DEPRECATION") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { - RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace") + // 重启按钮 + var showDropdown by remember { mutableStateOf(false) } + KsuIsValid { + IconButton(onClick = { + showDropdown = true + }) { + Icon( + imageVector = Icons.Filled.PowerSettingsNew, + contentDescription = stringResource(id = R.string.reboot) + ) + + DropdownMenu(expanded = showDropdown, onDismissRequest = { + showDropdown = false + }) { + RebootDropdownItem(id = R.string.reboot) + + val pm = + LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? + @Suppress("DEPRECATION") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { + RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace") + } + RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery") + RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader") + RebootDropdownItem(id = R.string.reboot_download, reason = "download") + RebootDropdownItem(id = R.string.reboot_edl, reason = "edl") } - RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery") - RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader") - RebootDropdownItem(id = R.string.reboot_download, reason = "download") - RebootDropdownItem(id = R.string.reboot_edl, reason = "edl") } } } @@ -731,7 +775,7 @@ private fun InfoCard( systemInfo.seLinuxStatus, icon = Icons.Default.Security, ) - + if (!isHideZygiskImplement && !isSimpleMode && systemInfo.zygiskImplement != "None") { InfoCardItem( stringResource(R.string.home_zygisk_implement), @@ -741,7 +785,6 @@ private fun InfoCard( } if (!isSimpleMode) { - // 根据showKpmInfo决定是否显示KPM信息 if (lkmMode != true && !showKpmInfo) { val displayVersion = if (systemInfo.kpmVersion.isEmpty() || systemInfo.kpmVersion.startsWith("Error")) { @@ -785,21 +828,15 @@ private fun InfoCard( private fun SuSFSInfoText(systemInfo: HomeViewModel.SystemInfo): String = buildString { append(systemInfo.suSFSVersion) - val isSUS_SU = systemInfo.suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU" - val isKprobesHook = Natives.getHookType() == "Kprobes" - when { - isSUS_SU && isKprobesHook -> { - append(" (${systemInfo.suSFSVariant})") - if (systemInfo.susSUMode.isNotEmpty()) { - append(" ${stringResource(R.string.sus_su_mode)} ${systemInfo.susSUMode}") - } - } - Natives.getHookType() == "Manual" -> { append(" (${stringResource(R.string.manual_hook)})") } + Natives.getHookType() == "Inline" -> { + append(" (${stringResource(R.string.inline_hook)})") + } + else -> { append(" (${Natives.getHookType()})") } @@ -829,7 +866,7 @@ private fun StatusCardPreview() { StatusCard( HomeViewModel.SystemStatus( isManager = true, - ksuVersion = 20000, + ksuVersion = 40000, lkmMode = true, kernelVersion = KernelVersion(5, 10, 101), isRootAvailable = true @@ -858,6 +895,23 @@ private fun StatusCardPreview() { } } +@Composable +private fun IncompatibleKernelCard() { + val currentKver = remember { Natives.version } + val threshold = Natives.MINIMAL_NEW_IOCTL_KERNEL + + val msg = stringResource( + id = R.string.incompatible_kernel_msg, + currentKver, + threshold + ) + + WarningCard( + message = msg, + color = MaterialTheme.colorScheme.error + ) +} + @Preview @Composable private fun WarningCardPreview() { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt index 9f4fb7c..5b04b89 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt @@ -1,8 +1,10 @@ package com.sukisu.ultra.ui.screen import android.app.Activity +import android.content.Context import android.content.Intent import android.net.Uri +import android.provider.OpenableColumns import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -17,7 +19,9 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Input import androidx.compose.material.icons.filled.AutoFixHigh +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.FileUpload import androidx.compose.material.icons.filled.Security import androidx.compose.material3.* @@ -33,6 +37,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import com.maxkeppeker.sheets.core.models.base.Header import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeler.sheets.list.ListDialog @@ -47,7 +52,7 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import com.sukisu.ultra.R import com.sukisu.ultra.getKernelVersion import com.sukisu.ultra.ui.component.DialogHandle -import com.sukisu.ultra.ui.component.SlotSelectionDialog +import com.sukisu.ultra.ui.component.SuperDropdown import com.sukisu.ultra.ui.component.rememberConfirmDialog import com.sukisu.ultra.ui.component.rememberCustomDialog import com.sukisu.ultra.ui.theme.CardConfig @@ -56,6 +61,7 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation import com.sukisu.ultra.ui.util.* +import zako.zako.zako.zakoui.screen.kernelFlash.component.SlotSelectionDialog /** * @author ShirkNeko @@ -71,19 +77,49 @@ enum class KpmPatchOption { @OptIn(ExperimentalMaterial3Api::class) @Destination @Composable -fun InstallScreen(navigator: DestinationsNavigator) { +fun InstallScreen( + navigator: DestinationsNavigator, + preselectedKernelUri: String? = null +) { + val context = LocalContext.current var installMethod by remember { mutableStateOf(null) } var lkmSelection by remember { mutableStateOf(LkmSelection.KmiNone) } var kpmPatchOption by remember { mutableStateOf(KpmPatchOption.FOLLOW_KERNEL) } - val context = LocalContext.current var showRebootDialog by remember { mutableStateOf(false) } var showSlotSelectionDialog by remember { mutableStateOf(false) } + var showKpmPatchDialog by remember { mutableStateOf(false) } var tempKernelUri by remember { mutableStateOf(null) } + val kernelVersion = getKernelVersion() val isGKI = kernelVersion.isGKI() - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value val summary = stringResource(R.string.horizon_kernel_summary) + // 处理预选的内核文件 + LaunchedEffect(preselectedKernelUri) { + preselectedKernelUri?.let { uriString -> + try { + val preselectedUri = uriString.toUri() + val horizonMethod = InstallMethod.HorizonKernel( + uri = preselectedUri, + summary = summary + ) + installMethod = horizonMethod + tempKernelUri = preselectedUri + + if (isAbDevice) { + showSlotSelectionDialog = true + } else { + showKpmPatchDialog = true + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + if (showRebootDialog) { RebootDialog( show = true, @@ -103,6 +139,10 @@ fun InstallScreen(navigator: DestinationsNavigator) { ) } + var partitionSelectionIndex by remember { mutableIntStateOf(0) } + var partitionsState by remember { mutableStateOf>(emptyList()) } + var hasCustomSelected by remember { mutableStateOf(false) } + val onInstall = { installMethod?.let { method -> when (method) { @@ -119,10 +159,13 @@ fun InstallScreen(navigator: DestinationsNavigator) { } } else -> { + val isOta = method is InstallMethod.DirectInstallToInactiveSlot + val partitionSelection = partitionsState.getOrNull(partitionSelectionIndex) val flashIt = FlashIt.FlashBoot( boot = if (method is InstallMethod.SelectFile) method.uri else null, lkm = lkmSelection, - ota = method is InstallMethod.DirectInstallToInactiveSlot + ota = isOta, + partition = partitionSelection ) navigator.navigate(FlashScreenDestination(flashIt)) } @@ -143,6 +186,20 @@ fun InstallScreen(navigator: DestinationsNavigator) { summary = summary ) installMethod = horizonMethod + + if (preselectedKernelUri != null) { + showKpmPatchDialog = true + } + } + ) + + KpmPatchSelectionDialog( + show = showKpmPatchDialog, + currentOption = kpmPatchOption, + onDismiss = { showKpmPatchDialog = false }, + onOptionSelected = { option -> + kpmPatchOption = option + showKpmPatchDialog = false } ) @@ -165,6 +222,32 @@ fun InstallScreen(navigator: DestinationsNavigator) { } } + val selectLkmLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + it.data?.data?.let { uri -> + val isKo = isKoFile(context, uri) + if (isKo) { + lkmSelection = LkmSelection.LkmUri(uri) + } else { + lkmSelection = LkmSelection.KmiNone + Toast.makeText( + context, + context.getString(R.string.install_only_support_ko_file), + Toast.LENGTH_SHORT + ).show() + } + } + } + } + + val onLkmUpload = { + selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { + type = "application/octet-stream" + }) + } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) Scaffold( @@ -194,6 +277,7 @@ fun InstallScreen(navigator: DestinationsNavigator) { showSlotSelectionDialog = true } else { installMethod = method + showKpmPatchDialog = true } } else { installMethod = method @@ -204,32 +288,103 @@ fun InstallScreen(navigator: DestinationsNavigator) { selectedMethod = installMethod ) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) + // 选择LKM直接安装分区 + AnimatedVisibility( + visible = installMethod is InstallMethod.DirectInstall || installMethod is InstallMethod.DirectInstallToInactiveSlot, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut() ) { - (lkmSelection as? LkmSelection.LkmUri)?.let { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { ElevatedCard( colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), elevation = getCardElevation(), modifier = Modifier .fillMaxWidth() - .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) + .padding(bottom = 12.dp), ) { - Text( - text = stringResource( - id = R.string.selected_lkm, - it.uri.lastPathSegment ?: "(file)" - ), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(16.dp) + val isOta = installMethod is InstallMethod.DirectInstallToInactiveSlot + val suffix = produceState(initialValue = "", isOta) { + value = getSlotSuffix(isOta) + }.value + + val partitions = produceState(initialValue = emptyList()) { + value = getAvailablePartitions() + }.value + + val defaultPartition = produceState(initialValue = "") { + value = getDefaultPartition() + }.value + + partitionsState = partitions + val displayPartitions = partitions.map { name -> + if (defaultPartition == name) "$name (default)" else name + } + + val defaultIndex = partitions.indexOf(defaultPartition).takeIf { it >= 0 } ?: 0 + if (!hasCustomSelected) partitionSelectionIndex = defaultIndex + + SuperDropdown( + items = displayPartitions, + selectedIndex = partitionSelectionIndex, + title = "${stringResource(R.string.install_select_partition)} (${suffix})", + onSelectedIndexChange = { index -> + hasCustomSelected = true + partitionSelectionIndex = index + }, + leftAction = { + Icon( + Icons.Default.Edit, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(end = 16.dp), + contentDescription = null + ) + } + ) + } + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + if (isGKI) { + // 使用本地的LKM文件 + ElevatedCard( + colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), + elevation = getCardElevation(), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + ) { + ListItem( + headlineContent = { + Text(stringResource(id = R.string.install_upload_lkm_file)) + }, + supportingContent = { + (lkmSelection as? LkmSelection.LkmUri)?.let { + Text( + stringResource( + id = R.string.selected_lkm, + it.uri.lastPathSegment ?: "(file)" + ) + ) + } + }, + leadingContent = { + Icon( + Icons.AutoMirrored.Filled.Input, + contentDescription = null + ) + }, + modifier = Modifier + .fillMaxWidth() + .clickable { onLkmUpload() } ) } } @@ -242,12 +397,6 @@ fun InstallScreen(navigator: DestinationsNavigator) { modifier = Modifier .fillMaxWidth() .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) ) { Text( text = stringResource( @@ -269,12 +418,6 @@ fun InstallScreen(navigator: DestinationsNavigator) { modifier = Modifier .fillMaxWidth() .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) ) { Text( text = when (kpmPatchOption) { @@ -316,6 +459,47 @@ fun InstallScreen(navigator: DestinationsNavigator) { } } +@Composable +private fun KpmPatchSelectionDialog( + show: Boolean, + currentOption: KpmPatchOption, + onDismiss: () -> Unit, + onOptionSelected: (KpmPatchOption) -> Unit +) { + if (show) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.kpm_patch_options)) }, + text = { + Column { + Text( + text = stringResource(R.string.kpm_patch_description), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + + KpmPatchOptionGroup( + selectedOption = currentOption, + onOptionChanged = onOptionSelected + ) + } + }, + confirmButton = { + TextButton( + onClick = { onOptionSelected(currentOption) } + ) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + ) + } +} + @Composable private fun RebootDialog( show: Boolean, @@ -378,15 +562,15 @@ private fun SelectInstallMethod( selectedMethod: InstallMethod? = null ) { val rootAvailable = rootAvailable() - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value + val defaultPartitionName = produceState(initialValue = "boot") { + value = getDefaultPartition() + }.value val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary) val selectFileTip = stringResource( - id = R.string.select_file_tip, - if (isInitBoot()) { - "init_boot / vendor_boot ${stringResource(R.string.select_file_tip_vendor)}" - } else { - "boot" - } + id = R.string.select_file_tip, defaultPartitionName ) val radioOptions = mutableListOf( @@ -404,6 +588,10 @@ private fun SelectInstallMethod( var selectedOption by remember { mutableStateOf(null) } var currentSelectingMethod by remember { mutableStateOf(null) } + LaunchedEffect(selectedMethod) { + selectedOption = selectedMethod + } + val selectImageLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { @@ -479,7 +667,6 @@ private fun SelectInstallMethod( modifier = Modifier .fillMaxWidth() .padding(bottom = 16.dp) - .clip(MaterialTheme.shapes.large) ) { MaterialTheme( colorScheme = MaterialTheme.colorScheme.copy( @@ -518,7 +705,7 @@ private fun SelectInstallMethod( bottom = 16.dp ) ) { - radioOptions.take(3).forEach { option -> + radioOptions.filter { it !is InstallMethod.HorizonKernel }.forEach { option -> val interactionSource = remember { MutableInteractionSource() } Surface( color = if (option.javaClass == selectedOption?.javaClass) @@ -586,7 +773,6 @@ private fun SelectInstallMethod( modifier = Modifier .fillMaxWidth() .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.large) ) { MaterialTheme( colorScheme = MaterialTheme.colorScheme.copy( @@ -884,6 +1070,31 @@ private fun TopBar( ) } +private fun isKoFile(context: Context, uri: Uri): Boolean { + val seg = uri.lastPathSegment ?: "" + if (seg.endsWith(".ko", ignoreCase = true)) return true + + return try { + context.contentResolver.query( + uri, + arrayOf(OpenableColumns.DISPLAY_NAME), + null, + null, + null + )?.use { cursor -> + val idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (idx != -1 && cursor.moveToFirst()) { + val name = cursor.getString(idx) + name?.endsWith(".ko", ignoreCase = true) == true + } else { + false + } + } ?: false + } catch (_: Throwable) { + false + } +} + @Preview @Composable fun SelectInstallPreview() { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt new file mode 100644 index 0000000..929a8b2 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt @@ -0,0 +1,941 @@ +package com.sukisu.ultra.ui.screen + +import android.content.Context +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.* +import com.sukisu.ultra.ui.theme.CardConfig +import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha +import com.sukisu.ultra.ui.theme.getCardColors +import com.sukisu.ultra.ui.theme.getCardElevation +import com.sukisu.ultra.ui.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.* +import java.time.format.DateTimeFormatter +import android.os.Process.myUid +import androidx.core.content.edit + +private val SPACING_SMALL = 4.dp +private val SPACING_MEDIUM = 8.dp +private val SPACING_LARGE = 16.dp + +private const val PAGE_SIZE = 10000 +private const val MAX_TOTAL_LOGS = 100000 + +private const val LOGS_PATCH = "/data/adb/ksu/log/sulog.log" + +data class LogEntry( + val timestamp: String, + val type: LogType, + val uid: String, + val comm: String, + val details: String, + val pid: String, + val rawLine: String +) + +data class LogPageInfo( + val currentPage: Int = 0, + val totalPages: Int = 0, + val totalLogs: Int = 0, + val hasMore: Boolean = false +) + +enum class LogType(val displayName: String, val color: Color) { + SU_GRANT("SU_GRANT", Color(0xFF4CAF50)), + SU_EXEC("SU_EXEC", Color(0xFF2196F3)), + PERM_CHECK("PERM_CHECK", Color(0xFFFF9800)), + SYSCALL("SYSCALL", Color(0xFF00BCD4)), + MANAGER_OP("MANAGER_OP", Color(0xFF9C27B0)), + UNKNOWN("UNKNOWN", Color(0xFF757575)) +} + +enum class LogExclType(val displayName: String, val color: Color) { + CURRENT_APP("Current app", Color(0xFF9E9E9E)), + PRCTL_STAR("prctl_*", Color(0xFF00BCD4)), + PRCTL_UNKNOWN("prctl_unknown", Color(0xFF00BCD4)), + SETUID("setuid", Color(0xFF00BCD4)) +} + +private val utcFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") +private val localFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + +private fun saveExcludedSubTypes(context: Context, types: Set) { + val prefs = context.getSharedPreferences("sulog", Context.MODE_PRIVATE) + val nameSet = types.map { it.name }.toSet() + prefs.edit { putStringSet("excluded_subtypes", nameSet) } +} + +private fun loadExcludedSubTypes(context: Context): Set { + val prefs = context.getSharedPreferences("sulog", Context.MODE_PRIVATE) + val nameSet = prefs.getStringSet("excluded_subtypes", emptySet()) ?: emptySet() + return nameSet.mapNotNull { name -> + LogExclType.entries.firstOrNull { it.name == name } + }.toSet() +} + +@OptIn(ExperimentalMaterial3Api::class) +@Destination +@Composable +fun LogViewerScreen(navigator: DestinationsNavigator) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val snackBarHost = LocalSnackbarHost.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + + var logEntries by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(false) } + var filterType by rememberSaveable { mutableStateOf(null) } + var searchQuery by rememberSaveable { mutableStateOf("") } + var showSearchBar by rememberSaveable { mutableStateOf(false) } + var pageInfo by remember { mutableStateOf(LogPageInfo()) } + var lastLogFileHash by remember { mutableStateOf("") } + val currentUid = remember { myUid().toString() } + + val initialExcluded = remember { + loadExcludedSubTypes(context) + } + + var excludedSubTypes by rememberSaveable { mutableStateOf(initialExcluded) } + + LaunchedEffect(excludedSubTypes) { + saveExcludedSubTypes(context, excludedSubTypes) + } + + val filteredEntries = remember( + logEntries, filterType, searchQuery, excludedSubTypes + ) { + logEntries.filter { entry -> + val matchesSearch = searchQuery.isEmpty() || + entry.comm.contains(searchQuery, ignoreCase = true) || + entry.details.contains(searchQuery, ignoreCase = true) || + entry.uid.contains(searchQuery, ignoreCase = true) + + // 排除本应用 + if (LogExclType.CURRENT_APP in excludedSubTypes && entry.uid == currentUid) return@filter false + + // 排除 SYSCALL 子类型 + if (entry.type == LogType.SYSCALL) { + val detail = entry.details + if (LogExclType.PRCTL_STAR in excludedSubTypes && detail.startsWith("Syscall: prctl") && !detail.startsWith("Syscall: prctl_unknown")) return@filter false + if (LogExclType.PRCTL_UNKNOWN in excludedSubTypes && detail.startsWith("Syscall: prctl_unknown")) return@filter false + if (LogExclType.SETUID in excludedSubTypes && detail.startsWith("Syscall: setuid")) return@filter false + } + + // 普通类型筛选 + val matchesFilter = filterType == null || entry.type == filterType + matchesFilter && matchesSearch + } + } + + val loadingDialog = rememberLoadingDialog() + val confirmDialog = rememberConfirmDialog() + + val loadPage: (Int, Boolean) -> Unit = { page, forceRefresh -> + scope.launch { + if (isLoading) return@launch + + isLoading = true + try { + loadLogsWithPagination( + page, + forceRefresh, + lastLogFileHash + ) { entries, newPageInfo, newHash -> + logEntries = if (page == 0 || forceRefresh) { + entries + } else { + logEntries + entries + } + pageInfo = newPageInfo + lastLogFileHash = newHash + } + } finally { + isLoading = false + } + } + } + + val onManualRefresh: () -> Unit = { + loadPage(0, true) + } + + val loadNextPage: () -> Unit = { + if (pageInfo.hasMore && !isLoading) { + loadPage(pageInfo.currentPage + 1, false) + } + } + + LaunchedEffect(Unit) { + while (true) { + delay(5_000) + if (!isLoading) { + scope.launch { + val hasNewLogs = checkForNewLogs(lastLogFileHash) + if (hasNewLogs) { + loadPage(0, true) + } + } + } + } + } + + LaunchedEffect(Unit) { + loadPage(0, true) + } + + Scaffold( + topBar = { + LogViewerTopBar( + scrollBehavior = scrollBehavior, + onBackClick = { navigator.navigateUp() }, + showSearchBar = showSearchBar, + searchQuery = searchQuery, + onSearchQueryChange = { searchQuery = it }, + onSearchToggle = { showSearchBar = !showSearchBar }, + onRefresh = onManualRefresh, + onClearLogs = { + scope.launch { + val result = confirmDialog.awaitConfirm( + title = context.getString(R.string.log_viewer_clear_logs), + content = context.getString(R.string.log_viewer_clear_logs_confirm) + ) + if (result == ConfirmResult.Confirmed) { + loadingDialog.withLoading { + clearLogs() + loadPage(0, true) + } + snackBarHost.showSnackbar(context.getString(R.string.log_viewer_logs_cleared)) + } + } + } + ) + }, + snackbarHost = { SnackbarHost(snackBarHost) }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { + LogControlPanel( + filterType = filterType, + onFilterTypeSelected = { filterType = it }, + logCount = filteredEntries.size, + totalCount = logEntries.size, + pageInfo = pageInfo, + excludedSubTypes = excludedSubTypes, + onExcludeToggle = { excl -> + excludedSubTypes = if (excl in excludedSubTypes) + excludedSubTypes - excl + else + excludedSubTypes + excl + } + ) + + // 日志列表 + if (isLoading && logEntries.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else if (filteredEntries.isEmpty()) { + EmptyLogState( + hasLogs = logEntries.isNotEmpty(), + onRefresh = onManualRefresh + ) + } else { + LogList( + entries = filteredEntries, + pageInfo = pageInfo, + isLoading = isLoading, + onLoadMore = loadNextPage, + modifier = Modifier.fillMaxSize() + ) + } + } + } +} + +@Composable +private fun LogControlPanel( + filterType: LogType?, + onFilterTypeSelected: (LogType?) -> Unit, + logCount: Int, + totalCount: Int, + pageInfo: LogPageInfo, + excludedSubTypes: Set, + onExcludeToggle: (LogExclType) -> Unit +) { + var isExpanded by rememberSaveable { mutableStateOf(true) } + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), + colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow), + elevation = getCardElevation() + ) { + Column { + // 标题栏(点击展开/收起) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { isExpanded = !isExpanded } + .padding(SPACING_LARGE), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.settings), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + Icon( + imageVector = if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column( + modifier = Modifier.padding(horizontal = SPACING_LARGE) + ) { + // 类型过滤 + Text( + text = stringResource(R.string.log_viewer_filter_type), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + LazyRow(horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)) { + item { + FilterChip( + onClick = { onFilterTypeSelected(null) }, + label = { Text(stringResource(R.string.log_viewer_all_types)) }, + selected = filterType == null + ) + } + items(LogType.entries.toTypedArray()) { type -> + FilterChip( + onClick = { onFilterTypeSelected(if (filterType == type) null else type) }, + label = { Text(type.displayName) }, + selected = filterType == type, + leadingIcon = { + Box( + modifier = Modifier + .size(8.dp) + .background(type.color, RoundedCornerShape(4.dp)) + ) + } + ) + } + } + + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + + // 排除子类型 + Text( + text = stringResource(R.string.log_viewer_exclude_subtypes), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + LazyRow(horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)) { + items(LogExclType.entries.toTypedArray()) { excl -> + val label = if (excl == LogExclType.CURRENT_APP) + stringResource(R.string.log_viewer_exclude_current_app) + else excl.displayName + + FilterChip( + onClick = { onExcludeToggle(excl) }, + label = { Text(label) }, + selected = excl in excludedSubTypes, + leadingIcon = { + Box( + modifier = Modifier + .size(8.dp) + .background(excl.color, RoundedCornerShape(4.dp)) + ) + } + ) + } + } + + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + + // 统计信息 + Column(verticalArrangement = Arrangement.spacedBy(SPACING_SMALL)) { + Text( + text = stringResource(R.string.log_viewer_showing_entries, logCount, totalCount), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (pageInfo.totalPages > 0) { + Text( + text = stringResource( + R.string.log_viewer_page_info, + pageInfo.currentPage + 1, + pageInfo.totalPages, + pageInfo.totalLogs + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (pageInfo.totalLogs >= MAX_TOTAL_LOGS) { + Text( + text = stringResource(R.string.log_viewer_too_many_logs, MAX_TOTAL_LOGS), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + } + } + + Spacer(modifier = Modifier.height(SPACING_LARGE)) + } + } + } + } +} + +@Composable +private fun LogList( + entries: List, + pageInfo: LogPageInfo, + isLoading: Boolean, + onLoadMore: () -> Unit, + modifier: Modifier = Modifier +) { + val listState = rememberLazyListState() + + LazyColumn( + state = listState, + modifier = modifier, + contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), + verticalArrangement = Arrangement.spacedBy(SPACING_SMALL) + ) { + items(entries) { entry -> + LogEntryCard(entry = entry) + } + + // 加载更多按钮或加载指示器 + if (pageInfo.hasMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(SPACING_LARGE), + contentAlignment = Alignment.Center + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp) + ) + } else { + Button( + onClick = onLoadMore, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = Icons.Filled.ExpandMore, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(SPACING_MEDIUM)) + Text(stringResource(R.string.log_viewer_load_more)) + } + } + } + } + } else if (entries.isNotEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(SPACING_LARGE), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.log_viewer_all_logs_loaded), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} + +@Composable +private fun LogEntryCard(entry: LogEntry) { + var expanded by remember { mutableStateOf(false) } + + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded }, + colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Column( + modifier = Modifier.padding(SPACING_MEDIUM) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM) + ) { + Box( + modifier = Modifier + .size(12.dp) + .background(entry.type.color, RoundedCornerShape(6.dp)) + ) + Text( + text = entry.type.displayName, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold + ) + } + Text( + text = entry.timestamp, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(modifier = Modifier.height(SPACING_SMALL)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "UID: ${entry.uid}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "PID: ${entry.pid}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Text( + text = entry.comm, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + maxLines = if (expanded) Int.MAX_VALUE else 1, + overflow = TextOverflow.Ellipsis + ) + + if (entry.details.isNotEmpty()) { + Spacer(modifier = Modifier.height(SPACING_SMALL)) + Text( + text = entry.details, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = if (expanded) Int.MAX_VALUE else 2, + overflow = TextOverflow.Ellipsis + ) + } + + AnimatedVisibility( + visible = expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Column { + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + Text( + text = stringResource(R.string.log_viewer_raw_log), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(SPACING_SMALL)) + Text( + text = entry.rawLine, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} + +@Composable +private fun EmptyLogState( + hasLogs: Boolean, + onRefresh: () -> Unit +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(SPACING_LARGE) + ) { + Icon( + imageVector = if (hasLogs) Icons.Filled.FilterList else Icons.Filled.Description, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource( + if (hasLogs) R.string.log_viewer_no_matching_logs + else R.string.log_viewer_no_logs + ), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Button(onClick = onRefresh) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(SPACING_MEDIUM)) + Text(stringResource(R.string.log_viewer_refresh)) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun LogViewerTopBar( + scrollBehavior: TopAppBarScrollBehavior? = null, + onBackClick: () -> Unit, + showSearchBar: Boolean, + searchQuery: String, + onSearchQueryChange: (String) -> Unit, + onSearchToggle: () -> Unit, + onRefresh: () -> Unit, + onClearLogs: () -> Unit +) { + val colorScheme = MaterialTheme.colorScheme + val cardColor = if (CardConfig.isCustomBackgroundEnabled) { + colorScheme.surfaceContainerLow + } else { + colorScheme.background + } + + Column { + TopAppBar( + title = { + Text( + text = stringResource(R.string.log_viewer_title), + style = MaterialTheme.typography.titleLarge + ) + }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.log_viewer_back) + ) + } + }, + actions = { + IconButton(onClick = onSearchToggle) { + Icon( + imageVector = if (showSearchBar) Icons.Filled.SearchOff else Icons.Filled.Search, + contentDescription = stringResource(R.string.log_viewer_search) + ) + } + IconButton(onClick = onRefresh) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringResource(R.string.log_viewer_refresh) + ) + } + IconButton(onClick = onClearLogs) { + Icon( + imageVector = Icons.Filled.DeleteSweep, + contentDescription = stringResource(R.string.log_viewer_clear_logs) + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = cardColor.copy(alpha = cardAlpha), + scrolledContainerColor = cardColor.copy(alpha = cardAlpha) + ), + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior + ) + + AnimatedVisibility( + visible = showSearchBar, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + OutlinedTextField( + value = searchQuery, + onValueChange = onSearchQueryChange, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), + placeholder = { Text(stringResource(R.string.log_viewer_search_placeholder)) }, + leadingIcon = { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null + ) + }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { onSearchQueryChange("") }) { + Icon( + imageVector = Icons.Filled.Clear, + contentDescription = stringResource(R.string.log_viewer_clear_search) + ) + } + } + }, + singleLine = true + ) + } + } +} + +private suspend fun checkForNewLogs( + lastHash: String +): Boolean { + return withContext(Dispatchers.IO) { + try { + val shell = getRootShell() + val logPath = "/data/adb/ksu/log/sulog.log" + + val result = runCmd(shell, "stat -c '%Y %s' $logPath 2>/dev/null || echo '0 0'") + val currentHash = result.trim() + + currentHash != lastHash && currentHash != "0 0" + } catch (_: Exception) { + false + } + } +} + +private suspend fun loadLogsWithPagination( + page: Int, + forceRefresh: Boolean, + lastHash: String, + onLoaded: (List, LogPageInfo, String) -> Unit +) { + withContext(Dispatchers.IO) { + try { + val shell = getRootShell() + + // 获取文件信息 + val statResult = runCmd(shell, "stat -c '%Y %s' $LOGS_PATCH 2>/dev/null || echo '0 0'") + val currentHash = statResult.trim() + + if (!forceRefresh && currentHash == lastHash && currentHash != "0 0") { + withContext(Dispatchers.Main) { + onLoaded(emptyList(), LogPageInfo(), currentHash) + } + return@withContext + } + + // 获取总行数 + val totalLinesResult = runCmd(shell, "wc -l < $LOGS_PATCH 2>/dev/null || echo '0'") + val totalLines = totalLinesResult.trim().toIntOrNull() ?: 0 + + if (totalLines == 0) { + withContext(Dispatchers.Main) { + onLoaded(emptyList(), LogPageInfo(), currentHash) + } + return@withContext + } + + // 限制最大日志数量 + val effectiveTotal = minOf(totalLines, MAX_TOTAL_LOGS) + val totalPages = (effectiveTotal + PAGE_SIZE - 1) / PAGE_SIZE + + // 计算要读取的行数范围 + val startLine = if (page == 0) { + maxOf(1, totalLines - effectiveTotal + 1) + } else { + val skipLines = page * PAGE_SIZE + maxOf(1, totalLines - effectiveTotal + 1 + skipLines) + } + + val endLine = minOf(startLine + PAGE_SIZE - 1, totalLines) + + if (startLine > totalLines) { + withContext(Dispatchers.Main) { + onLoaded(emptyList(), LogPageInfo(page, totalPages, effectiveTotal, false), currentHash) + } + return@withContext + } + + val result = runCmd(shell, "sed -n '${startLine},${endLine}p' $LOGS_PATCH 2>/dev/null || echo ''") + val entries = parseLogEntries(result) + + val hasMore = endLine < totalLines + val pageInfo = LogPageInfo(page, totalPages, effectiveTotal, hasMore) + + withContext(Dispatchers.Main) { + onLoaded(entries, pageInfo, currentHash) + } + } catch (_: Exception) { + withContext(Dispatchers.Main) { + onLoaded(emptyList(), LogPageInfo(), lastHash) + } + } + } +} + +private suspend fun clearLogs() { + withContext(Dispatchers.IO) { + try { + val shell = getRootShell() + runCmd(shell, "echo '' > $LOGS_PATCH") + } catch (_: Exception) { + } + } +} + +private fun parseLogEntries(logContent: String): List { + if (logContent.isBlank()) return emptyList() + + val entries = logContent.lines() + .filter { it.isNotBlank() && it.startsWith("[") } + .mapNotNull { line -> + try { + parseLogLine(line) + } catch (_: Exception) { + null + } + } + + return entries.reversed() +} +private fun utcToLocal(utc: String): String { + return try { + val instant = LocalDateTime.parse(utc, utcFormatter).atOffset(ZoneOffset.UTC).toInstant() + val local = instant.atZone(ZoneId.systemDefault()) + local.format(localFormatter) + } catch (_: Exception) { + utc + } +} + +private fun parseLogLine(line: String): LogEntry? { + // 解析格式: [timestamp] TYPE: UID=xxx COMM=xxx ... + val timestampRegex = """\[(.*?)]""".toRegex() + val timestampMatch = timestampRegex.find(line) ?: return null + val timestamp = utcToLocal(timestampMatch.groupValues[1]) + + val afterTimestamp = line.substring(timestampMatch.range.last + 1).trim() + val parts = afterTimestamp.split(":") + if (parts.size < 2) return null + + val typeStr = parts[0].trim() + val type = when (typeStr) { + "SU_GRANT" -> LogType.SU_GRANT + "SU_EXEC" -> LogType.SU_EXEC + "PERM_CHECK" -> LogType.PERM_CHECK + "SYSCALL" -> LogType.SYSCALL + "MANAGER_OP" -> LogType.MANAGER_OP + else -> LogType.UNKNOWN + } + + val details = parts[1].trim() + val uid: String = extractValue(details, "UID") ?: "" + val comm: String = extractValue(details, "COMM") ?: "" + val pid: String = extractValue(details, "PID") ?: "" + + // 构建详细信息字符串 + val detailsStr = when (type) { + LogType.SU_GRANT -> { + val method: String = extractValue(details, "METHOD") ?: "" + "Method: $method" + } + LogType.SU_EXEC -> { + val target: String = extractValue(details, "TARGET") ?: "" + val result: String = extractValue(details, "RESULT") ?: "" + "Target: $target, Result: $result" + } + LogType.PERM_CHECK -> { + val result: String = extractValue(details, "RESULT") ?: "" + "Result: $result" + } + LogType.SYSCALL -> { + val syscall = extractValue(details, "SYSCALL") ?: "" + val args = extractValue(details, "ARGS") ?: "" + "Syscall: $syscall, Args: $args" + } + LogType.MANAGER_OP -> { + val op: String = extractValue(details, "OP") ?: "" + val managerUid: String = extractValue(details, "MANAGER_UID") ?: "" + val targetUid: String = extractValue(details, "TARGET_UID") ?: "" + "Operation: $op, Manager UID: $managerUid, Target UID: $targetUid" + } + else -> details + } + + return LogEntry( + timestamp = timestamp, + type = type, + uid = uid, + comm = comm, + details = detailsStr, + pid = pid, + rawLine = line + ) +} + +private fun extractValue(text: String, key: String): String? { + val regex = """$key=(\S+)""".toRegex() + return regex.find(text)?.groupValues?.get(1) +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt index 3f4cead..65d8574 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt @@ -75,6 +75,10 @@ import com.sukisu.ultra.ui.component.* import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation import com.sukisu.ultra.ui.util.* +import com.sukisu.ultra.ui.util.module.ModuleModify +import com.sukisu.ultra.ui.util.module.ModuleOperationUtils +import com.sukisu.ultra.ui.util.module.ModuleUtils +import com.sukisu.ultra.ui.util.module.verifyModuleSignature import com.sukisu.ultra.ui.viewmodel.ModuleViewModel import com.sukisu.ultra.ui.webui.WebUIActivity import com.sukisu.ultra.ui.webui.WebUIXActivity @@ -84,7 +88,6 @@ import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit -// 菜单项数据类 data class ModuleBottomSheetMenuItem( val icon: ImageVector, val titleRes: Int, @@ -93,7 +96,7 @@ data class ModuleBottomSheetMenuItem( /** * @author ShirkNeko - * @date 2025/5/31. + * @date 2025/9/29. */ @SuppressLint("ResourceType", "AutoboxingStateCreation") @OptIn(ExperimentalMaterial3Api::class) @@ -102,24 +105,21 @@ data class ModuleBottomSheetMenuItem( fun ModuleScreen(navigator: DestinationsNavigator) { val viewModel = viewModel() val context = LocalContext.current - val prefs = context.getSharedPreferences("settings",MODE_PRIVATE) + val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) val snackBarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() val confirmDialog = rememberConfirmDialog() var lastClickTime by remember { mutableStateOf(0L) } - // 签名验证弹窗状态 var showSignatureDialog by remember { mutableStateOf(false) } var signatureDialogMessage by remember { mutableStateOf("") } var isForceVerificationFailed by remember { mutableStateOf(false) } var pendingInstallAction by remember { mutableStateOf<(() -> Unit)?>(null) } - // 初始化缓存系统 LaunchedEffect(Unit) { viewModel.initializeCache(context) } - // BottomSheet状态 val bottomSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true ) @@ -280,7 +280,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) { val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost) val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost) - LaunchedEffect(Unit) { if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) { viewModel.sortEnabledFirst = prefs.getBoolean("module_sort_enabled_first", false) @@ -291,7 +290,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) { val isSafeMode = Natives.isSafeMode val hasMagisk = hasMagisk() - val hideInstallButton = isSafeMode || hasMagisk val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) @@ -300,7 +298,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) { contract = ActivityResultContracts.StartActivityForResult() ) { viewModel.fetchModuleList() } - // BottomSheet菜单项 val bottomSheetMenuItems = remember { listOf( ModuleBottomSheetMenuItem( @@ -478,7 +475,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) { } } - // BottomSheet if (showBottomSheet) { ModalBottomSheet( onDismissRequest = { @@ -620,7 +616,6 @@ private fun ModuleBottomSheetContent( modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) ) - // 排序选项 Column( modifier = Modifier.padding(horizontal = 24.dp), verticalArrangement = Arrangement.spacedBy(8.dp) @@ -682,7 +677,6 @@ private fun ModuleBottomSheetContent( @Composable private fun ModuleBottomSheetMenuItemView(menuItem: ModuleBottomSheetMenuItem) { - // 添加交互状态 val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() @@ -810,7 +804,6 @@ private fun ModuleList( return } - // changelog is not empty, show it and wait for confirm val confirmResult = confirmDialog.awaitConfirm( changelogText, content = changelog, @@ -872,7 +865,7 @@ private fun ModuleList( ModuleOperationUtils.handleModuleUninstall(module.dirId) uninstallModule(module.dirId) } else { - restoreModule(module.dirId) + undoUninstallModule(module.dirId) } } } @@ -901,6 +894,7 @@ private fun ModuleList( reboot() } } + PullToRefreshBox( modifier = boxModifier, onRefresh = { @@ -1003,7 +997,6 @@ private fun ModuleList( } ) - // fix last item shadow incomplete in LazyColumn Spacer(Modifier.height(1.dp)) } } @@ -1011,7 +1004,6 @@ private fun ModuleList( } DownloadListener(context, onInstallModule) - } } @@ -1044,7 +1036,6 @@ fun ModuleItem( val indication = LocalIndication.current val viewModel = viewModel() - // 使用缓存系统获取模块大小 val sizeStr = remember(module.dirId) { viewModel.getModuleSize(module.dirId) } @@ -1152,10 +1143,8 @@ fun ModuleItem( modifier = Modifier .fillMaxWidth() .combinedClickable( - onClick = { - }, + onClick = { }, onLongClick = { - // 长按复制updateJson地址 val clipData = ClipData.newPlainText( "Update JSON URL", module.updateJson @@ -1163,7 +1152,6 @@ fun ModuleItem( clipboardManager.setPrimaryClip(clipData) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - // 显示复制成功的提示 Toast.makeText( context, context.getString(R.string.module_update_json_copied), @@ -1202,8 +1190,8 @@ fun ModuleItem( maxLines = 4, textDecoration = textDecoration, ) - if (!isHideTagRow) { + if (!isHideTagRow) { Spacer(modifier = Modifier.height(12.dp)) // 文件夹名称和大小标签 Row( @@ -1276,8 +1264,7 @@ fun ModuleItem( onClick = { onClick(module) }, interactionSource = interactionSource, contentPadding = ButtonDefaults.TextButtonContentPadding, - - ) { + ) { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.AutoMirrored.Outlined.Wysiwyg, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt index 1b3c7a0..ae56e6c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt @@ -2,6 +2,7 @@ package com.sukisu.ultra.ui.screen import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult @@ -16,6 +17,10 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Undo import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.rounded.EnhancedEncryption +import androidx.compose.material.icons.rounded.FolderDelete +import androidx.compose.material.icons.rounded.RemoveCircle +import androidx.compose.material.icons.rounded.RemoveModerator import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -36,6 +41,8 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination +import com.ramcosta.composedestinations.generated.destinations.LogViewerScreenDestination +import com.ramcosta.composedestinations.generated.destinations.UmountManagerScreenDestination import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.sukisu.ultra.BuildConfig @@ -46,12 +53,7 @@ import com.sukisu.ultra.ui.theme.CardConfig import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation -import com.sukisu.ultra.ui.util.LocalSnackbarHost -import com.sukisu.ultra.ui.util.getBugreportFile -import com.sukisu.ultra.ui.util.getRootShell -import com.sukisu.ultra.ui.util.setUidAutoScan -import com.sukisu.ultra.ui.util.setUidMultiUserScan -import com.topjohnwu.superuser.ShellUtils +import com.sukisu.ultra.ui.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -60,7 +62,7 @@ import java.time.format.DateTimeFormatter /** * @author ShirkNeko - * @date 2025/5/31. + * @date 2025/9/29. */ private val SPACING_SMALL = 3.dp private val SPACING_MEDIUM = 8.dp @@ -74,6 +76,7 @@ fun SettingScreen(navigator: DestinationsNavigator) { val snackBarHost = LocalSnackbarHost.current val context = LocalContext.current val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + var isSuLogEnabled by remember { mutableStateOf(Natives.isSuLogEnabled()) } var selectedEngine by rememberSaveable { mutableStateOf( prefs.getString("webui_engine", "default") ?: "default" @@ -81,7 +84,6 @@ fun SettingScreen(navigator: DestinationsNavigator) { } Scaffold( - // containerColor = MaterialTheme.colorScheme.surfaceBright, topBar = { TopBar(scrollBehavior = scrollBehavior) }, @@ -132,165 +134,266 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) + val modeItems = listOf( + stringResource(id = R.string.settings_mode_default), + stringResource(id = R.string.settings_mode_temp_enable), + stringResource(id = R.string.settings_mode_always_enable), + ) + var enhancedSecurityMode by rememberSaveable { + mutableIntStateOf( + run { + val currentEnabled = Natives.isEnhancedSecurityEnabled() + val savedPersist = prefs.getInt("enhanced_security_mode", 0) + if (savedPersist == 2) 2 else if (currentEnabled) 1 else 0 + } + ) + } + val enhancedStatus by produceState(initialValue = "") { + value = getFeatureStatus("enhanced_security") + } + val enhancedSummary = when (enhancedStatus) { + "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) + "managed" -> stringResource(id = R.string.feature_status_managed_summary) + else -> stringResource(id = R.string.settings_enable_enhanced_security_summary) + } + SuperDropdown( + icon = Icons.Rounded.EnhancedEncryption, + title = stringResource(id = R.string.settings_enable_enhanced_security), + summary = enhancedSummary, + items = modeItems, + leftAction = { + Icon( + Icons.Rounded.EnhancedEncryption, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_enable_enhanced_security), + tint = MaterialTheme.colorScheme.onBackground + ) + }, + enabled = enhancedStatus == "supported", + selectedIndex = enhancedSecurityMode, + onSelectedIndexChange = { index -> + when (index) { + // Default: disable and save to persist + 0 -> if (Natives.setEnhancedSecurityEnabled(false)) { + execKsud("feature save", true) + prefs.edit { putInt("enhanced_security_mode", 0) } + enhancedSecurityMode = 0 + } + + // Temporarily enable: save disabled state first, then enable + 1 -> if (Natives.setEnhancedSecurityEnabled(false)) { + execKsud("feature save", true) + if (Natives.setEnhancedSecurityEnabled(true)) { + prefs.edit { putInt("enhanced_security_mode", 0) } + enhancedSecurityMode = 1 + } + } + + // Permanently enable: enable and save + 2 -> if (Natives.setEnhancedSecurityEnabled(true)) { + execKsud("feature save", true) + prefs.edit { putInt("enhanced_security_mode", 2) } + enhancedSecurityMode = 2 + } + } + } + ) + + var suCompatMode by rememberSaveable { + mutableIntStateOf( + run { + val currentEnabled = Natives.isSuEnabled() + val savedPersist = prefs.getInt("su_compat_mode", 0) + if (savedPersist == 2) 2 else if (!currentEnabled) 1 else 0 + } + ) + } + val suStatus by produceState(initialValue = "") { + value = getFeatureStatus("su_compat") + } + val suSummary = when (suStatus) { + "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) + "managed" -> stringResource(id = R.string.feature_status_managed_summary) + else -> stringResource(id = R.string.settings_disable_su_summary) + } + SuperDropdown( + icon = Icons.Rounded.RemoveModerator, + title = stringResource(id = R.string.settings_disable_su), + summary = suSummary, + items = modeItems, + leftAction = { + Icon( + Icons.Rounded.RemoveModerator, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_disable_su), + tint = MaterialTheme.colorScheme.onBackground + ) + }, + enabled = suStatus == "supported", + selectedIndex = suCompatMode, + onSelectedIndexChange = { index -> + when (index) { + // Default: enable and save to persist + 0 -> if (Natives.setSuEnabled(true)) { + execKsud("feature save", true) + prefs.edit { putInt("su_compat_mode", 0) } + suCompatMode = 0 + } + + // Temporarily disable: save enabled state first, then disable + 1 -> if (Natives.setSuEnabled(true)) { + execKsud("feature save", true) + if (Natives.setSuEnabled(false)) { + prefs.edit { putInt("su_compat_mode", 0) } + suCompatMode = 1 + } + } + + // Permanently disable: disable and save + 2 -> if (Natives.setSuEnabled(false)) { + execKsud("feature save", true) + prefs.edit { putInt("su_compat_mode", 2) } + suCompatMode = 2 + } + } + } + ) + + var kernelUmountMode by rememberSaveable { + mutableIntStateOf( + run { + val currentEnabled = Natives.isKernelUmountEnabled() + val savedPersist = prefs.getInt("kernel_umount_mode", 0) + if (savedPersist == 2) 2 else if (!currentEnabled) 1 else 0 + } + ) + } + val umountStatus by produceState(initialValue = "") { + value = getFeatureStatus("kernel_umount") + } + val umountSummary = when (umountStatus) { + "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) + "managed" -> stringResource(id = R.string.feature_status_managed_summary) + else -> stringResource(id = R.string.settings_disable_kernel_umount_summary) + } + SuperDropdown( + icon = Icons.Rounded.RemoveCircle, + title = stringResource(id = R.string.settings_disable_kernel_umount), + summary = umountSummary, + items = modeItems, + leftAction = { + Icon( + Icons.Rounded.RemoveCircle, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_disable_kernel_umount), + tint = MaterialTheme.colorScheme.onBackground + ) + }, + enabled = umountStatus == "supported", + selectedIndex = kernelUmountMode, + onSelectedIndexChange = { index -> + when (index) { + // Default: enable and save to persist + 0 -> if (Natives.setKernelUmountEnabled(true)) { + execKsud("feature save", true) + prefs.edit { putInt("kernel_umount_mode", 0) } + kernelUmountMode = 0 + } + + // Temporarily disable: save enabled state first, then disable + 1 -> if (Natives.setKernelUmountEnabled(true)) { + execKsud("feature save", true) + if (Natives.setKernelUmountEnabled(false)) { + prefs.edit { putInt("kernel_umount_mode", 0) } + kernelUmountMode = 1 + } + } + + // Permanently disable: disable and save + 2 -> if (Natives.setKernelUmountEnabled(false)) { + execKsud("feature save", true) + prefs.edit { putInt("kernel_umount_mode", 2) } + kernelUmountMode = 2 + } + } + } + ) + + var suLogMode by rememberSaveable { + mutableIntStateOf( + run { + val currentEnabled = Natives.isSuLogEnabled() + val savedPersist = prefs.getInt("sulog_mode", 0) + if (savedPersist == 2) 2 else if (!currentEnabled) 1 else 0 + } + ) + } + val suLogStatus by produceState(initialValue = "") { + value = getFeatureStatus("sulog") + } + val suLogSummary = when (suLogStatus) { + "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) + "managed" -> stringResource(id = R.string.feature_status_managed_summary) + else -> stringResource(id = R.string.settings_disable_sulog_summary) + } + SuperDropdown( + title = stringResource(id = R.string.settings_disable_sulog), + summary = suLogSummary, + items = modeItems, + leftAction = { + Icon( + Icons.Rounded.RemoveCircle, + modifier = Modifier.padding(end = 16.dp), + contentDescription = stringResource(id = R.string.settings_disable_sulog), + tint = MaterialTheme.colorScheme.onBackground + ) + }, + enabled = suLogStatus == "supported", + selectedIndex = suLogMode, + onSelectedIndexChange = { index -> + when (index) { + // Default: enable and save to persist + 0 -> if (Natives.setSuLogEnabled(true)) { + execKsud("feature save", true) + prefs.edit { putInt("sulog_mode", 0) } + suLogMode = 0 + isSuLogEnabled = true + } + + // Temporarily disable: save enabled state first, then disable + 1 -> if (Natives.setSuLogEnabled(true)) { + execKsud("feature save", true) + if (Natives.setSuLogEnabled(false)) { + prefs.edit { putInt("sulog_mode", 0) } + suLogMode = 1 + isSuLogEnabled = false + } + } + + // Permanently disable: disable and save + 2 -> if (Natives.setSuLogEnabled(false)) { + execKsud("feature save", true) + prefs.edit { putInt("sulog_mode", 2) } + suLogMode = 2 + isSuLogEnabled = false + } + } + } + ) + // 卸载模块开关 - var umountChecked by rememberSaveable { - mutableStateOf(Natives.isDefaultUmountModules()) - } - + var umountChecked by rememberSaveable { mutableStateOf(Natives.isDefaultUmountModules()) } SwitchItem( - icon = Icons.Filled.FolderDelete, - title = stringResource(R.string.settings_umount_modules_default), - summary = stringResource(R.string.settings_umount_modules_default_summary), + icon = Icons.Rounded.FolderDelete, + title = stringResource(id = R.string.settings_umount_modules_default), + summary = stringResource(id = R.string.settings_umount_modules_default_summary), checked = umountChecked, - onCheckedChange = { enabled -> - if (Natives.setDefaultUmountModules(enabled)) { - umountChecked = enabled + onCheckedChange = { + if (Natives.setDefaultUmountModules(it)) { + umountChecked = it } } ) - - // SU 禁用开关 - if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) { - var isSuDisabled by rememberSaveable { - mutableStateOf(!Natives.isSuEnabled()) - } - SwitchItem( - icon = Icons.Filled.RemoveModerator, - title = stringResource(R.string.settings_disable_su), - summary = stringResource(R.string.settings_disable_su_summary), - checked = isSuDisabled, - onCheckedChange = { enabled -> - val shouldEnable = !enabled - if (Natives.setSuEnabled(shouldEnable)) { - isSuDisabled = enabled - } - } - ) - } - // 强制签名验证开关 - var forceSignatureVerification by rememberSaveable { - mutableStateOf(prefs.getBoolean("force_signature_verification", false)) - } - SwitchItem( - icon = Icons.Filled.Security, - title = stringResource(R.string.module_signature_verification), - summary = stringResource(R.string.module_signature_verification_summary), - checked = forceSignatureVerification, - onCheckedChange = { enabled -> - prefs.edit { putBoolean("force_signature_verification", enabled) } - forceSignatureVerification = enabled - } - ) - if (Natives.version >= Natives.MINIMAL_SUPPORTED_UID_SCANNER) { - var uidAutoScanEnabled by rememberSaveable { - mutableStateOf(prefs.getBoolean("uid_auto_scan", false)) - } - - var uidMultiUserScanEnabled by rememberSaveable { - mutableStateOf(prefs.getBoolean("uid_multi_user_scan", false)) - } - // 用户态扫描应用列表开关 - SwitchItem( - icon = Icons.Filled.Scanner, - title = stringResource(R.string.uid_auto_scan_title), - summary = stringResource(R.string.uid_auto_scan_summary), - checked = uidAutoScanEnabled, - onCheckedChange = { enabled -> - scope.launch { - try { - if (setUidAutoScan(enabled)) { - uidAutoScanEnabled = enabled - prefs.edit { putBoolean("uid_auto_scan", enabled) } - - if (!enabled) { - uidMultiUserScanEnabled = false - prefs.edit { putBoolean("uid_multi_user_scan", false) } - } - } else { - snackBarHost.showSnackbar(context.getString(R.string.uid_scanner_setting_failed)) - } - } catch (e: Exception) { - snackBarHost.showSnackbar( - context.getString( - R.string.uid_scanner_setting_error, - e.message ?: "" - ) - ) - } - } - } - ) - - // 多用户应用扫描开关 - 仅在启用用户态扫描时显示 - AnimatedVisibility( - visible = uidAutoScanEnabled, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - SwitchItem( - icon = Icons.Filled.Groups, - title = stringResource(R.string.uid_multi_user_scan_title), - summary = stringResource(R.string.uid_multi_user_scan_summary), - checked = uidMultiUserScanEnabled, - onCheckedChange = { enabled -> - scope.launch { - try { - if (setUidMultiUserScan(enabled)) { - uidMultiUserScanEnabled = enabled - prefs.edit { putBoolean("uid_multi_user_scan", enabled) } - } else { - snackBarHost.showSnackbar(context.getString(R.string.uid_scanner_setting_failed)) - } - } catch (e: Exception) { - snackBarHost.showSnackbar( - context.getString( - R.string.uid_scanner_setting_error, - e.message ?: "" - ) - ) - } - } - } - ) - } - // 清理运行环境 - AnimatedVisibility( - visible = uidAutoScanEnabled, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - val confirmDialog = rememberConfirmDialog() - val scope = rememberCoroutineScope() - - SettingItem( - icon = Icons.Filled.CleaningServices, - title = stringResource(R.string.clean_runtime_environment), - summary = stringResource(R.string.clean_runtime_environment_summary), - onClick = { - scope.launch { - val result = confirmDialog.awaitConfirm( - title = context.getString(R.string.clean_runtime_environment), - content = context.getString(R.string.clean_runtime_environment_confirm) - ) - if (result == ConfirmResult.Confirmed) { - val cleanResult = cleanRuntimeEnvironment() - if (cleanResult) { - uidAutoScanEnabled = false - prefs.edit { putBoolean("uid_auto_scan", false) } - - uidMultiUserScanEnabled = false - prefs.edit { putBoolean("uid_multi_user_scan", false) } - - snackBarHost.showSnackbar(context.getString(R.string.clean_runtime_environment_success)) - } else { - snackBarHost.showSnackbar(context.getString(R.string.clean_runtime_environment_failed)) - } - } - } - } - ) - } - } } ) } @@ -389,6 +492,33 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) + // 查看使用日志 + KsuIsValid { + if (isSuLogEnabled) { + SettingItem( + icon = Icons.Filled.Visibility, + title = stringResource(R.string.log_viewer_view_logs), + summary = stringResource(R.string.log_viewer_view_logs_summary), + onClick = { + navigator.navigate(LogViewerScreenDestination) + } + ) + } + } + val lkmMode = Natives.isLkmMode + KsuIsValid { + if (lkmMode) { + SettingItem( + icon = Icons.Filled.FolderOff, + title = stringResource(R.string.umount_path_manager), + summary = stringResource(R.string.umount_path_manager_summary), + onClick = { + navigator.navigate(UmountManagerScreenDestination) + } + ) + } + } + if (showBottomsheet) { LogBottomSheet( onDismiss = { showBottomsheet = false }, @@ -430,8 +560,6 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) } - - val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode if (lkmMode) { UninstallItem(navigator) { loadingDialog.withLoading(it) @@ -459,23 +587,6 @@ fun SettingScreen(navigator: DestinationsNavigator) { } } -fun cleanRuntimeEnvironment(): Boolean { - val shell = getRootShell() - return try { - try { - ShellUtils.fastCmd(shell, "/data/adb/uid_scanner stop") - } catch (_: Exception) { - } - ShellUtils.fastCmdResult(shell, "rm -rf /data/misc/user_uid") - ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/uid_scanner") - ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/ksu/bin/user_uid") - ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/service.d/uid_scanner.sh") - true - } catch (_: Exception) { - false - } -} - @Composable private fun SettingsGroupCard( title: String, @@ -781,7 +892,6 @@ enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle { return rememberCustomDialog { dismiss -> val options = listOf( - // UninstallType.TEMPORARY, UninstallType.PERMANENT, UninstallType.RESTORE_STOCK_IMAGE ) @@ -938,4 +1048,4 @@ private fun TopBar( windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt index e2434b7..d38d211 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt @@ -1,9 +1,12 @@ package com.sukisu.ultra.ui.screen import android.annotation.SuppressLint -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.* -import androidx.compose.foundation.background +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.expandVertically +import androidx.compose.animation.* +import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource @@ -22,25 +25,38 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.runtime.* +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage +import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import com.dergoogler.mmrl.ui.component.LabelItem import com.dergoogler.mmrl.ui.component.LabelItemDefaults @@ -53,64 +69,23 @@ import com.sukisu.ultra.R import com.sukisu.ultra.ui.component.FabMenuPresets import com.sukisu.ultra.ui.component.SearchAppBar import com.sukisu.ultra.ui.component.VerticalExpandableFab -import com.sukisu.ultra.ui.util.ModuleModify +import com.sukisu.ultra.ui.util.module.ModuleModify import com.sukisu.ultra.ui.viewmodel.AppCategory import com.sukisu.ultra.ui.viewmodel.SortType import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import java.io.File -// 应用优先级枚举 enum class AppPriority(val value: Int) { - ROOT(1), // root权限应用 - CUSTOM(2), // 自定义应用 - DEFAULT(3) // 默认应用 + ROOT(1), CUSTOM(2), DEFAULT(3) } -// 菜单项数据类 data class BottomSheetMenuItem( val icon: ImageVector, val titleRes: Int, val onClick: () -> Unit ) -/** - * 获取应用的优先级 - */ -private fun getAppPriority(app: SuperUserViewModel.AppInfo): AppPriority { - return when { - app.allowSu -> AppPriority.ROOT - app.hasCustomProfile -> AppPriority.CUSTOM - else -> AppPriority.DEFAULT - } -} - -/** - * 获取多选模式的主按钮图标 - */ -private fun getMultiSelectMainIcon(isExpanded: Boolean): ImageVector { - return if (isExpanded) { - Icons.Filled.Close - } else { - Icons.Filled.GridView - } -} - -/** - * 获取单选模式的主按钮图标 - */ -private fun getSingleSelectMainIcon(isExpanded: Boolean): ImageVector { - return if (isExpanded) { - Icons.Filled.Close - } else { - Icons.Filled.Add - } -} - -/** - * @author ShirkNeko - * @date 2025/6/8 - */ @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Destination @Composable @@ -122,231 +97,90 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { val context = LocalContext.current val snackBarHostState = remember { SnackbarHostState() } - // 使用ViewModel中的状态,这些状态现在都会从SharedPreferences中加载并自动保存 - val selectedCategory = viewModel.selectedCategory - val currentSortType = viewModel.currentSortType - - // BottomSheet状态 - val bottomSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ) + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var showBottomSheet by remember { mutableStateOf(false) } - // 添加备份和还原启动器 val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState) val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState) - LaunchedEffect(key1 = navigator) { + LaunchedEffect(navigator) { viewModel.search = "" - if (viewModel.appList.isEmpty()) { - // viewModel.fetchAppList() - } } - LaunchedEffect(viewModel.search) { - if (viewModel.search.isEmpty()) { - // 取消自动滚动到顶部的行为 - // listState.scrollToItem(0) - } - } - - // 监听选中应用的变化,如果在多选模式下没有选中任何应用,则自动退出多选模式 LaunchedEffect(viewModel.selectedApps, viewModel.showBatchActions) { if (viewModel.showBatchActions && viewModel.selectedApps.isEmpty()) { viewModel.showBatchActions = false } } - // 应用分类和排序逻辑 - val filteredAndSortedApps = remember( - viewModel.appList, - selectedCategory, - currentSortType, + val filteredAndSortedAppGroups = remember( + viewModel.appGroupList, + viewModel.selectedCategory, + viewModel.currentSortType, viewModel.search, viewModel.showSystemApps ) { - var apps = viewModel.appList + var groups = viewModel.appGroupList // 按分类筛选 - apps = when (selectedCategory) { - AppCategory.ALL -> apps - AppCategory.ROOT -> apps.filter { it.allowSu } - AppCategory.CUSTOM -> apps.filter { !it.allowSu && it.hasCustomProfile } - AppCategory.DEFAULT -> apps.filter { !it.allowSu && !it.hasCustomProfile } + groups = when (viewModel.selectedCategory) { + AppCategory.ALL -> groups + AppCategory.ROOT -> groups.filter { it.allowSu } + AppCategory.CUSTOM -> groups.filter { !it.allowSu && it.hasCustomProfile } + AppCategory.DEFAULT -> groups.filter { !it.allowSu && !it.hasCustomProfile } } - // 优先级排序 + 二次排序 - apps = apps.sortedWith { app1, app2 -> - val priority1 = getAppPriority(app1) - val priority2 = getAppPriority(app2) + // 排序 + groups.sortedWith { group1, group2 -> + val priority1 = when { + group1.allowSu -> AppPriority.ROOT + group1.hasCustomProfile -> AppPriority.CUSTOM + else -> AppPriority.DEFAULT + } + val priority2 = when { + group2.allowSu -> AppPriority.ROOT + group2.hasCustomProfile -> AppPriority.CUSTOM + else -> AppPriority.DEFAULT + } - // 首先按优先级排序 val priorityComparison = priority1.value.compareTo(priority2.value) - if (priorityComparison != 0) { priorityComparison } else { - // 在相同优先级内按指定排序方式排序 - when (currentSortType) { - SortType.NAME_ASC -> app1.label.lowercase().compareTo(app2.label.lowercase()) - SortType.NAME_DESC -> app2.label.lowercase().compareTo(app1.label.lowercase()) - SortType.INSTALL_TIME_NEW -> app2.packageInfo.firstInstallTime.compareTo(app1.packageInfo.firstInstallTime) - SortType.INSTALL_TIME_OLD -> app1.packageInfo.firstInstallTime.compareTo(app2.packageInfo.firstInstallTime) - SortType.SIZE_DESC -> { - val size1: Long = app1.packageInfo.applicationInfo?.let { - try { - File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length() - } catch (_: Exception) { - 0L - } - } ?: 0L - val size2: Long = app2.packageInfo.applicationInfo?.let { - try { - File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length() - } catch (_: Exception) { - 0L - } - } ?: 0L - size2.compareTo(size1) - } - SortType.SIZE_ASC -> { - val size1: Long = app1.packageInfo.applicationInfo?.let { - try { - File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length() - } catch (_: Exception) { - 0L - } - } ?: 0L - val size2: Long = app2.packageInfo.applicationInfo?.let { - try { - File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length() - } catch (_: Exception) { - 0L - } - } ?: 0L - size1.compareTo(size2) - } - SortType.USAGE_FREQ -> app1.label.lowercase().compareTo(app2.label.lowercase()) // 默认按名称排序 + when (viewModel.currentSortType) { + SortType.NAME_ASC -> group1.mainApp.label.lowercase() + .compareTo(group2.mainApp.label.lowercase()) + SortType.NAME_DESC -> group2.mainApp.label.lowercase() + .compareTo(group1.mainApp.label.lowercase()) + SortType.INSTALL_TIME_NEW -> group2.mainApp.packageInfo.firstInstallTime + .compareTo(group1.mainApp.packageInfo.firstInstallTime) + SortType.INSTALL_TIME_OLD -> group1.mainApp.packageInfo.firstInstallTime + .compareTo(group2.mainApp.packageInfo.firstInstallTime) + else -> group1.mainApp.label.lowercase() + .compareTo(group2.mainApp.label.lowercase()) } } } - - apps } - // 计算应用数量 - val appCounts = remember(viewModel.appList, viewModel.showSystemApps) { + val appCounts = remember(viewModel.appGroupList, viewModel.showSystemApps) { mapOf( - AppCategory.ALL to viewModel.appList.size, - AppCategory.ROOT to viewModel.appList.count { it.allowSu }, - AppCategory.CUSTOM to viewModel.appList.count { !it.allowSu && it.hasCustomProfile }, - AppCategory.DEFAULT to viewModel.appList.count { !it.allowSu && !it.hasCustomProfile } + AppCategory.ALL to viewModel.appGroupList.size, + AppCategory.ROOT to viewModel.appGroupList.count { it.allowSu }, + AppCategory.CUSTOM to viewModel.appGroupList.count { !it.allowSu && it.hasCustomProfile }, + AppCategory.DEFAULT to viewModel.appGroupList.count { !it.allowSu && !it.hasCustomProfile } ) } - // BottomSheet菜单项 - val bottomSheetMenuItems = remember(viewModel.showSystemApps) { - listOf( - BottomSheetMenuItem( - icon = Icons.Filled.Refresh, - titleRes = R.string.refresh, - onClick = { - scope.launch { - viewModel.fetchAppList() - bottomSheetState.hide() - showBottomSheet = false - } - } - ), - BottomSheetMenuItem( - icon = if (viewModel.showSystemApps) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, - titleRes = if (viewModel.showSystemApps) { - R.string.hide_system_apps - } else { - R.string.show_system_apps - }, - onClick = { - viewModel.updateShowSystemApps(!viewModel.showSystemApps) - scope.launch { - kotlinx.coroutines.delay(100) - bottomSheetState.hide() - showBottomSheet = false - } - } - ), - BottomSheetMenuItem( - icon = Icons.Filled.Save, - titleRes = R.string.backup_allowlist, - onClick = { - backupLauncher.launch(ModuleModify.createAllowlistBackupIntent()) - scope.launch { - bottomSheetState.hide() - showBottomSheet = false - } - } - ), - BottomSheetMenuItem( - icon = Icons.Filled.RestoreFromTrash, - titleRes = R.string.restore_allowlist, - onClick = { - restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent()) - scope.launch { - bottomSheetState.hide() - showBottomSheet = false - } - } - ) - ) - } - - // 记录FAB展开状态用于图标动画 - var isFabExpanded by remember { mutableStateOf(false) } - Scaffold( topBar = { SearchAppBar( - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text(stringResource(R.string.superuser)) - // 显示当前分类和应用数量 - if (selectedCategory != AppCategory.ALL) { - Surface( - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.primaryContainer, - modifier = Modifier.padding(start = 4.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = stringResource(selectedCategory.displayNameRes), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - Text( - text = "(${appCounts[selectedCategory] ?: 0})", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } - } - } - }, + title = { TopBarTitle(viewModel.selectedCategory, appCounts) }, searchText = viewModel.search, onSearchTextChange = { viewModel.search = it }, onClearClick = { viewModel.search = "" }, dropdownContent = { - IconButton( - onClick = { - showBottomSheet = true - }, - ) { + IconButton(onClick = { showBottomSheet = true }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = stringResource(id = R.string.settings), @@ -359,179 +193,344 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { snackbarHost = { SnackbarHost(snackBarHostState) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), floatingActionButton = { - VerticalExpandableFab( - menuItems = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { - FabMenuPresets.getBatchActionMenuItems( - onCancel = { - viewModel.selectedApps = emptySet() - viewModel.showBatchActions = false - }, - onDeny = { - scope.launch { - viewModel.updateBatchPermissions(false) - } - }, - onAllow = { - scope.launch { - viewModel.updateBatchPermissions(true) - } - }, - onUnmountModules = { - scope.launch { - viewModel.updateBatchPermissions( - allowSu = false, - umountModules = true - ) - } - }, - onDisableUnmount = { - scope.launch { - viewModel.updateBatchPermissions( - allowSu = false, - umountModules = false - ) - } - } - ) - } else { - FabMenuPresets.getScrollMenuItems( - onScrollToTop = { - scope.launch { - listState.animateScrollToItem(0) - } - }, - onScrollToBottom = { - scope.launch { - val lastIndex = filteredAndSortedApps.size - 1 - if (lastIndex >= 0) { - listState.animateScrollToItem(lastIndex) - } - } - } - ) - }, - buttonSpacing = 72.dp, - animationDurationMs = 300, - staggerDelayMs = 50, - // 根据模式选择不同的图标 - mainButtonIcon = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { - getMultiSelectMainIcon(isFabExpanded) - } else { - getSingleSelectMainIcon(isFabExpanded) - }, - mainButtonExpandedIcon = Icons.Filled.Close - ) + SuperUserFab(viewModel, filteredAndSortedAppGroups, listState, scope) } ) { innerPadding -> - PullToRefreshBox( - modifier = Modifier.padding(innerPadding), - onRefresh = { - scope.launch { viewModel.fetchAppList() } - }, - isRefreshing = viewModel.isRefreshing - ) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) + SuperUserContent( + innerPadding = innerPadding, + viewModel = viewModel, + filteredAndSortedAppGroups = filteredAndSortedAppGroups, + listState = listState, + scrollBehavior = scrollBehavior, + navigator = navigator, + scope = scope + ) + + if (showBottomSheet) { + SuperUserBottomSheet( + bottomSheetState = bottomSheetState, + onDismiss = { showBottomSheet = false }, + viewModel = viewModel, + appCounts = appCounts, + backupLauncher = backupLauncher, + restoreLauncher = restoreLauncher, + scope = scope, + listState = listState + ) + } + } +} + +@Composable +private fun TopBarTitle( + selectedCategory: AppCategory, + appCounts: Map +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(stringResource(R.string.superuser)) + + if (selectedCategory != AppCategory.ALL) { + Surface( + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.primaryContainer, + modifier = Modifier.padding(start = 4.dp) ) { - items(filteredAndSortedApps, key = { it.packageName + it.uid }) { app -> - AppItem( - app = app, - isSelected = viewModel.selectedApps.contains(app.packageName), - onToggleSelection = { viewModel.toggleAppSelection(app.packageName) }, + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(selectedCategory.displayNameRes), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + Text( + text = "(${appCounts[selectedCategory] ?: 0})", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + } + } +} + +@Composable +private fun SuperUserFab( + viewModel: SuperUserViewModel, + filteredAndSortedAppGroups: List, + listState: androidx.compose.foundation.lazy.LazyListState, + scope: CoroutineScope +) { + VerticalExpandableFab( + menuItems = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { + FabMenuPresets.getBatchActionMenuItems( + onCancel = { + viewModel.selectedApps = emptySet() + viewModel.showBatchActions = false + }, + onDeny = { scope.launch { viewModel.updateBatchPermissions(false) } }, + onAllow = { scope.launch { viewModel.updateBatchPermissions(true) } }, + onUnmountModules = { + scope.launch { viewModel.updateBatchPermissions( + allowSu = false, + umountModules = true + ) } + }, + onDisableUnmount = { + scope.launch { viewModel.updateBatchPermissions( + allowSu = false, + umountModules = false + ) } + } + ) + } else { + FabMenuPresets.getScrollMenuItems( + onScrollToTop = { scope.launch { listState.animateScrollToItem(0) } }, + onScrollToBottom = { + scope.launch { + val lastIndex = filteredAndSortedAppGroups.size - 1 + if (lastIndex >= 0) listState.animateScrollToItem(lastIndex) + } + } + ) + }, + mainButtonIcon = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { + Icons.Filled.GridView + } else { + Icons.Filled.Add + }, + mainButtonExpandedIcon = Icons.Filled.Close + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SuperUserContent( + innerPadding: PaddingValues, + viewModel: SuperUserViewModel, + filteredAndSortedAppGroups: List, + listState: androidx.compose.foundation.lazy.LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + navigator: DestinationsNavigator, + scope: CoroutineScope +) { + val expandedGroups = remember { mutableStateOf(setOf()) } + val density = LocalDensity.current + val targetSizePx = remember(density) { with(density) { 36.dp.roundToPx() } } + val context = LocalContext.current + + PullToRefreshBox( + modifier = Modifier.padding(innerPadding), + onRefresh = { scope.launch { viewModel.fetchAppList() } }, + isRefreshing = viewModel.isRefreshing + ) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { + filteredAndSortedAppGroups.forEachIndexed { _, appGroup -> + item(key = "${appGroup.uid}-${appGroup.mainApp.packageName}") { + AppGroupItem( + expandedGroups = expandedGroups, + appGroup = appGroup, + isSelected = appGroup.packageNames.any { viewModel.selectedApps.contains(it) }, + onToggleSelection = { + appGroup.packageNames.forEach { viewModel.toggleAppSelection(it) } + }, onClick = { if (viewModel.showBatchActions) { - viewModel.toggleAppSelection(app.packageName) + appGroup.packageNames.forEach { viewModel.toggleAppSelection(it) } + } else if (appGroup.apps.size > 1) { + expandedGroups.value = if (expandedGroups.value.contains(appGroup.uid)) { + expandedGroups.value - appGroup.uid + } else { + expandedGroups.value + appGroup.uid + } } else { - navigator.navigate(AppProfileScreenDestination(app)) + navigator.navigate(AppProfileScreenDestination(appGroup.mainApp)) } }, onLongClick = { if (!viewModel.showBatchActions) { viewModel.toggleBatchMode() - viewModel.toggleAppSelection(app.packageName) + appGroup.packageNames.forEach { viewModel.toggleAppSelection(it) } } }, viewModel = viewModel ) } - // 当没有应用显示时显示加载动画或空状态 - if (filteredAndSortedApps.isEmpty()) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .height(400.dp), - contentAlignment = Alignment.Center - ) { - // 根据加载状态显示不同内容 - if ((viewModel.isRefreshing || viewModel.appList.isEmpty()) && viewModel.search.isEmpty()) { - LoadingAnimation( - isLoading = true - ) - } else { - EmptyState( - selectedCategory = selectedCategory, - isSearchEmpty = viewModel.search.isNotEmpty() - ) - } - } - } - } - } - } + if (appGroup.apps.size <= 1) return@forEachIndexed - // BottomSheet - if (showBottomSheet) { - ModalBottomSheet( - onDismissRequest = { - showBottomSheet = false - }, - sheetState = bottomSheetState, - dragHandle = { - Surface( - modifier = Modifier.padding(vertical = 11.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), - shape = RoundedCornerShape(16.dp) - ) { - Box( - Modifier.size( - width = 32.dp, - height = 4.dp + items(appGroup.apps, key = { "${it.packageName}-${it.uid}" }) { app -> + val painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(context) + .data(app.packageInfo) + .size(targetSizePx) + .crossfade(true) + .build() + ) + + val listItemContent = remember(app.packageName, appGroup.uid) { + @Composable { + ListItem( + modifier = Modifier + .clickable { navigator.navigate(AppProfileScreenDestination(app)) } + .fillMaxWidth() + .padding(start = 10.dp), + headlineContent = { Text(app.label, style = MaterialTheme.typography.bodyMedium) }, + supportingContent = { Text(app.packageName, style = MaterialTheme.typography.bodySmall) }, + leadingContent = { + Image( + painter = painter, + contentDescription = app.label, + modifier = Modifier + .padding(4.dp) + .size(36.dp), + contentScale = ContentScale.Crop + ) + } ) - ) + } + } + + AnimatedVisibility( + visible = expandedGroups.value.contains(appGroup.uid), + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + listItemContent() } } - ) { - BottomSheetContent( - menuItems = bottomSheetMenuItems, - currentSortType = currentSortType, - onSortTypeChanged = { newSortType -> - viewModel.updateCurrentSortType(newSortType) - scope.launch { - bottomSheetState.hide() - showBottomSheet = false + } + + if (filteredAndSortedAppGroups.isEmpty()) { + item { + Box( + modifier = Modifier.fillMaxWidth().height(400.dp), + contentAlignment = Alignment.Center + ) { + if ((viewModel.isRefreshing || viewModel.appGroupList.isEmpty()) && viewModel.search.isEmpty()) { + LoadingAnimation(isLoading = true) + } else { + EmptyState( + selectedCategory = viewModel.selectedCategory, + isSearchEmpty = viewModel.search.isNotEmpty() + ) } - }, - selectedCategory = selectedCategory, - onCategorySelected = { newCategory -> - viewModel.updateSelectedCategory(newCategory) - scope.launch { - listState.animateScrollToItem(0) - bottomSheetState.hide() - showBottomSheet = false - } - }, - appCounts = appCounts - ) + } + } } } } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SuperUserBottomSheet( + bottomSheetState: SheetState, + onDismiss: () -> Unit, + viewModel: SuperUserViewModel, + appCounts: Map, + backupLauncher: androidx.activity.result.ActivityResultLauncher, + restoreLauncher: androidx.activity.result.ActivityResultLauncher, + scope: CoroutineScope, + listState: androidx.compose.foundation.lazy.LazyListState +) { + val bottomSheetMenuItems = remember(viewModel.showSystemApps) { + listOf( + BottomSheetMenuItem( + icon = Icons.Filled.Refresh, + titleRes = R.string.refresh, + onClick = { + scope.launch { + viewModel.fetchAppList() + bottomSheetState.hide() + onDismiss() + } + } + ), + BottomSheetMenuItem( + icon = if (viewModel.showSystemApps) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, + titleRes = if (viewModel.showSystemApps) R.string.hide_system_apps else R.string.show_system_apps, + onClick = { + viewModel.updateShowSystemApps(!viewModel.showSystemApps) + scope.launch { + kotlinx.coroutines.delay(100) + bottomSheetState.hide() + onDismiss() + } + } + ), + BottomSheetMenuItem( + icon = Icons.Filled.Save, + titleRes = R.string.backup_allowlist, + onClick = { + backupLauncher.launch(ModuleModify.createAllowlistBackupIntent()) + scope.launch { + bottomSheetState.hide() + onDismiss() + } + } + ), + BottomSheetMenuItem( + icon = Icons.Filled.RestoreFromTrash, + titleRes = R.string.restore_allowlist, + onClick = { + restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent()) + scope.launch { + bottomSheetState.hide() + onDismiss() + } + } + ) + ) + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = bottomSheetState, + dragHandle = { + Surface( + modifier = Modifier.padding(vertical = 11.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + shape = RoundedCornerShape(16.dp) + ) { + Box(Modifier.size(width = 32.dp, height = 4.dp)) + } + } + ) { + BottomSheetContent( + menuItems = bottomSheetMenuItems, + currentSortType = viewModel.currentSortType, + onSortTypeChanged = { newSortType -> + viewModel.updateCurrentSortType(newSortType) + scope.launch { + bottomSheetState.hide() + onDismiss() + } + }, + selectedCategory = viewModel.selectedCategory, + onCategorySelected = { newCategory -> + viewModel.updateSelectedCategory(newCategory) + scope.launch { + listState.animateScrollToItem(0) + bottomSheetState.hide() + onDismiss() + } + }, + appCounts = appCounts + ) + } +} + @Composable private fun BottomSheetContent( menuItems: List, @@ -542,11 +541,8 @@ private fun BottomSheetContent( appCounts: Map ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 24.dp) + modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp) ) { - // 标题 Text( text = stringResource(R.string.menu_options), style = MaterialTheme.typography.headlineSmall, @@ -554,7 +550,6 @@ private fun BottomSheetContent( modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) ) - // 菜单选项网格 LazyVerticalGrid( columns = GridCells.Fixed(4), modifier = Modifier.fillMaxWidth(), @@ -563,13 +558,10 @@ private fun BottomSheetContent( verticalArrangement = Arrangement.spacedBy(16.dp) ) { items(menuItems) { menuItem -> - BottomSheetMenuItemView( - menuItem = menuItem - ) + BottomSheetMenuItemView(menuItem = menuItem) } } - // 排序选项 Spacer(modifier = Modifier.height(24.dp)) HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp)) @@ -594,7 +586,6 @@ private fun BottomSheetContent( } } - // 应用分类选项 Spacer(modifier = Modifier.height(24.dp)) HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp)) @@ -648,10 +639,7 @@ private fun CategoryChip( modifier = modifier .fillMaxWidth() .scale(scale) - .clickable( - interactionSource = interactionSource, - indication = null - ) { onClick() }, + .clickable(interactionSource = interactionSource, indication = null) { onClick() }, shape = RoundedCornerShape(12.dp), color = if (isSelected) { MaterialTheme.colorScheme.primaryContainer @@ -661,13 +649,10 @@ private fun CategoryChip( tonalElevation = if (isSelected) 4.dp else 0.dp ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - // 分类信息行 Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -687,7 +672,6 @@ private fun CategoryChip( overflow = TextOverflow.Ellipsis ) - // 选中指示器 AnimatedVisibility( visible = isSelected, enter = scaleIn() + fadeIn(), @@ -702,7 +686,6 @@ private fun CategoryChip( } } - // 应用数量 Text( text = "$appCount apps", style = MaterialTheme.typography.labelSmall, @@ -718,7 +701,6 @@ private fun CategoryChip( @Composable private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) { - // 添加交互状态 val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() @@ -735,10 +717,7 @@ private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) { modifier = Modifier .fillMaxWidth() .scale(scale) - .clickable( - interactionSource = interactionSource, - indication = null - ) { menuItem.onClick() } + .clickable(interactionSource = interactionSource, indication = null) { menuItem.onClick() } .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -748,9 +727,7 @@ private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) { color = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer ) { - Box( - contentAlignment = Alignment.Center - ) { + Box(contentAlignment = Alignment.Center) { Icon( imageVector = menuItem.icon, contentDescription = stringResource(menuItem.titleRes), @@ -770,40 +747,129 @@ private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) { } } +@Composable +private fun LoadingAnimation( + modifier: Modifier = Modifier, + isLoading: Boolean = true +) { + val infiniteTransition = rememberInfiniteTransition(label = "loading") + + val alpha by infiniteTransition.animateFloat( + initialValue = 0.3f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(600, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse + ), + label = "alpha" + ) + + AnimatedVisibility( + visible = isLoading, + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + modifier = modifier + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + LinearProgressIndicator( + modifier = Modifier.width(200.dp).height(4.dp), + color = MaterialTheme.colorScheme.primary.copy(alpha = alpha), + trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) + ) + } + } +} + +@Composable +@SuppressLint("ModifierParameter") +private fun EmptyState( + selectedCategory: AppCategory, + modifier: Modifier = Modifier, + isSearchEmpty: Boolean = false +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier + ) { + Icon( + imageVector = if (isSearchEmpty) Icons.Filled.SearchOff else Icons.Filled.Archive, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), + modifier = Modifier.size(96.dp).padding(bottom = 16.dp) + ) + Text( + text = if (isSearchEmpty || selectedCategory == AppCategory.ALL) { + stringResource(R.string.no_apps_found) + } else { + stringResource(R.string.no_apps_in_category) + }, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + ) + } +} + @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable -private fun AppItem( - app: SuperUserViewModel.AppInfo, +private fun AppGroupItem( + appGroup: SuperUserViewModel.AppGroup, isSelected: Boolean, onToggleSelection: () -> Unit, onClick: () -> Unit, onLongClick: () -> Unit, - viewModel: SuperUserViewModel + viewModel: SuperUserViewModel, + expandedGroups: MutableState> ) { + val mainApp = appGroup.mainApp + ListItem( - modifier = Modifier - .pointerInput(Unit) { - detectTapGestures( - onLongPress = { onLongClick() }, - onTap = { onClick() } - ) - }, - headlineContent = { Text(app.label) }, + modifier = Modifier.pointerInput(Unit) { + detectTapGestures( + onLongPress = { onLongClick() }, + onTap = { onClick() } + ) + }, + headlineContent = { + Text(mainApp.label) + }, supportingContent = { Column { - Text(app.packageName) + val summaryText = if (appGroup.apps.size > 1) { + stringResource(R.string.group_contains_apps, appGroup.apps.size) + } else { + mainApp.packageName + } - Spacer(modifier = Modifier.height(4.dp)) - - FlowRow( - horizontalArrangement = Arrangement.spacedBy(4.dp) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween ) { - if (app.allowSu) { - LabelItem( - text = "ROOT", + Text(summaryText) + + if (appGroup.apps.size > 1) { + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.rotate( + animateFloatAsState( + targetValue = if (expandedGroups.value.contains(appGroup.uid)) 180f else 0f, + animationSpec = tween(200, easing = LinearOutSlowInEasing), + label = "" + ).value + ) ) + } + } + + FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + if (appGroup.allowSu) { + LabelItem(text = "ROOT") } else { - if (Natives.uidShouldUmount(app.uid)) { + if (Natives.uidShouldUmount(appGroup.uid)) { LabelItem( text = "UMOUNT", style = LabelItemDefaults.style.copy( @@ -813,15 +879,15 @@ private fun AppItem( ) } } - if (app.hasCustomProfile) { + if (appGroup.hasCustomProfile) { LabelItem( text = "CUSTOM", style = LabelItemDefaults.style.copy( - containerColor = MaterialTheme.colorScheme.onTertiary, + containerColor = MaterialTheme.colorScheme.tertiaryContainer, contentColor = MaterialTheme.colorScheme.onTertiaryContainer, ) ) - } else if (!app.allowSu) { + } else if (!appGroup.allowSu) { LabelItem( text = "DEFAULT", style = LabelItemDefaults.style.copy( @@ -829,24 +895,42 @@ private fun AppItem( ) ) } + if (appGroup.apps.size > 1) { + appGroup.userName?.let { + LabelItem( + text = it, + style = LabelItemDefaults.style.copy( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) + ) + } + } } } }, leadingContent = { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(app.packageInfo) + .data(mainApp.packageInfo) .crossfade(true) .build(), - contentDescription = app.label, - modifier = Modifier - .padding(4.dp) - .width(48.dp) - .height(48.dp) + contentDescription = mainApp.label, + modifier = Modifier.padding(4.dp).width(48.dp).height(48.dp) ) }, trailingContent = { - if (viewModel.showBatchActions) { + AnimatedVisibility( + visible = viewModel.showBatchActions, + enter = fadeIn(animationSpec = tween(200)) + scaleIn( + animationSpec = tween(200), + initialScale = 0.6f + ), + exit = fadeOut(animationSpec = tween(200)) + scaleOut( + animationSpec = tween(200), + targetScale = 0.6f + ) + ) { val checkboxInteractionSource = remember { MutableInteractionSource() } val isCheckboxPressed by checkboxInteractionSource.collectIsPressedAsState() @@ -874,103 +958,4 @@ private fun AppItem( } } ) -} - -@Composable -fun LabelText(label: String) { - Box( - modifier = Modifier - .padding(top = 4.dp, end = 4.dp) - .background( - Color.Black, - shape = RoundedCornerShape(4.dp) - ) - ) { - Text( - text = label, - modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp), - style = TextStyle( - fontSize = 8.sp, - color = Color.White, - ) - ) - } -} - -/** - * 加载动画组件 - */ -@Composable -private fun LoadingAnimation( - modifier: Modifier = Modifier, - isLoading: Boolean = true -) { - val infiniteTransition = rememberInfiniteTransition(label = "loading") - - // 透明度动画 - val alpha by infiniteTransition.animateFloat( - initialValue = 0.3f, - targetValue = 1f, - animationSpec = infiniteRepeatable( - animation = tween(600, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse - ), - label = "alpha" - ) - - AnimatedVisibility( - visible = isLoading, - enter = fadeIn() + scaleIn(), - exit = fadeOut() + scaleOut(), - modifier = modifier - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - // 进度指示器 - LinearProgressIndicator( - modifier = Modifier - .width(200.dp) - .height(4.dp), - color = MaterialTheme.colorScheme.primary.copy(alpha = alpha), - trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) - ) - } - } -} - -/** - * 空状态组件 - */ -@Composable -@SuppressLint("ModifierParameter") -private fun EmptyState( - selectedCategory: AppCategory, - modifier: Modifier = Modifier, - isSearchEmpty: Boolean = false -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = modifier - ) { - Icon( - imageVector = if (isSearchEmpty) Icons.Filled.SearchOff else Icons.Filled.Archive, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), - modifier = Modifier - .size(96.dp) - .padding(bottom = 16.dp) - ) - Text( - text = if (isSearchEmpty || selectedCategory == AppCategory.ALL) { - stringResource(R.string.no_apps_found) - } else { - stringResource(R.string.no_apps_in_category) - }, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - ) - } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt index 41eb32f..6aa1def 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt @@ -3,10 +3,12 @@ package com.sukisu.ultra.ui.screen import android.content.ClipData import android.content.ClipboardManager import android.widget.Toast +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -17,10 +19,13 @@ import androidx.compose.material3.* import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.core.content.getSystemService import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel @@ -253,4 +258,25 @@ private fun TopBar( windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) -} \ No newline at end of file +} + +@Composable +fun LabelText(label: String) { + Box( + modifier = Modifier + .padding(top = 4.dp, end = 4.dp) + .background( + Color.Black, + shape = RoundedCornerShape(4.dp) + ) + ) { + Text( + text = label, + modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp), + style = TextStyle( + fontSize = 8.sp, + color = Color.White, + ) + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt new file mode 100644 index 0000000..a93de1d --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt @@ -0,0 +1,410 @@ +package com.sukisu.ultra.ui.screen + +import android.content.Context +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.rememberConfirmDialog +import com.sukisu.ultra.ui.component.ConfirmResult +import com.sukisu.ultra.ui.theme.CardConfig +import com.sukisu.ultra.ui.theme.getCardColors +import com.sukisu.ultra.ui.theme.getCardElevation +import com.sukisu.ultra.ui.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +private val SPACING_SMALL = 3.dp +private val SPACING_MEDIUM = 8.dp +private val SPACING_LARGE = 16.dp + +data class UmountPathEntry( + val path: String, + val flags: Int, +) + +@OptIn(ExperimentalMaterial3Api::class) +@Destination +@Composable +fun UmountManagerScreen(navigator: DestinationsNavigator) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val snackBarHost = LocalSnackbarHost.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + val confirmDialog = rememberConfirmDialog() + + var pathList by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(false) } + var showAddDialog by remember { mutableStateOf(false) } + + fun loadPaths() { + scope.launch(Dispatchers.IO) { + isLoading = true + val result = listUmountPaths() + val entries = parseUmountPaths(result) + withContext(Dispatchers.Main) { + pathList = entries + isLoading = false + } + } + } + + LaunchedEffect(Unit) { + loadPaths() + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.umount_path_manager)) }, + navigationIcon = { + IconButton(onClick = { navigator.navigateUp() }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + actions = { + IconButton(onClick = { loadPaths() }) { + Icon(Icons.Filled.Refresh, contentDescription = null) + } + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy( + alpha = CardConfig.cardAlpha + ) + ) + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { showAddDialog = true } + ) { + Icon(Icons.Filled.Add, contentDescription = null) + } + }, + snackbarHost = { SnackbarHost(snackBarHost) } + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(SPACING_LARGE), + colors = getCardColors(MaterialTheme.colorScheme.primaryContainer), + elevation = getCardElevation() + ) { + Column( + modifier = Modifier.padding(SPACING_LARGE) + ) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + Text( + text = stringResource(R.string.umount_path_restart_notice), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + if (isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), + verticalArrangement = Arrangement.spacedBy(SPACING_MEDIUM) + ) { + items(pathList, key = { it.path }) { entry -> + UmountPathCard( + entry = entry, + onDelete = { + scope.launch(Dispatchers.IO) { + val success = removeUmountPath(entry.path) + withContext(Dispatchers.Main) { + if (success) { + snackBarHost.showSnackbar( + context.getString(R.string.umount_path_removed) + ) + loadPaths() + } else { + snackBarHost.showSnackbar( + context.getString(R.string.operation_failed) + ) + } + } + } + } + ) + } + + item { + Spacer(modifier = Modifier.height(SPACING_LARGE)) + } + + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SPACING_LARGE), + horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM) + ) { + Button( + onClick = { + scope.launch { + if (confirmDialog.awaitConfirm( + title = context.getString(R.string.confirm_action), + content = context.getString(R.string.confirm_clear_custom_paths) + ) == ConfirmResult.Confirmed) { + withContext(Dispatchers.IO) { + val success = clearCustomUmountPaths() + withContext(Dispatchers.Main) { + if (success) { + snackBarHost.showSnackbar( + context.getString(R.string.custom_paths_cleared) + ) + loadPaths() + } else { + snackBarHost.showSnackbar( + context.getString(R.string.operation_failed) + ) + } + } + } + } + } + }, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Filled.DeleteForever, contentDescription = null) + Spacer(modifier = Modifier.width(SPACING_MEDIUM)) + Text(stringResource(R.string.clear_custom_paths)) + } + + Button( + onClick = { + scope.launch(Dispatchers.IO) { + val success = applyUmountConfigToKernel() + withContext(Dispatchers.Main) { + if (success) { + snackBarHost.showSnackbar( + context.getString(R.string.config_applied) + ) + } else { + snackBarHost.showSnackbar( + context.getString(R.string.operation_failed) + ) + } + } + } + }, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Filled.Check, contentDescription = null) + Spacer(modifier = Modifier.width(SPACING_MEDIUM)) + Text(stringResource(R.string.apply_config)) + } + } + } + } + } + } + + if (showAddDialog) { + AddUmountPathDialog( + onDismiss = { showAddDialog = false }, + onConfirm = { path, flags -> + showAddDialog = false + + scope.launch(Dispatchers.IO) { + val success = addUmountPath(path, flags) + withContext(Dispatchers.Main) { + if (success) { + saveUmountConfig() + snackBarHost.showSnackbar( + context.getString(R.string.umount_path_added) + ) + loadPaths() + } else { + snackBarHost.showSnackbar( + context.getString(R.string.operation_failed) + ) + } + } + } + } + ) + } + } +} + +@Composable +fun UmountPathCard( + entry: UmountPathEntry, + onDelete: () -> Unit +) { + val confirmDialog = rememberConfirmDialog() + val scope = rememberCoroutineScope() + val context = LocalContext.current + + Card( + modifier = Modifier.fillMaxWidth(), + colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow), + elevation = getCardElevation() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(SPACING_LARGE), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Folder, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.width(SPACING_LARGE)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = entry.path, + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(SPACING_SMALL)) + Text( + text = buildString { + append(context.getString(R.string.flags)) + append(": ") + append(entry.flags.toUmountFlagName(context)) + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + IconButton( + onClick = { + scope.launch { + if (confirmDialog.awaitConfirm( + title = context.getString(R.string.confirm_delete), + content = context.getString(R.string.confirm_delete_umount_path, entry.path) + ) == ConfirmResult.Confirmed) { + onDelete() + } + } + } + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + } + } + } +} + +@Composable +fun AddUmountPathDialog( + onDismiss: () -> Unit, + onConfirm: (String, Int) -> Unit +) { + var path by rememberSaveable { mutableStateOf("") } + var flags by rememberSaveable { mutableStateOf("-1") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.add_umount_path)) }, + text = { + Column { + OutlinedTextField( + value = path, + onValueChange = { path = it }, + label = { Text(stringResource(R.string.mount_path)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + + OutlinedTextField( + value = flags, + onValueChange = { flags = it }, + label = { Text(stringResource(R.string.umount_flags)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + supportingText = { Text(stringResource(R.string.umount_flags_hint)) } + ) + } + }, + confirmButton = { + TextButton( + onClick = { + val flagsInt = flags.toIntOrNull() ?: -1 + onConfirm(path, flagsInt) + }, + enabled = path.isNotBlank() + ) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + ) +} + +private fun parseUmountPaths(output: String): List { + val lines = output.lines().filter { it.isNotBlank() } + if (lines.size < 2) return emptyList() + + return lines.drop(2).mapNotNull { line -> + val parts = line.trim().split(Regex("\\s+")) + if (parts.size >= 2) { + UmountPathEntry( + path = parts[0], + flags = parts[1].toIntOrNull() ?: -1 + ) + } else null + } +} + +private fun Int.toUmountFlagName(context: Context): String { + return when (this) { + -1 -> context.getString(R.string.mnt_detach) + else -> this.toString() + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt similarity index 90% rename from manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt index 583207b..00353b2 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt @@ -1,6 +1,7 @@ -package com.sukisu.ultra.ui.screen +package com.sukisu.ultra.ui.susfs import android.annotation.SuppressLint +import android.content.Context import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.* @@ -25,11 +26,13 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.* +import com.sukisu.ultra.ui.susfs.component.* import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.util.SuSFSManager -import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion158 -import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion159 +import com.sukisu.ultra.ui.susfs.util.SuSFSManager +import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158 +import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion159 +import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion1512 +import com.sukisu.ultra.ui.util.getSuSFSVersion import com.sukisu.ultra.ui.util.isAbDevice import kotlinx.coroutines.launch import java.io.File @@ -43,6 +46,7 @@ enum class SuSFSTab(val displayNameRes: Int) { BASIC_SETTINGS(R.string.susfs_tab_basic_settings), SUS_PATHS(R.string.susfs_tab_sus_paths), SUS_LOOP_PATHS(R.string.susfs_tab_sus_loop_paths), + SUS_MAPS(R.string.susfs_tab_sus_maps), SUS_MOUNTS(R.string.susfs_tab_sus_mounts), TRY_UMOUNT(R.string.susfs_tab_try_umount), KSTAT_CONFIG(R.string.susfs_tab_kstat_config), @@ -50,11 +54,12 @@ enum class SuSFSTab(val displayNameRes: Int) { ENABLED_FEATURES(R.string.susfs_tab_enabled_features); companion object { - fun getAllTabs(isSusVersion158: Boolean, isSusVersion159: Boolean): List { + fun getAllTabs(isSusVersion158: Boolean, isSusVersion159: Boolean, isSusVersion1512: Boolean): List { return when { - isSusVersion159 -> entries.toList() - isSusVersion158 -> entries.filter { it != SUS_LOOP_PATHS } - else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS } + isSusVersion1512 -> entries.toList() + isSusVersion159 -> entries.filter { it != SUS_MAPS} + isSusVersion158 -> entries.filter { it != SUS_LOOP_PATHS && it != SUS_MAPS } + else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS && it != SUS_MAPS } } } } @@ -93,6 +98,7 @@ fun SuSFSConfigScreen( // 路径管理相关状态 var susPaths by remember { mutableStateOf(emptySet()) } var susLoopPaths by remember { mutableStateOf(emptySet()) } + var susMaps by remember { mutableStateOf(emptySet()) } var susMounts by remember { mutableStateOf(emptySet()) } var tryUmounts by remember { mutableStateOf(emptySet()) } var androidDataPath by remember { mutableStateOf("") } @@ -117,16 +123,17 @@ fun SuSFSConfigScreen( // 对话框状态 var showAddPathDialog by remember { mutableStateOf(false) } var showAddLoopPathDialog by remember { mutableStateOf(false) } + var showAddSusMapDialog by remember { mutableStateOf(false) } var showAddAppPathDialog by remember { mutableStateOf(false) } var showAddMountDialog by remember { mutableStateOf(false) } var showAddUmountDialog by remember { mutableStateOf(false) } - var showRunUmountDialog by remember { mutableStateOf(false) } var showAddKstatStaticallyDialog by remember { mutableStateOf(false) } var showAddKstatDialog by remember { mutableStateOf(false) } // 编辑状态 var editingPath by remember { mutableStateOf(null) } var editingLoopPath by remember { mutableStateOf(null) } + var editingSusMap by remember { mutableStateOf(null) } var editingMount by remember { mutableStateOf(null) } var editingUmount by remember { mutableStateOf(null) } var editingKstatConfig by remember { mutableStateOf(null) } @@ -135,6 +142,7 @@ fun SuSFSConfigScreen( // 重置确认对话框状态 var showResetPathsDialog by remember { mutableStateOf(false) } var showResetLoopPathsDialog by remember { mutableStateOf(false) } + var showResetSusMapsDialog by remember { mutableStateOf(false) } var showResetMountsDialog by remember { mutableStateOf(false) } var showResetUmountsDialog by remember { mutableStateOf(false) } var showResetKstatDialog by remember { mutableStateOf(false) } @@ -148,7 +156,7 @@ fun SuSFSConfigScreen( var isNavigating by remember { mutableStateOf(false) } - val allTabs = SuSFSTab.getAllTabs(isSusVersion158(), isSusVersion159()) + val allTabs = SuSFSTab.getAllTabs(isSusVersion158(), isSusVersion159(), isSusVersion1512()) // 实时判断是否可以启用开机自启动 val canEnableAutoStart by remember { @@ -157,6 +165,38 @@ fun SuSFSConfigScreen( } } + var showVersionMismatchDialog by remember { mutableStateOf(false) } + + if (showVersionMismatchDialog) { + AlertDialog( + onDismissRequest = { showVersionMismatchDialog = false }, + title = { + Text( + text = stringResource(R.string.warning), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + }, + text = { + Text( + stringResource( + R.string.susfs_version_mismatch, + try { getSuSFSVersion() } catch (_: Exception) { "unknown" }, + SuSFSManager.MAX_SUSFS_VERSION + ) + ) + }, + confirmButton = { + TextButton( + onClick = { showVersionMismatchDialog = false }, + modifier = Modifier.padding(8.dp) + ) { + Text(stringResource(R.string.confirm)) + } + } + ) + } + // 文件选择器 val backupFileLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.CreateDocument("application/json") @@ -242,25 +282,42 @@ fun SuSFSConfigScreen( // 加载当前配置 LaunchedEffect(Unit) { - unameValue = SuSFSManager.getUnameValue(context) - buildTimeValue = SuSFSManager.getBuildTimeValue(context) - autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) - executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) - susPaths = SuSFSManager.getSusPaths(context) - susLoopPaths = SuSFSManager.getSusLoopPaths(context) - susMounts = SuSFSManager.getSusMounts(context) - tryUmounts = SuSFSManager.getTryUmounts(context) - androidDataPath = SuSFSManager.getAndroidDataPath(context) - sdcardPath = SuSFSManager.getSdcardPath(context) - kstatConfigs = SuSFSManager.getKstatConfigs(context) - addKstatPaths = SuSFSManager.getAddKstatPaths(context) - hideSusMountsForAllProcs = SuSFSManager.getHideSusMountsForAllProcs(context) - enableHideBl = SuSFSManager.getEnableHideBl(context) - enableCleanupResidue = SuSFSManager.getEnableCleanupResidue(context) - umountForZygoteIsoService = SuSFSManager.getUmountForZygoteIsoService(context) - enableAvcLogSpoofing = SuSFSManager.getEnableAvcLogSpoofing(context) + coroutineScope.launch { + try { + val version = getSuSFSVersion() + val binaryName = "ksu_susfs_${version.removePrefix("v")}" - loadSlotInfo() + val isBinaryAvailable = try { + context.assets.open(binaryName).use { true } + } catch (_: Exception) { false } + + if (!isBinaryAvailable) { + showVersionMismatchDialog = true + } + } catch (_: Exception) { + } + + unameValue = SuSFSManager.getUnameValue(context) + buildTimeValue = SuSFSManager.getBuildTimeValue(context) + autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) + executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) + susPaths = SuSFSManager.getSusPaths(context) + susLoopPaths = SuSFSManager.getSusLoopPaths(context) + susMaps = SuSFSManager.getSusMaps(context) + susMounts = SuSFSManager.getSusMounts(context) + tryUmounts = SuSFSManager.getTryUmounts(context) + androidDataPath = SuSFSManager.getAndroidDataPath(context) + sdcardPath = SuSFSManager.getSdcardPath(context) + kstatConfigs = SuSFSManager.getKstatConfigs(context) + addKstatPaths = SuSFSManager.getAddKstatPaths(context) + hideSusMountsForAllProcs = SuSFSManager.getHideSusMountsForAllProcs(context) + enableHideBl = SuSFSManager.getEnableHideBl(context) + enableCleanupResidue = SuSFSManager.getEnableCleanupResidue(context) + umountForZygoteIsoService = SuSFSManager.getUmountForZygoteIsoService(context) + enableAvcLogSpoofing = SuSFSManager.getEnableAvcLogSpoofing(context) + + loadSlotInfo() + } } // 当切换到启用功能状态标签页时加载数据 @@ -419,6 +476,7 @@ fun SuSFSConfigScreen( executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) susPaths = SuSFSManager.getSusPaths(context) susLoopPaths = SuSFSManager.getSusLoopPaths(context) + susMaps = SuSFSManager.getSusMaps(context) susMounts = SuSFSManager.getSusMounts(context) tryUmounts = SuSFSManager.getTryUmounts(context) androidDataPath = SuSFSManager.getAndroidDataPath(context) @@ -537,6 +595,35 @@ fun SuSFSConfigScreen( initialValue = editingLoopPath ?: "" ) + AddPathDialog( + showDialog = showAddSusMapDialog, + onDismiss = { + showAddSusMapDialog = false + editingSusMap = null + }, + onConfirm = { path -> + coroutineScope.launch { + isLoading = true + val success = if (editingSusMap != null) { + SuSFSManager.editSusMap(context, editingSusMap!!, path) + } else { + SuSFSManager.addSusMap(context, path) + } + if (success) { + susMaps = SuSFSManager.getSusMaps(context) + } + isLoading = false + showAddSusMapDialog = false + editingSusMap = null + } + }, + isLoading = isLoading, + titleRes = if (editingSusMap != null) R.string.susfs_edit_sus_map else R.string.susfs_add_sus_map, + labelRes = R.string.susfs_sus_map_label, + placeholderRes = R.string.susfs_sus_map_placeholder, + initialValue = editingSusMap ?: "" + ) + AddAppPathDialog( showDialog = showAddAppPathDialog, onDismiss = { showAddAppPathDialog = false }, @@ -629,8 +716,21 @@ fun SuSFSConfigScreen( isLoading = true val success = if (editingKstatConfig != null) { SuSFSManager.editKstatConfig( - context, editingKstatConfig!!, path, ino, dev, nlink, size, atime, atimeNsec, - mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize + context, + editingKstatConfig!!, + path, + ino, + dev, + nlink, + size, + atime, + atimeNsec, + mtime, + mtimeNsec, + ctime, + ctimeNsec, + blocks, + blksize ) } else { SuSFSManager.addKstatStatically( @@ -680,22 +780,6 @@ fun SuSFSConfigScreen( ) // 确认对话框 - ConfirmDialog( - showDialog = showRunUmountDialog, - onDismiss = { showRunUmountDialog = false }, - onConfirm = { - coroutineScope.launch { - isLoading = true - SuSFSManager.runTryUmount(context) - isLoading = false - showRunUmountDialog = false - } - }, - titleRes = R.string.susfs_run_umount_confirm_title, - messageRes = R.string.susfs_run_umount_confirm_message, - isLoading = isLoading - ) - ConfirmDialog( showDialog = showConfirmReset, onDismiss = { showConfirmReset = false }, @@ -760,6 +844,27 @@ fun SuSFSConfigScreen( isDestructive = true ) + ConfirmDialog( + showDialog = showResetSusMapsDialog, + onDismiss = { showResetSusMapsDialog = false }, + onConfirm = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveSusMaps(context, emptySet()) + susMaps = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetSusMapsDialog = false + } + }, + titleRes = R.string.susfs_reset_sus_maps_title, + messageRes = R.string.susfs_reset_sus_maps_message, + isLoading = isLoading, + isDestructive = true + ) + ConfirmDialog( showDialog = showResetMountsDialog, onDismiss = { showResetMountsDialog = false }, @@ -979,6 +1084,28 @@ fun SuSFSConfigScreen( } } + SuSFSTab.SUS_MAPS -> { + OutlinedButton( + onClick = { showResetSusMapsDialog = true }, + enabled = !isLoading && susMaps.isNotEmpty(), + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + stringResource(R.string.susfs_reset_sus_maps_title), + fontWeight = FontWeight.Medium + ) + } + } + SuSFSTab.SUS_MOUNTS -> { OutlinedButton( onClick = { showResetMountsDialog = true }, @@ -1110,12 +1237,12 @@ fun SuSFSConfigScreen( .padding(horizontal = 12.dp) ) { // 标签页 - ScrollableTabRow( + PrimaryScrollableTabRow( selectedTabIndex = allTabs.indexOf(selectedTab), - edgePadding = 0.dp, modifier = Modifier.fillMaxWidth(), containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface + contentColor = MaterialTheme.colorScheme.onSurface, + edgePadding = 0.dp ) { allTabs.forEach { tab -> Tab( @@ -1243,6 +1370,26 @@ fun SuSFSConfigScreen( } ) } + SuSFSTab.SUS_MAPS -> { + SusMapsContent( + susMaps = susMaps, + isLoading = isLoading, + onAddSusMap = { showAddSusMapDialog = true }, + onRemoveSusMap = { map -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeSusMap(context, map)) { + susMaps = SuSFSManager.getSusMaps(context) + } + isLoading = false + } + }, + onEditSusMap = { map -> + editingSusMap = map + showAddSusMapDialog = true + } + ) + } SuSFSTab.SUS_MOUNTS -> { val isSusVersion158 = remember { isSusVersion158() } @@ -1268,7 +1415,11 @@ fun SuSFSConfigScreen( onToggleHideSusMountsForAllProcs = { hideForAll -> coroutineScope.launch { isLoading = true - if (SuSFSManager.setHideSusMountsForAllProcs(context, hideForAll)) { + if (SuSFSManager.setHideSusMountsForAllProcs( + context, + hideForAll + ) + ) { hideSusMountsForAllProcs = hideForAll } isLoading = false @@ -1283,7 +1434,6 @@ fun SuSFSConfigScreen( umountForZygoteIsoService = umountForZygoteIsoService, isLoading = isLoading, onAddUmount = { showAddUmountDialog = true }, - onRunUmount = { showRunUmountDialog = true }, onRemoveUmount = { umountEntry -> coroutineScope.launch { isLoading = true @@ -1300,7 +1450,8 @@ fun SuSFSConfigScreen( onToggleUmountForZygoteIsoService = { enabled -> coroutineScope.launch { isLoading = true - val success = SuSFSManager.setUmountForZygoteIsoService(context, enabled) + val success = + SuSFSManager.setUmountForZygoteIsoService(context, enabled) if (success) { umountForZygoteIsoService = enabled } @@ -1411,7 +1562,7 @@ private fun BasicSettingsContent( isLoading: Boolean, onAutoStartToggle: (Boolean) -> Unit, onShowSlotInfo: () -> Unit, - context: android.content.Context, + context: Context, onShowBackupDialog: () -> Unit, onShowRestoreDialog: () -> Unit, enableHideBl: Boolean, @@ -1422,7 +1573,9 @@ private fun BasicSettingsContent( onEnableAvcLogSpoofingChange: (Boolean) -> Unit ) { var scriptLocationExpanded by remember { mutableStateOf(false) } - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value val isSusVersion159 = isSusVersion159() Column( @@ -1498,7 +1651,7 @@ private fun BasicSettingsContent( trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = scriptLocationExpanded) }, modifier = Modifier .fillMaxWidth() - .menuAnchor(MenuAnchorType.PrimaryEditable, true), + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true), shape = RoundedCornerShape(8.dp), enabled = !isLoading ) @@ -1912,7 +2065,9 @@ private fun SlotInfoDialog( onUseUname: (String) -> Unit, onUseBuildTime: (String) -> Unit ) { - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value if (showDialog && isAbDevice) { AlertDialog( diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt similarity index 99% rename from manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt index f3fb780..41a0c4c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.component +package com.sukisu.ultra.ui.susfs.component import android.annotation.SuppressLint import android.content.pm.PackageInfo @@ -29,7 +29,7 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.sukisu.ultra.R -import com.sukisu.ultra.ui.util.SuSFSManager +import com.sukisu.ultra.ui.susfs.util.SuSFSManager import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import kotlinx.coroutines.launch @@ -464,7 +464,7 @@ fun AddTryUmountDialog( trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = umountModeExpanded) }, modifier = Modifier .fillMaxWidth() - .menuAnchor(MenuAnchorType.PrimaryEditable, true), + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true), shape = RoundedCornerShape(8.dp) ) ExposedDropdownMenu( diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt similarity index 87% rename from manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt index 5e2bd89..be683f2 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.component +package com.sukisu.ultra.ui.susfs.component import android.annotation.SuppressLint import androidx.compose.foundation.layout.* @@ -18,8 +18,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.sukisu.ultra.R -import com.sukisu.ultra.ui.util.SuSFSManager -import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion158 +import com.sukisu.ultra.ui.susfs.util.SuSFSManager +import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158 import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel /** @@ -310,6 +310,116 @@ fun SusLoopPathsContent( } } +/** + * SUS Maps内容组件 + */ +@Composable +fun SusMapsContent( + susMaps: Set, + isLoading: Boolean, + onAddSusMap: () -> Unit, + onRemoveSusMap: (String) -> Unit, + onEditSusMap: ((String) -> Unit)? = null +) { + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 说明卡片 + item { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.sus_maps_description_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource(R.string.sus_maps_description_text), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.sus_maps_warning), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.secondary + ) + Text( + text = stringResource(R.string.sus_maps_debug_info), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + } + + if (susMaps.isEmpty()) { + item { + EmptyStateCard( + message = stringResource(R.string.susfs_no_sus_maps_configured) + ) + } + } else { + item { + SectionHeader( + title = stringResource(R.string.sus_maps_section), + subtitle = null, + icon = Icons.Default.Security, + count = susMaps.size + ) + } + + items(susMaps.toList()) { map -> + PathItemCard( + path = map, + icon = Icons.Default.Security, + onDelete = { onRemoveSusMap(map) }, + onEdit = if (onEditSusMap != null) { { onEditSusMap(map) } } else null, + isLoading = isLoading + ) + } + } + + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onAddSusMap, + modifier = Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(8.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = stringResource(R.string.add)) + } + } + } + } + } +} + /** * SUS挂载内容组件 */ @@ -395,7 +505,6 @@ fun TryUmountContent( umountForZygoteIsoService: Boolean, isLoading: Boolean, onAddUmount: () -> Unit, - onRunUmount: () -> Unit, onRemoveUmount: (String) -> Unit, onEditUmount: ((String) -> Unit)? = null, onToggleUmountForZygoteIsoService: (Boolean) -> Unit @@ -509,24 +618,6 @@ fun TryUmountContent( Spacer(modifier = Modifier.width(8.dp)) Text(text = stringResource(R.string.add)) } - - if (tryUmounts.isNotEmpty()) { - Button( - onClick = onRunUmount, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.susfs_run)) - } - } } } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt similarity index 89% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt index 0da7793..e199568 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt @@ -1,14 +1,14 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.susfs.util import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo +import android.os.Build import android.util.Log import android.widget.Toast import com.dergoogler.mmrl.platform.Platform.Companion.context -import com.sukisu.ultra.Natives import com.sukisu.ultra.R import com.topjohnwu.superuser.Shell import kotlinx.coroutines.Dispatchers @@ -19,9 +19,14 @@ import java.io.File import java.io.FileOutputStream import java.io.IOException import androidx.core.content.edit +import com.sukisu.ultra.ui.util.getRootShell +import com.sukisu.ultra.ui.util.getSuSFSVersion +import com.sukisu.ultra.ui.util.getSuSFSFeatures import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import org.json.JSONArray import org.json.JSONObject import java.text.SimpleDateFormat import java.util.* @@ -37,6 +42,8 @@ object SuSFSManager { private const val KEY_AUTO_START_ENABLED = "auto_start_enabled" private const val KEY_SUS_PATHS = "sus_paths" private const val KEY_SUS_LOOP_PATHS = "sus_loop_paths" + + private const val KEY_SUS_MAPS = "sus_maps" private const val KEY_SUS_MOUNTS = "sus_mounts" private const val KEY_TRY_UMOUNTS = "try_umounts" private const val KEY_ANDROID_DATA_PATH = "android_data_path" @@ -60,6 +67,8 @@ object SuSFSManager { private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID" private const val MIN_VERSION_FOR_HIDE_MOUNT = "1.5.8" private const val MIN_VERSION_FOR_LOOP_PATH = "1.5.9" + private const val MIN_VERSION_SUS_MAPS = "1.5.12" + const val MAX_SUSFS_VERSION = "2.0.0" private const val BACKUP_FILE_EXTENSION = ".susfs_backup" private const val MEDIA_DATA_PATH = "/data/media/0/Android/data" private const val CGROUP_UID_PATH_PREFIX = "/sys/fs/cgroup/uid_" @@ -112,7 +121,7 @@ object SuSFSManager { configurationsJson.keys().forEach { key -> val value = configurationsJson.get(key) configurations[key] = when (value) { - is org.json.JSONArray -> { + is JSONArray -> { val set = mutableSetOf() for (i in 0 until value.length()) { set.add(value.getString(i)) @@ -147,6 +156,7 @@ object SuSFSManager { val executeInPostFsData: Boolean, val susPaths: Set, val susLoopPaths: Set, + val susMaps: Set, val susMounts: Set, val tryUmounts: Set, val androidDataPath: String, @@ -169,6 +179,7 @@ object SuSFSManager { buildTimeValue != DEFAULT_BUILD_TIME || susPaths.isNotEmpty() || susLoopPaths.isNotEmpty() || + susMaps.isNotEmpty() || susMounts.isNotEmpty() || tryUmounts.isNotEmpty() || kstatConfigs.isNotEmpty() || @@ -180,11 +191,23 @@ object SuSFSManager { private fun getPrefs(context: Context): SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - private fun getSuSFSVersionUse(): String = try { - getSuSFSVersion() - } catch (_: Exception) { MIN_VERSION_FOR_HIDE_MOUNT } + private fun getSuSFSVersionUse(context: Context): String = try { + val version = getSuSFSVersion() + val binaryName = "${SUSFS_BINARY_TARGET_NAME}_${version.removePrefix("v")}" + if (isBinaryAvailable(context, binaryName)) { + version + } else { + MAX_SUSFS_VERSION + } + } catch (_: Exception) { + MAX_SUSFS_VERSION + } - private fun getSuSFSBinaryName(): String = "${SUSFS_BINARY_TARGET_NAME}_${getSuSFSVersionUse().removePrefix("v")}" + fun isBinaryAvailable(context: Context, binaryName: String): Boolean = try { + context.assets.open(binaryName).use { true } + } catch (_: IOException) { false } + + private fun getSuSFSBinaryName(context: Context): String = "${SUSFS_BINARY_TARGET_NAME}_${getSuSFSVersionUse(context).removePrefix("v")}" private fun getSuSFSTargetPath(): String = "/data/adb/ksu/bin/$SUSFS_BINARY_TARGET_NAME" @@ -222,29 +245,19 @@ object SuSFSManager { return 0 } - /** - * 检查是否支持设置sdcard路径等功能(1.5.8+) - */ - fun isSusVersion158(): Boolean { - return try { - val currentVersion = getSuSFSVersion() - compareVersions(currentVersion, MIN_VERSION_FOR_HIDE_MOUNT) >= 0 - } catch (_: Exception) { - true - } + private fun isVersionAtLeast(minVersion: String): Boolean = try { + compareVersions(getSuSFSVersion(), minVersion) >= 0 + } catch (_: Exception) { + true } + // 检查是否支持设置sdcard路径等功能(1.5.8+) + fun isSusVersion158(): Boolean = isVersionAtLeast(MIN_VERSION_FOR_HIDE_MOUNT) - /** - * 检查是否支持循环路径和AVC日志欺骗等功能(1.5.9+) - */ - fun isSusVersion159(): Boolean { - return try { - val currentVersion = getSuSFSVersion() - compareVersions(currentVersion, MIN_VERSION_FOR_LOOP_PATH) >= 0 - } catch (_: Exception) { - true - } - } + // 检查是否支持循环路径和AVC日志欺骗等功能(1.5.9+) + fun isSusVersion159(): Boolean = isVersionAtLeast(MIN_VERSION_FOR_LOOP_PATH) + + // 检查是否支持隐藏内存映射(1.5.12+) + fun isSusVersion1512(): Boolean = isVersionAtLeast(MIN_VERSION_SUS_MAPS) /** * 获取当前模块配置 @@ -257,6 +270,7 @@ object SuSFSManager { executeInPostFsData = getExecuteInPostFsData(context), susPaths = getSusPaths(context), susLoopPaths = getSusLoopPaths(context), + susMaps = getSusMaps(context), susMounts = getSusMounts(context), tryUmounts = getTryUmounts(context), androidDataPath = getAndroidDataPath(context), @@ -304,7 +318,7 @@ object SuSFSManager { fun saveExecuteInPostFsData(context: Context, executeInPostFsData: Boolean) { getPrefs(context).edit { putBoolean(KEY_EXECUTE_IN_POST_FS_DATA, executeInPostFsData) } if (isAutoStartEnabled(context)) { - kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Dispatchers.Default).launch { updateMagiskModule(context) } } @@ -361,6 +375,12 @@ object SuSFSManager { fun getSusLoopPaths(context: Context): Set = getPrefs(context).getStringSet(KEY_SUS_LOOP_PATHS, emptySet()) ?: emptySet() + fun saveSusMaps(context: Context, maps: Set) = + getPrefs(context).edit { putStringSet(KEY_SUS_MAPS, maps) } + + fun getSusMaps(context: Context): Set = + getPrefs(context).getStringSet(KEY_SUS_MAPS, emptySet()) ?: emptySet() + fun saveSusMounts(context: Context, mounts: Set) = getPrefs(context).edit { putStringSet(KEY_SUS_MOUNTS, mounts) } @@ -526,6 +546,7 @@ object SuSFSManager { KEY_AUTO_START_ENABLED to isAutoStartEnabled(context), KEY_SUS_PATHS to getSusPaths(context), KEY_SUS_LOOP_PATHS to getSusLoopPaths(context), + KEY_SUS_MAPS to getSusMaps(context), KEY_SUS_MOUNTS to getSusMounts(context), KEY_TRY_UMOUNTS to getTryUmounts(context), KEY_ANDROID_DATA_PATH to getAndroidDataPath(context), @@ -552,7 +573,7 @@ object SuSFSManager { // 获取设备信息 private fun getDeviceInfo(): String { return try { - "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL} (${android.os.Build.VERSION.RELEASE})" + "${Build.MANUFACTURER} ${Build.MODEL} (${Build.VERSION.RELEASE})" } catch (_: Exception) { "Unknown Device" } @@ -710,7 +731,7 @@ object SuSFSManager { // 二进制文件管理 private suspend fun copyBinaryFromAssets(context: Context): String? = withContext(Dispatchers.IO) { try { - val binaryName = getSuSFSBinaryName() + val binaryName = getSuSFSBinaryName(context) val targetPath = getSuSFSTargetPath() val tempFile = File(context.cacheDir, binaryName) @@ -731,7 +752,7 @@ object SuSFSManager { } fun isBinaryAvailable(context: Context): Boolean = try { - context.assets.open(getSuSFSBinaryName()).use { true } + context.assets.open(getSuSFSBinaryName(context)).use { true } } catch (_: IOException) { false } // 命令执行 @@ -818,9 +839,10 @@ object SuSFSManager { // 功能状态获取 suspend fun getEnabledFeatures(context: Context): List = withContext(Dispatchers.IO) { try { - val status = Natives.getSusfsFeatureStatus() - if (status != null) { - parseEnabledFeaturesFromStatus(context, status) + val featuresOutput = getSuSFSFeatures() + + if (featuresOutput.isNotBlank() && featuresOutput != "Invalid") { + parseEnabledFeaturesFromOutput(context, featuresOutput) } else { getDefaultDisabledFeatures(context) } @@ -830,23 +852,53 @@ object SuSFSManager { } } + private fun parseEnabledFeaturesFromOutput(context: Context, featuresOutput: String): List { + val enabledConfigs = featuresOutput.lines() + .map { it.trim() } + .filter { it.isNotEmpty() } + .toSet() + + val featureMap = mapOf( + "CONFIG_KSU_SUSFS_SUS_PATH" to context.getString(R.string.sus_path_feature_label), + "CONFIG_KSU_SUSFS_SUS_MOUNT" to context.getString(R.string.sus_mount_feature_label), + "CONFIG_KSU_SUSFS_TRY_UMOUNT" to context.getString(R.string.try_umount_feature_label), + "CONFIG_KSU_SUSFS_SPOOF_UNAME" to context.getString(R.string.spoof_uname_feature_label), + "CONFIG_KSU_SUSFS_SPOOF_CMDLINE_OR_BOOTCONFIG" to context.getString(R.string.spoof_cmdline_feature_label), + "CONFIG_KSU_SUSFS_OPEN_REDIRECT" to context.getString(R.string.open_redirect_feature_label), + "CONFIG_KSU_SUSFS_ENABLE_LOG" to context.getString(R.string.enable_log_feature_label), + "CONFIG_KSU_SUSFS_AUTO_ADD_TRY_UMOUNT_FOR_BIND_MOUNT" to context.getString(R.string.auto_try_umount_bind_feature_label), + "CONFIG_KSU_SUSFS_HIDE_KSU_SUSFS_SYMBOLS" to context.getString(R.string.hide_symbols_feature_label), + "CONFIG_KSU_SUSFS_SUS_KSTAT" to context.getString(R.string.sus_kstat_feature_label), + ) + + + return featureMap.map { (configKey, displayName) -> + val isEnabled = enabledConfigs.contains(configKey) + + val statusText = if (isEnabled) { + context.getString(R.string.susfs_feature_enabled) + } else { + context.getString(R.string.susfs_feature_disabled) + } + + val canConfigure = displayName == context.getString(R.string.enable_log_feature_label) + + EnabledFeature(displayName, isEnabled, statusText, canConfigure) + }.sortedBy { it.name } + } + private fun getDefaultDisabledFeatures(context: Context): List { val defaultFeatures = listOf( "sus_path_feature_label" to context.getString(R.string.sus_path_feature_label), - "sus_loop_path_feature_label" to context.getString(R.string.sus_loop_path_feature_label), "sus_mount_feature_label" to context.getString(R.string.sus_mount_feature_label), "try_umount_feature_label" to context.getString(R.string.try_umount_feature_label), "spoof_uname_feature_label" to context.getString(R.string.spoof_uname_feature_label), "spoof_cmdline_feature_label" to context.getString(R.string.spoof_cmdline_feature_label), "open_redirect_feature_label" to context.getString(R.string.open_redirect_feature_label), "enable_log_feature_label" to context.getString(R.string.enable_log_feature_label), - "auto_default_mount_feature_label" to context.getString(R.string.auto_default_mount_feature_label), - "auto_bind_mount_feature_label" to context.getString(R.string.auto_bind_mount_feature_label), "auto_try_umount_bind_feature_label" to context.getString(R.string.auto_try_umount_bind_feature_label), "hide_symbols_feature_label" to context.getString(R.string.hide_symbols_feature_label), "sus_kstat_feature_label" to context.getString(R.string.sus_kstat_feature_label), - "magic_mount_feature_label" to context.getString(R.string.magic_mount_feature_label), - "sus_su_feature_label" to context.getString(R.string.sus_su_feature_label) ) return defaultFeatures.map { (_, displayName) -> @@ -859,31 +911,6 @@ object SuSFSManager { }.sortedBy { it.name } } - private fun parseEnabledFeaturesFromStatus(context: Context, status: Natives.SusfsFeatureStatus): List { - val featureList = listOf( - Triple("status_sus_path", context.getString(R.string.sus_path_feature_label), status.statusSusPath), - Triple("status_sus_mount", context.getString(R.string.sus_mount_feature_label), status.statusSusMount), - Triple("status_try_umount", context.getString(R.string.try_umount_feature_label), status.statusTryUmount), - Triple("status_spoof_uname", context.getString(R.string.spoof_uname_feature_label), status.statusSpoofUname), - Triple("status_spoof_cmdline", context.getString(R.string.spoof_cmdline_feature_label), status.statusSpoofCmdline), - Triple("status_open_redirect", context.getString(R.string.open_redirect_feature_label), status.statusOpenRedirect), - Triple("status_enable_log", context.getString(R.string.enable_log_feature_label), status.statusEnableLog), - Triple("status_auto_default_mount", context.getString(R.string.auto_default_mount_feature_label), status.statusAutoDefaultMount), - Triple("status_auto_bind_mount", context.getString(R.string.auto_bind_mount_feature_label), status.statusAutoBindMount), - Triple("status_auto_try_umount_bind", context.getString(R.string.auto_try_umount_bind_feature_label), status.statusAutoTryUmountBind), - Triple("status_hide_symbols", context.getString(R.string.hide_symbols_feature_label), status.statusHideSymbols), - Triple("status_sus_kstat", context.getString(R.string.sus_kstat_feature_label), status.statusSusKstat), - Triple("status_magic_mount", context.getString(R.string.magic_mount_feature_label), status.statusMagicMount), - Triple("status_sus_su", context.getString(R.string.sus_su_feature_label), status.statusSusSu) - ) - - return featureList.map { (id, displayName, isEnabled) -> - val statusText = if (isEnabled) context.getString(R.string.susfs_feature_enabled) else context.getString(R.string.susfs_feature_disabled) - val canConfigure = id == "status_enable_log" - EnabledFeature(displayName, isEnabled, statusText, canConfigure) - }.sortedBy { it.name } - } - // sus日志开关 suspend fun setEnableLog(context: Context, enabled: Boolean): Boolean { val success = executeSusfsCommand(context, "enable_log ${if (enabled) 1 else 0}") @@ -1107,6 +1134,54 @@ object SuSFSManager { } } + // 添加 SUS Maps + suspend fun addSusMap(context: Context, map: String): Boolean { + val success = executeSusfsCommand(context, "add_sus_map '$map'") + if (success) { + saveSusMaps(context, getSusMaps(context) + map) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_sus_map_added_success, map)) + } + return success + } + + suspend fun removeSusMap(context: Context, map: String): Boolean { + saveSusMaps(context, getSusMaps(context) - map) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_sus_map_removed, map)) + return true + } + + suspend fun editSusMap(context: Context, oldMap: String, newMap: String): Boolean { + return try { + val currentMaps = getSusMaps(context).toMutableSet() + if (!currentMaps.remove(oldMap)) { + showToast(context, "Original SUS map not found: $oldMap") + return false + } + + saveSusMaps(context, currentMaps) + + val success = addSusMap(context, newMap) + + if (success) { + showToast(context, context.getString(R.string.susfs_sus_map_updated, oldMap, newMap)) + return true + } else { + // 如果添加新映射失败,恢复旧映射 + currentMaps.add(oldMap) + saveSusMaps(context, currentMaps) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Failed to update SUS map, reverted to original") + return false + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, "Error updating SUS map: ${e.message}") + false + } + } + // 添加SUS挂载 suspend fun addSusMount(context: Context, mount: String): Boolean { val success = executeSusfsCommand(context, "add_sus_mount '$mount'") @@ -1208,8 +1283,6 @@ object SuSFSManager { } } - suspend fun runTryUmount(context: Context): Boolean = executeSusfsCommand(context, "run_try_umount") - // Zygote隔离服务卸载控制 suspend fun setUmountForZygoteIsoService(context: Context, enabled: Boolean): Boolean { if (!isSusVersion158()) { @@ -1361,7 +1434,7 @@ object SuSFSManager { if (success) { saveAndroidDataPath(context, path) if (isAutoStartEnabled(context)) { - kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Dispatchers.Default).launch { updateMagiskModule(context) } } @@ -1375,7 +1448,7 @@ object SuSFSManager { if (success) { saveSdcardPath(context, path) if (isAutoStartEnabled(context)) { - kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Dispatchers.Default).launch { updateMagiskModule(context) } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt similarity index 97% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt index 1cf9cf8..e0d9bae 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.susfs.util import android.annotation.SuppressLint @@ -497,6 +497,10 @@ object ScriptGenerator { if (config.susLoopPaths.isNotEmpty()) { generateSusLoopPathsSection(config.susLoopPaths) } + + if (config.susMaps.isNotEmpty()) { + generateSusMapsSection(config.susMaps) + } } } @@ -504,6 +508,17 @@ object ScriptGenerator { } } + private fun StringBuilder.generateSusMapsSection(susMaps: Set) { + if (susMaps.isNotEmpty()) { + appendLine("# 添加SUS映射") + susMaps.forEach { map -> + appendLine("\"${'$'}SUSFS_BIN\" add_sus_map '$map'") + appendLine("echo \"$(get_current_time): 添加SUS映射: $map\" >> \"${'$'}LOG_FILE\"") + } + appendLine() + } + } + @SuppressLint("SdCardPath") private fun StringBuilder.generatePathSettingSection(androidDataPath: String, sdcardPath: String) { appendLine("# 路径配置") diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt index e05ce3b..2757852 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt @@ -6,114 +6,187 @@ import androidx.compose.material3.CardDefaults import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +@Stable object CardConfig { // 卡片透明度 var cardAlpha by mutableFloatStateOf(1f) + internal set // 卡片亮度 var cardDim by mutableFloatStateOf(0f) + internal set // 卡片阴影 var cardElevation by mutableStateOf(0.dp) - var isShadowEnabled by mutableStateOf(true) - var isCustomAlphaSet by mutableStateOf(false) - var isCustomDimSet by mutableStateOf(false) - var isUserDarkModeEnabled by mutableStateOf(false) - var isUserLightModeEnabled by mutableStateOf(false) - var isCustomBackgroundEnabled by mutableStateOf(false) + internal set + + // 功能开关 + var isShadowEnabled by mutableStateOf(true) + internal set + var isCustomBackgroundEnabled by mutableStateOf(false) + internal set + + var isCustomAlphaSet by mutableStateOf(false) + internal set + var isCustomDimSet by mutableStateOf(false) + internal set + var isUserDarkModeEnabled by mutableStateOf(false) + internal set + var isUserLightModeEnabled by mutableStateOf(false) + internal set + + // 配置键名 + private object Keys { + const val CARD_ALPHA = "card_alpha" + const val CARD_DIM = "card_dim" + const val CUSTOM_BACKGROUND_ENABLED = "custom_background_enabled" + const val IS_SHADOW_ENABLED = "is_shadow_enabled" + const val IS_CUSTOM_ALPHA_SET = "is_custom_alpha_set" + const val IS_CUSTOM_DIM_SET = "is_custom_dim_set" + const val IS_USER_DARK_MODE_ENABLED = "is_user_dark_mode_enabled" + const val IS_USER_LIGHT_MODE_ENABLED = "is_user_light_mode_enabled" + } + + fun updateAlpha(alpha: Float, isCustom: Boolean = true) { + cardAlpha = alpha.coerceIn(0f, 1f) + if (isCustom) isCustomAlphaSet = true + } + + fun updateDim(dim: Float, isCustom: Boolean = true) { + cardDim = dim.coerceIn(0f, 1f) + if (isCustom) isCustomDimSet = true + } + + fun updateShadow(enabled: Boolean, elevation: Dp = cardElevation) { + isShadowEnabled = enabled + cardElevation = if (enabled) elevation else cardElevation + } + + fun updateBackground(enabled: Boolean) { + isCustomBackgroundEnabled = enabled + // 自定义背景时自动禁用阴影以获得更好的视觉效果 + if (enabled) { + updateShadow(false) + } + } + + fun updateThemePreference(darkMode: Boolean?, lightMode: Boolean?) { + isUserDarkModeEnabled = darkMode ?: false + isUserLightModeEnabled = lightMode ?: false + } + + fun reset() { + cardAlpha = 1f + cardDim = 0f + cardElevation = 0.dp + isShadowEnabled = true + isCustomBackgroundEnabled = false + isCustomAlphaSet = false + isCustomDimSet = false + isUserDarkModeEnabled = false + isUserLightModeEnabled = false + } + + fun setThemeDefaults(isDarkMode: Boolean) { + if (!isCustomAlphaSet) { + updateAlpha(if (isDarkMode) 0.88f else 1f, false) + } + if (!isCustomDimSet) { + updateDim(if (isDarkMode) 0.25f else 0f, false) + } + // 暗色模式下默认启用轻微阴影 + if (isDarkMode && !isCustomBackgroundEnabled) { + updateShadow(true, 2.dp) + } + } - /** - * 保存卡片配置到SharedPreferences - */ fun save(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val prefs = context.getSharedPreferences("card_settings", Context.MODE_PRIVATE) prefs.edit().apply { - putFloat("card_alpha", cardAlpha) - putFloat("card_dim", cardDim) - putBoolean("custom_background_enabled", isCustomBackgroundEnabled) - putBoolean("is_shadow_enabled", isShadowEnabled) - putBoolean("is_custom_alpha_set", isCustomAlphaSet) - putBoolean("is_custom_dim_set", isCustomDimSet) - putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled) - putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled) + putFloat(Keys.CARD_ALPHA, cardAlpha) + putFloat(Keys.CARD_DIM, cardDim) + putBoolean(Keys.CUSTOM_BACKGROUND_ENABLED, isCustomBackgroundEnabled) + putBoolean(Keys.IS_SHADOW_ENABLED, isShadowEnabled) + putBoolean(Keys.IS_CUSTOM_ALPHA_SET, isCustomAlphaSet) + putBoolean(Keys.IS_CUSTOM_DIM_SET, isCustomDimSet) + putBoolean(Keys.IS_USER_DARK_MODE_ENABLED, isUserDarkModeEnabled) + putBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, isUserLightModeEnabled) apply() } } - /** - * 从SharedPreferences加载卡片配置 - */ fun load(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - cardAlpha = prefs.getFloat("card_alpha", 1f) - cardDim = prefs.getFloat("card_dim", 0f) - isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false) - isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true) - isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false) - isCustomDimSet = prefs.getBoolean("is_custom_dim_set", false) - isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false) - isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false) - updateShadowEnabled(isShadowEnabled) + val prefs = context.getSharedPreferences("card_settings", Context.MODE_PRIVATE) + cardAlpha = prefs.getFloat(Keys.CARD_ALPHA, 1f).coerceIn(0f, 1f) + cardDim = prefs.getFloat(Keys.CARD_DIM, 0f).coerceIn(0f, 1f) + isCustomBackgroundEnabled = prefs.getBoolean(Keys.CUSTOM_BACKGROUND_ENABLED, false) + isShadowEnabled = prefs.getBoolean(Keys.IS_SHADOW_ENABLED, true) + isCustomAlphaSet = prefs.getBoolean(Keys.IS_CUSTOM_ALPHA_SET, false) + isCustomDimSet = prefs.getBoolean(Keys.IS_CUSTOM_DIM_SET, false) + isUserDarkModeEnabled = prefs.getBoolean(Keys.IS_USER_DARK_MODE_ENABLED, false) + isUserLightModeEnabled = prefs.getBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, false) + + // 应用阴影设置 + updateShadow(isShadowEnabled, if (isShadowEnabled) cardElevation else 0.dp) } - /** - * 更新阴影启用状态 - */ + @Deprecated("使用 updateShadow 替代", ReplaceWith("updateShadow(enabled)")) fun updateShadowEnabled(enabled: Boolean) { - isShadowEnabled = enabled - cardElevation = 0.dp - } - - /** - * 设置主题模式默认值 - */ - fun setThemeDefaults(isDarkMode: Boolean) { - if (!isCustomAlphaSet) { - cardAlpha = 1f - } - if (!isCustomDimSet) { - cardDim = if (isDarkMode) 0.5f else 0f - } - updateShadowEnabled(isShadowEnabled) + updateShadow(enabled) } } -/** - * 获取卡片颜色配置 - */ -@Composable -fun getCardColors(originalColor: Color) = CardDefaults.cardColors( - containerColor = originalColor.copy(alpha = CardConfig.cardAlpha), - contentColor = determineContentColor(originalColor) -) +object CardStyleProvider { -/** - * 获取卡片阴影配置 - */ -@Composable -fun getCardElevation() = CardDefaults.cardElevation( - defaultElevation = CardConfig.cardElevation, - pressedElevation = CardConfig.cardElevation, - focusedElevation = CardConfig.cardElevation, - hoveredElevation = CardConfig.cardElevation, - draggedElevation = CardConfig.cardElevation, - disabledElevation = CardConfig.cardElevation -) + @Composable + fun getCardColors(originalColor: Color) = CardDefaults.cardColors( + containerColor = originalColor.copy(alpha = CardConfig.cardAlpha), + contentColor = determineContentColor(originalColor), + disabledContainerColor = originalColor.copy(alpha = CardConfig.cardAlpha * 0.38f), + disabledContentColor = determineContentColor(originalColor).copy(alpha = 0.38f) + ) -/** - * 根据背景颜色、主题模式和用户设置确定内容颜色 - */ -@Composable -private fun determineContentColor(originalColor: Color): Color { - val isDarkTheme = isSystemInDarkTheme() - if (ThemeConfig.isThemeChanging) { - return if (isDarkTheme) Color.White else Color.Black - } + @Composable + fun getCardElevation() = CardDefaults.cardElevation( + defaultElevation = CardConfig.cardElevation, + pressedElevation = if (CardConfig.isShadowEnabled) { + (CardConfig.cardElevation.value + 0).dp + } else 0.dp, + focusedElevation = if (CardConfig.isShadowEnabled) { + (CardConfig.cardElevation.value + 0).dp + } else 0.dp, + hoveredElevation = if (CardConfig.isShadowEnabled) { + (CardConfig.cardElevation.value + 0).dp + } else 0.dp, + draggedElevation = if (CardConfig.isShadowEnabled) { + (CardConfig.cardElevation.value + 0).dp + } else 0.dp, + disabledElevation = 0.dp + ) - return when { - CardConfig.isUserLightModeEnabled -> Color.Black - !isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black - isDarkTheme -> Color.White - else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White + @Composable + private fun determineContentColor(originalColor: Color): Color { + val isDarkTheme = isSystemInDarkTheme() + + return when { + ThemeConfig.isThemeChanging -> { + if (isDarkTheme) Color.White else Color.Black + } + CardConfig.isUserLightModeEnabled -> Color.Black + CardConfig.isUserDarkModeEnabled -> Color.White + else -> { + val luminance = originalColor.luminance() + val threshold = if (isDarkTheme) 0.4f else 0.6f + if (luminance > threshold) Color.Black else Color.White + } + } } } + +// 向后兼容 +@Composable +fun getCardColors(originalColor: Color) = CardStyleProvider.getCardColors(originalColor) + +@Composable +fun getCardElevation() = CardStyleProvider.getCardElevation() diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt index ad8cb63..87ac86d 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt @@ -1,6 +1,5 @@ package com.sukisu.ultra.ui.theme -import android.content.ContentResolver import android.content.Context import android.net.Uri import android.os.Build @@ -9,9 +8,7 @@ import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box @@ -28,36 +25,38 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.core.content.edit import androidx.core.net.toUri import coil.compose.AsyncImagePainter import coil.compose.rememberAsyncImagePainter -import com.sukisu.ultra.ui.util.BackgroundTransformation -import com.sukisu.ultra.ui.util.saveTransformedBackground +import com.sukisu.ultra.ui.theme.util.BackgroundTransformation +import com.sukisu.ultra.ui.theme.util.saveTransformedBackground +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.launch import java.io.File import java.io.FileOutputStream -import java.io.InputStream -/** - * 主题配置对象,管理应用的主题相关状态 - */ +@Stable object ThemeConfig { + // 主题状态 var customBackgroundUri by mutableStateOf(null) var forceDarkMode by mutableStateOf(null) var currentTheme by mutableStateOf(ThemeColors.Default) var useDynamicColor by mutableStateOf(false) + + // 背景状态 var backgroundImageLoaded by mutableStateOf(false) - var needsResetOnThemeChange by mutableStateOf(false) var isThemeChanging by mutableStateOf(false) var preventBackgroundRefresh by mutableStateOf(false) + // 主题变化检测 private var lastDarkModeState: Boolean? = null + fun detectThemeChange(currentDarkMode: Boolean): Boolean { - val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode + val hasChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode lastDarkModeState = currentDarkMode - return isChanged + return hasChanged } fun resetBackgroundState() { @@ -66,11 +65,171 @@ object ThemeConfig { } isThemeChanging = true } + + fun updateTheme( + theme: ThemeColors? = null, + dynamicColor: Boolean? = null, + darkMode: Boolean? = null + ) { + theme?.let { currentTheme = it } + dynamicColor?.let { useDynamicColor = it } + darkMode?.let { forceDarkMode = it } + } + + fun reset() { + customBackgroundUri = null + forceDarkMode = null + currentTheme = ThemeColors.Default + useDynamicColor = false + backgroundImageLoaded = false + isThemeChanging = false + preventBackgroundRefresh = false + lastDarkModeState = null + } +} + +object ThemeManager { + private const val PREFS_NAME = "theme_prefs" + + fun saveThemeMode(context: Context, forceDark: Boolean?) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { + putString("theme_mode", when (forceDark) { + true -> "dark" + false -> "light" + null -> "system" + }) + } + ThemeConfig.forceDarkMode = forceDark + } + + fun loadThemeMode(context: Context) { + val mode = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString("theme_mode", "system") + + ThemeConfig.forceDarkMode = when (mode) { + "dark" -> true + "light" -> false + else -> null + } + } + + fun saveThemeColors(context: Context, themeName: String) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { + putString("theme_colors", themeName) + } + ThemeConfig.currentTheme = ThemeColors.fromName(themeName) + } + + fun loadThemeColors(context: Context) { + val themeName = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString("theme_colors", "default") ?: "default" + ThemeConfig.currentTheme = ThemeColors.fromName(themeName) + } + + fun saveDynamicColorState(context: Context, enabled: Boolean) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { + putBoolean("use_dynamic_color", enabled) + } + ThemeConfig.useDynamicColor = enabled + } + + + fun loadDynamicColorState(context: Context) { + val enabled = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getBoolean("use_dynamic_color", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + ThemeConfig.useDynamicColor = enabled + } +} + +object BackgroundManager { + private const val TAG = "BackgroundManager" + + fun saveAndApplyCustomBackground( + context: Context, + uri: Uri, + transformation: BackgroundTransformation? = null + ) { + try { + val finalUri = if (transformation != null) { + context.saveTransformedBackground(uri, transformation) + } else { + copyImageToInternalStorage(context, uri) + } + + saveBackgroundUri(context, finalUri) + ThemeConfig.customBackgroundUri = finalUri + CardConfig.updateBackground(true) + resetBackgroundState(context) + + } catch (e: Exception) { + Log.e(TAG, "保存背景失败: ${e.message}", e) + } + } + + fun clearCustomBackground(context: Context) { + saveBackgroundUri(context, null) + ThemeConfig.customBackgroundUri = null + CardConfig.updateBackground(false) + resetBackgroundState(context) + } + + fun loadCustomBackground(context: Context) { + val uriString = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) + .getString("custom_background", null) + + val newUri = uriString?.toUri() + val preventRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) + .getBoolean("prevent_background_refresh", false) + + ThemeConfig.preventBackgroundRefresh = preventRefresh + + if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) { + Log.d(TAG, "加载自定义背景: $uriString") + ThemeConfig.customBackgroundUri = newUri + ThemeConfig.backgroundImageLoaded = false + CardConfig.updateBackground(newUri != null) + } + } + + private fun saveBackgroundUri(context: Context, uri: Uri?) { + context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { + putString("custom_background", uri?.toString()) + putBoolean("prevent_background_refresh", false) + } + } + + private fun resetBackgroundState(context: Context) { + ThemeConfig.backgroundImageLoaded = false + ThemeConfig.preventBackgroundRefresh = false + context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { + putBoolean("prevent_background_refresh", false) + } + } + + private fun copyImageToInternalStorage(context: Context, uri: Uri): Uri? { + return try { + val inputStream = context.contentResolver.openInputStream(uri) ?: return null + val fileName = "custom_background_${System.currentTimeMillis()}.jpg" + val file = File(context.filesDir, fileName) + + FileOutputStream(file).use { outputStream -> + val buffer = ByteArray(8 * 1024) + var read: Int + while (inputStream.read(buffer).also { read = it } != -1) { + outputStream.write(buffer, 0, read) + } + outputStream.flush() + } + inputStream.close() + + Uri.fromFile(file) + } catch (e: Exception) { + Log.e(TAG, "复制图片失败: ${e.message}", e) + null + } + } } -/** - * 应用主题 - */ @Composable fun KernelSUTheme( darkTheme: Boolean = when(ThemeConfig.forceDarkMode) { @@ -84,198 +243,223 @@ fun KernelSUTheme( val context = LocalContext.current val systemIsDark = isSystemInDarkTheme() - // 检测系统主题变化并保存状态 - val themeChanged = ThemeConfig.detectThemeChange(systemIsDark) - LaunchedEffect(systemIsDark, themeChanged) { - if (ThemeConfig.forceDarkMode == null && themeChanged) { - Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark") - ThemeConfig.resetBackgroundState() - - if (!ThemeConfig.preventBackgroundRefresh) { - context.loadCustomBackground() - } - - CardConfig.apply { - load(context) - if (!isCustomAlphaSet) { - cardAlpha = if (systemIsDark) 0.50f else 1f - } - if (!isCustomDimSet) { - cardDim = if (systemIsDark) 0.5f else 0f - } - save(context) - } - } - } - - SystemBarStyle( - darkMode = darkTheme - ) - - // 初始加载配置 - LaunchedEffect(Unit) { - context.loadThemeMode() - context.loadThemeColors() - context.loadDynamicColorState() - CardConfig.load(context) - - if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) { - context.loadCustomBackground() - ThemeConfig.backgroundImageLoaded = false - } - - ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getBoolean("prevent_background_refresh", true) - } + // 初始化主题 + ThemeInitializer(context = context, systemIsDark = systemIsDark) // 创建颜色方案 - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context) - } - darkTheme -> createDarkColorScheme() - else -> createLightColorScheme() - } + val colorScheme = createColorScheme(context, darkTheme, dynamicColor) - // 根据暗色模式和自定义背景调整卡片配置 - val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null - if (darkTheme && !dynamicColor) { - CardConfig.setThemeDefaults(true) - } else if (!darkTheme && !dynamicColor) { - CardConfig.setThemeDefaults(false) - } - CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground) - - val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) } - - LaunchedEffect(ThemeConfig.customBackgroundUri) { - backgroundUri.value = ThemeConfig.customBackgroundUri - } - - val bgImagePainter = backgroundUri.value?.let { - rememberAsyncImagePainter( - model = it, - onError = { err -> - Log.e("ThemeSystem", "背景图加载失败: ${err.result.throwable.message}") - ThemeConfig.customBackgroundUri = null - context.saveCustomBackground(null) - }, - onSuccess = { - Log.d("ThemeSystem", "背景图加载成功") - ThemeConfig.backgroundImageLoaded = true - ThemeConfig.isThemeChanging = false - - ThemeConfig.preventBackgroundRefresh = true - context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { putBoolean("prevent_background_refresh", true) } - } - ) - } - - val transition = updateTransition( - targetState = ThemeConfig.backgroundImageLoaded, - label = "bgTransition" - ) - val bgAlpha by transition.animateFloat( - label = "bgAlpha", - transitionSpec = { - spring( - dampingRatio = 0.8f, - stiffness = 300f - ) - } - ) { loaded -> if (loaded) 1f else 0f } - - DisposableEffect(systemIsDark) { - onDispose { - if (ThemeConfig.isThemeChanging) { - ThemeConfig.isThemeChanging = false - } - } - } - - // 计算适用的暗化值 - val dimFactor = CardConfig.cardDim + // 系统栏样式 + SystemBarController(darkTheme) MaterialTheme( colorScheme = colorScheme, typography = Typography ) { Box(modifier = Modifier.fillMaxSize()) { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(-2f) - .background(if (darkTheme) if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background } - else if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background }) - ) - - // 自定义背景层 - backgroundUri.value?.let { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(-1f) - .alpha(bgAlpha) - ) { - // 背景图片 - bgImagePainter?.let { painter -> - Box( - modifier = Modifier - .fillMaxSize() - .paint( - painter = painter, - contentScale = ContentScale.Crop - ) - .graphicsLayer { - alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f - } - ) - } - - // 亮度调节层 (根据cardDim调整) - Box( - modifier = Modifier - .fillMaxSize() - .background( - if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f) - else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f) - ) - ) - - // 边缘渐变遮罩 - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.radialGradient( - colors = listOf( - Color.Transparent, - if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f) - else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f) - ), - radius = 1200f - ) - ) - ) - } - } - + // 背景层 + BackgroundLayer(darkTheme) // 内容层 - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(1f) - ) { + Box(modifier = Modifier.fillMaxSize().zIndex(1f)) { content() } } } } -/** - * 创建动态深色颜色方案 - */ +@Composable +private fun ThemeInitializer(context: Context, systemIsDark: Boolean) { + val themeChanged = ThemeConfig.detectThemeChange(systemIsDark) + val scope = rememberCoroutineScope() + + // 处理系统主题变化 + LaunchedEffect(systemIsDark, themeChanged) { + if (ThemeConfig.forceDarkMode == null && themeChanged) { + Log.d("ThemeSystem", "系统主题变化: $systemIsDark") + ThemeConfig.resetBackgroundState() + + if (!ThemeConfig.preventBackgroundRefresh) { + BackgroundManager.loadCustomBackground(context) + } + + CardConfig.apply { + load(context) + setThemeDefaults(systemIsDark) + save(context) + } + } + } + + // 初始加载配置 + LaunchedEffect(Unit) { + scope.launch { + ThemeManager.loadThemeMode(context) + ThemeManager.loadThemeColors(context) + ThemeManager.loadDynamicColorState(context) + CardConfig.load(context) + + if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) { + BackgroundManager.loadCustomBackground(context) + } + } + } +} + +@Composable +private fun BackgroundLayer(darkTheme: Boolean) { + val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) } + + LaunchedEffect(ThemeConfig.customBackgroundUri) { + backgroundUri.value = ThemeConfig.customBackgroundUri + } + + // 默认背景 + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(-2f) + .background( + if (CardConfig.isCustomBackgroundEnabled) { + MaterialTheme.colorScheme.surfaceContainerLow + } else { + MaterialTheme.colorScheme.background + } + ) + ) + + // 自定义背景 + backgroundUri.value?.let { uri -> + CustomBackgroundLayer(uri = uri, darkTheme = darkTheme) + } +} + +@Composable +private fun CustomBackgroundLayer(uri: Uri, darkTheme: Boolean) { + val painter = rememberAsyncImagePainter( + model = uri, + onError = { error -> + Log.e("ThemeSystem", "背景加载失败: ${error.result.throwable.message}") + ThemeConfig.customBackgroundUri = null + }, + onSuccess = { + Log.d("ThemeSystem", "背景加载成功") + ThemeConfig.backgroundImageLoaded = true + ThemeConfig.isThemeChanging = false + } + ) + + val transition = updateTransition( + targetState = ThemeConfig.backgroundImageLoaded, + label = "backgroundTransition" + ) + + val alpha by transition.animateFloat( + label = "backgroundAlpha", + transitionSpec = { + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ) + } + ) { loaded -> if (loaded) 1f else 0f } + + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(-1f) + .alpha(alpha) + ) { + // 背景图片 + Box( + modifier = Modifier + .fillMaxSize() + .paint(painter = painter, contentScale = ContentScale.Crop) + .graphicsLayer { + this.alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f + } + ) + + // 遮罩层 + BackgroundOverlay(darkTheme = darkTheme) + } +} + +@Composable +private fun BackgroundOverlay(darkTheme: Boolean) { + val dimFactor = CardConfig.cardDim + + // 主要遮罩层 + Box( + modifier = Modifier + .fillMaxSize() + .background( + if (darkTheme) { + Color.Black.copy(alpha = 0.3f + dimFactor * 0.4f) + } else { + Color.White.copy(alpha = 0.05f + dimFactor * 0.3f) + } + ) + ) + + // 边缘渐变遮罩 + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.radialGradient( + colors = listOf( + Color.Transparent, + if (darkTheme) { + Color.Black.copy(alpha = 0.2f + dimFactor * 0.2f) + } else { + Color.Black.copy(alpha = 0.05f + dimFactor * 0.1f) + } + ), + radius = 1000f + ) + ) + ) +} + +@Composable +private fun createColorScheme( + context: Context, + darkTheme: Boolean, + dynamicColor: Boolean +): ColorScheme { + return when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + if (darkTheme) createDynamicDarkColorScheme(context) + else createDynamicLightColorScheme(context) + } + darkTheme -> createDarkColorScheme() + else -> createLightColorScheme() + } +} + +@Composable +private fun SystemBarController(darkMode: Boolean) { + val context = LocalContext.current + val activity = context as ComponentActivity + + SideEffect { + activity.enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto( + Color.Transparent.toArgb(), + Color.Transparent.toArgb(), + ) { darkMode }, + navigationBarStyle = if (darkMode) { + SystemBarStyle.dark(Color.Transparent.toArgb()) + } else { + SystemBarStyle.light( + Color.Transparent.toArgb(), + Color.Transparent.toArgb() + ) + } + ) + } +} + @RequiresApi(Build.VERSION_CODES.S) @Composable private fun createDynamicDarkColorScheme(context: Context): ColorScheme { @@ -288,9 +472,6 @@ private fun createDynamicDarkColorScheme(context: Context): ColorScheme { ) } -/** - * 创建动态浅色颜色方案 - */ @RequiresApi(Build.VERSION_CODES.S) @Composable private fun createDynamicLightColorScheme(context: Context): ColorScheme { @@ -303,11 +484,6 @@ private fun createDynamicLightColorScheme(context: Context): ColorScheme { ) } - - -/** - * 创建深色颜色方案 - */ @Composable private fun createDarkColorScheme() = darkColorScheme( primary = ThemeConfig.currentTheme.primaryDark, @@ -347,9 +523,6 @@ private fun createDarkColorScheme() = darkColorScheme( surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestDark, ) -/** - * 创建浅色颜色方案 - */ @Composable private fun createLightColorScheme() = lightColorScheme( primary = ThemeConfig.currentTheme.primaryLight, @@ -389,218 +562,32 @@ private fun createLightColorScheme() = lightColorScheme( surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight, ) - -/** - * 复制图片到应用内部存储并提升持久性 - */ -private fun Context.copyImageToInternalStorage(uri: Uri): Uri? { - return try { - val contentResolver: ContentResolver = contentResolver - val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null - - val fileName = "custom_background.jpg" - val file = File(filesDir, fileName) - - val backupFile = File(filesDir, "${fileName}.backup") - val outputStream = FileOutputStream(backupFile) - val buffer = ByteArray(4 * 1024) - var read: Int - - while (inputStream.read(buffer).also { read = it } != -1) { - outputStream.write(buffer, 0, read) - } - - outputStream.flush() - outputStream.close() - inputStream.close() - - if (file.exists()) { - file.delete() - } - backupFile.renameTo(file) - - Uri.fromFile(file) - } catch (e: Exception) { - Log.e("ImageCopy", "复制图片失败: ${e.message}") - null - } -} - -/** - * 保存并应用自定义背景 - */ +// 向后兼容 +@OptIn(DelicateCoroutinesApi::class) fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) { - val finalUri = if (transformation != null) { - saveTransformedBackground(uri, transformation) - } else { - copyImageToInternalStorage(uri) + kotlinx.coroutines.GlobalScope.launch { + BackgroundManager.saveAndApplyCustomBackground(this@saveAndApplyCustomBackground, uri, transformation) } - - // 保存到配置文件 - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString("custom_background", finalUri?.toString()) - // 设置阻止刷新标志为false,允许新设置的背景加载一次 - putBoolean("prevent_background_refresh", false) - } - - ThemeConfig.customBackgroundUri = finalUri - ThemeConfig.backgroundImageLoaded = false - ThemeConfig.preventBackgroundRefresh = false - CardConfig.cardElevation = 0.dp - CardConfig.isCustomBackgroundEnabled = true } -/** - * 保存自定义背景 - */ fun Context.saveCustomBackground(uri: Uri?) { - val newUri = uri?.let { copyImageToInternalStorage(it) } - - // 保存到配置文件 - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString("custom_background", newUri?.toString()) - if (uri == null) { - // 如果清除背景,也重置阻止刷新标志 - putBoolean("prevent_background_refresh", false) - } else { - // 设置阻止刷新标志为false,允许新设置的背景加载一次 - putBoolean("prevent_background_refresh", false) - } - } - - ThemeConfig.customBackgroundUri = newUri - ThemeConfig.backgroundImageLoaded = false - ThemeConfig.preventBackgroundRefresh = false - if (uri != null) { - CardConfig.cardElevation = 0.dp - CardConfig.isCustomBackgroundEnabled = true + saveAndApplyCustomBackground(uri) + } else { + BackgroundManager.clearCustomBackground(this) } } -/** - * 加载自定义背景 - */ -fun Context.loadCustomBackground() { - val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getString("custom_background", null) - - val newUri = uriString?.toUri() - val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getBoolean("prevent_background_refresh", false) - - ThemeConfig.preventBackgroundRefresh = preventRefresh - - if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) { - Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh") - ThemeConfig.customBackgroundUri = newUri - ThemeConfig.backgroundImageLoaded = false - } -} - -/** - * 保存主题模式 - */ fun Context.saveThemeMode(forceDark: Boolean?) { - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString( - "theme_mode", when (forceDark) { - true -> "dark" - false -> "light" - null -> "system" - } - ) - } - ThemeConfig.forceDarkMode = forceDark - ThemeConfig.needsResetOnThemeChange = forceDark == null + ThemeManager.saveThemeMode(this, forceDark) } -/** - * 加载主题模式 - */ -fun Context.loadThemeMode() { - val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getString("theme_mode", "system") - ThemeConfig.forceDarkMode = when(mode) { - "dark" -> true - "light" -> false - else -> null - } - ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null -} - -/** - * 保存主题颜色 - */ fun Context.saveThemeColors(themeName: String) { - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString("theme_colors", themeName) - } - - ThemeConfig.currentTheme = ThemeColors.fromName(themeName) + ThemeManager.saveThemeColors(this, themeName) } -/** - * 加载主题颜色 - */ -fun Context.loadThemeColors() { - val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getString("theme_colors", "default") - ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default") -} - -/** - * 保存动态颜色状态 - */ fun Context.saveDynamicColorState(enabled: Boolean) { - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putBoolean("use_dynamic_color", enabled) - } - ThemeConfig.useDynamicColor = enabled -} - -/** - * 加载动态颜色状态 - */ -fun Context.loadDynamicColorState() { - val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getBoolean("use_dynamic_color", true) - - ThemeConfig.useDynamicColor = enabled -} - -@Composable -private fun SystemBarStyle( - darkMode: Boolean, - statusBarScrim: Color = Color.Transparent, - navigationBarScrim: Color = Color.Transparent, -) { - val context = LocalContext.current - val activity = context as ComponentActivity - - SideEffect { - activity.enableEdgeToEdge( - statusBarStyle = SystemBarStyle.auto( - statusBarScrim.toArgb(), - statusBarScrim.toArgb(), - ) { darkMode }, - navigationBarStyle = when { - darkMode -> SystemBarStyle.dark( - navigationBarScrim.toArgb() - ) - - else -> SystemBarStyle.light( - navigationBarScrim.toArgb(), - navigationBarScrim.toArgb(), - ) - } - ) - } + ThemeManager.saveDynamicColorState(this, enabled) } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt new file mode 100644 index 0000000..803d1f0 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt @@ -0,0 +1,411 @@ +package com.sukisu.ultra.ui.theme.component + +import android.net.Uri +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Fullscreen +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.theme.util.BackgroundTransformation +import com.sukisu.ultra.ui.theme.util.saveTransformedBackground +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.max + +@Composable +fun ImageEditorDialog( + imageUri: Uri, + onDismiss: () -> Unit, + onConfirm: (Uri) -> Unit +) { + // 图像变换状态 + val transformState = remember { ImageTransformState() } + val context = LocalContext.current + val scope = rememberCoroutineScope() + + // 尺寸状态 + var imageSize by remember { mutableStateOf(Size.Zero) } + var screenSize by remember { mutableStateOf(Size.Zero) } + + // 动画状态 + val animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ) + + val animatedScale by animateFloatAsState( + targetValue = transformState.scale, + animationSpec = animationSpec, + label = "ScaleAnimation" + ) + + val animatedOffsetX by animateFloatAsState( + targetValue = transformState.offsetX, + animationSpec = animationSpec, + label = "OffsetXAnimation" + ) + + val animatedOffsetY by animateFloatAsState( + targetValue = transformState.offsetY, + animationSpec = animationSpec, + label = "OffsetYAnimation" + ) + + // 工具函数 + val scaleToFullScreen = remember { + { + if (imageSize.height > 0 && screenSize.height > 0) { + val newScale = screenSize.height / imageSize.height + transformState.updateTransform(newScale, 0f, 0f) + } + } + } + + val saveImage: () -> Unit = remember { + { + scope.launch { + try { + val transformation = BackgroundTransformation( + transformState.scale, + transformState.offsetX, + transformState.offsetY + ) + val savedUri = context.saveTransformedBackground(imageUri, transformation) + savedUri?.let { onConfirm(it) } + } catch (_: Exception) { + } + } + } + } + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = false, + usePlatformDefaultWidth = false + ) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.radialGradient( + colors = listOf( + Color.Black.copy(alpha = 0.9f), + Color.Black.copy(alpha = 0.95f) + ), + radius = 800f + ) + ) + .onSizeChanged { size -> + screenSize = Size(size.width.toFloat(), size.height.toFloat()) + } + ) { + // 图像显示区域 + ImageDisplayArea( + imageUri = imageUri, + animatedScale = animatedScale, + animatedOffsetX = animatedOffsetX, + animatedOffsetY = animatedOffsetY, + transformState = transformState, + onImageSizeChanged = { imageSize = it }, + modifier = Modifier.fillMaxSize() + ) + + // 顶部工具栏 + TopToolbar( + onDismiss = onDismiss, + onFullscreen = scaleToFullScreen, + onConfirm = saveImage, + modifier = Modifier.align(Alignment.TopCenter) + ) + + // 底部提示信息 + BottomHintCard( + modifier = Modifier.align(Alignment.BottomCenter) + ) + } + } +} + +/** + * 图像变换状态管理类 + */ +private class ImageTransformState { + var scale by mutableFloatStateOf(1f) + var offsetX by mutableFloatStateOf(0f) + var offsetY by mutableFloatStateOf(0f) + + private var lastScale = 1f + private var lastOffsetX = 0f + private var lastOffsetY = 0f + + fun updateTransform(newScale: Float, newOffsetX: Float, newOffsetY: Float) { + val scaleDiff = abs(newScale - lastScale) + val offsetXDiff = abs(newOffsetX - lastOffsetX) + val offsetYDiff = abs(newOffsetY - lastOffsetY) + + if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) { + scale = newScale + offsetX = newOffsetX + offsetY = newOffsetY + lastScale = newScale + lastOffsetX = newOffsetX + lastOffsetY = newOffsetY + } + } + + fun resetToLast() { + scale = lastScale + offsetX = lastOffsetX + offsetY = lastOffsetY + } +} + +/** + * 图像显示区域组件 + */ +@Composable +private fun ImageDisplayArea( + imageUri: Uri, + animatedScale: Float, + animatedOffsetX: Float, + animatedOffsetY: Float, + transformState: ImageTransformState, + onImageSizeChanged: (Size) -> Unit, + modifier: Modifier = Modifier +) { + val scope = rememberCoroutineScope() + + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUri) + .crossfade(true) + .build(), + contentDescription = stringResource(R.string.settings_custom_background), + contentScale = ContentScale.Fit, + modifier = modifier + .graphicsLayer( + scaleX = animatedScale, + scaleY = animatedScale, + translationX = animatedOffsetX, + translationY = animatedOffsetY + ) + .pointerInput(Unit) { + detectTransformGestures { _, pan, zoom, _ -> + scope.launch { + try { + val newScale = (transformState.scale * zoom).coerceIn(0.5f, 3f) + val maxOffsetX = max(0f, size.width * (newScale - 1) / 2) + val maxOffsetY = max(0f, size.height * (newScale - 1) / 2) + + val newOffsetX = if (maxOffsetX > 0) { + (transformState.offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX) + } else 0f + + val newOffsetY = if (maxOffsetY > 0) { + (transformState.offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY) + } else 0f + + transformState.updateTransform(newScale, newOffsetX, newOffsetY) + } catch (_: Exception) { + transformState.resetToLast() + } + } + } + } + .onSizeChanged { size -> + onImageSizeChanged(Size(size.width.toFloat(), size.height.toFloat())) + } + ) +} + +/** + * 顶部工具栏组件 + */ +@Composable +private fun TopToolbar( + onDismiss: () -> Unit, + onFullscreen: () -> Unit, + onConfirm: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(24.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + // 关闭按钮 + ActionButton( + onClick = onDismiss, + icon = Icons.Default.Close, + contentDescription = stringResource(R.string.cancel), + backgroundColor = MaterialTheme.colorScheme.error.copy(alpha = 0.9f) + ) + + // 全屏按钮 + ActionButton( + onClick = onFullscreen, + icon = Icons.Default.Fullscreen, + contentDescription = stringResource(R.string.reprovision), + backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.9f) + ) + + // 确认按钮 + ActionButton( + onClick = onConfirm, + icon = Icons.Default.Check, + contentDescription = stringResource(R.string.confirm), + backgroundColor = Color(0xFF4CAF50).copy(alpha = 0.9f) + ) + } +} + +/** + * 操作按钮组件 + */ +@Composable +private fun ActionButton( + onClick: () -> Unit, + icon: androidx.compose.ui.graphics.vector.ImageVector, + contentDescription: String, + backgroundColor: Color, + modifier: Modifier = Modifier +) { + var isPressed by remember { mutableStateOf(false) } + + val buttonScale by animateFloatAsState( + targetValue = if (isPressed) 0.85f else 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessHigh + ), + label = "ButtonScale" + ) + + val buttonAlpha by animateFloatAsState( + targetValue = if (isPressed) 0.8f else 1f, + animationSpec = tween(100), + label = "ButtonAlpha" + ) + + Surface( + onClick = { + isPressed = true + onClick() + }, + modifier = modifier + .size(64.dp) + .graphicsLayer( + scaleX = buttonScale, + scaleY = buttonScale, + alpha = buttonAlpha + ), + shape = CircleShape, + color = backgroundColor, + shadowElevation = 8.dp + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Icon( + imageVector = icon, + contentDescription = contentDescription, + tint = Color.White, + modifier = Modifier.size(28.dp) + ) + } + } + + LaunchedEffect(isPressed) { + if (isPressed) { + kotlinx.coroutines.delay(150) + isPressed = false + } + } +} + +/** + * 底部提示卡片组件 + */ +@Composable +private fun BottomHintCard( + modifier: Modifier = Modifier +) { + var isVisible by remember { mutableStateOf(true) } + + val cardAlpha by animateFloatAsState( + targetValue = if (isVisible) 1f else 0f, + animationSpec = tween( + durationMillis = 500, + easing = EaseInOutCubic + ), + label = "HintAlpha" + ) + + val cardTranslationY by animateFloatAsState( + targetValue = if (isVisible) 0f else 100f, + animationSpec = tween( + durationMillis = 500, + easing = EaseInOutCubic + ), + label = "HintTranslation" + ) + + LaunchedEffect(Unit) { + kotlinx.coroutines.delay(4000) + isVisible = false + } + + Card( + modifier = modifier + .fillMaxWidth() + .padding(24.dp) + .alpha(cardAlpha) + .graphicsLayer { + translationY = cardTranslationY + }, + colors = CardDefaults.cardColors( + containerColor = Color.Black.copy(alpha = 0.85f) + ), + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 12.dp) + ) { + Text( + text = stringResource(id = R.string.image_editor_hint), + color = Color.White, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/util/BackgroundUtils.kt similarity index 98% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/theme/util/BackgroundUtils.kt index 1f66221..daf089b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/util/BackgroundUtils.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.theme.util import android.content.ContentResolver import android.content.Context diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java deleted file mode 100644 index fe1ebe6..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java +++ /dev/null @@ -1,572 +0,0 @@ -package com.sukisu.ultra.ui.util; -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.text.TextUtils; -import android.util.Log; - -import java.text.Collator; -import java.util.ArrayList; -import java.util.Locale; - -/** - * An object to convert Chinese character to its corresponding pinyin string. For characters with - * multiple possible pinyin string, only one is selected according to collator. Polyphone is not - * supported in this implementation. This class is implemented to achieve the best runtime - * performance and minimum runtime resources with tolerable sacrifice of accuracy. This - * implementation highly depends on zh_CN ICU collation data and must be always synchronized with - * ICU. - *

- * Currently this file is aligned to zh.txt in ICU 4.6 - */ -@SuppressWarnings("SizeReplaceableByIsEmpty") -public record HanziToPinyin(boolean mHasChinaCollator) { - private static final String TAG = "HanziToPinyin"; - - // Turn on this flag when we want to check internal data structure. - private static final boolean DEBUG = false; - - /** - * Unihans array. - *

- * Each unihans is the first one within same pinyin when collator is zh_CN. - */ - public static final char[] UNIHANS = { - '阿', '哎', '安', '肮', '凹', '八', - '挀', '扳', '邦', '勹', '陂', '奔', - '伻', '屄', '边', '灬', '憋', '汃', - '冫', '癶', '峬', '嚓', '偲', '参', - '仓', '撡', '冊', '嵾', '曽', '曾', - '層', '叉', '芆', '辿', '伥', '抄', - '车', '抻', '沈', '沉', '阷', '吃', - '充', '抽', '出', '欻', '揣', '巛', - '刅', '吹', '旾', '逴', '呲', '匆', - '凑', '粗', '汆', '崔', '邨', '搓', - '咑', '呆', '丹', '当', '刀', '嘚', - '扥', '灯', '氐', '嗲', '甸', '刁', - '爹', '丁', '丟', '东', '吺', '厾', - '耑', '襨', '吨', '多', '妸', '诶', - '奀', '鞥', '儿', '发', '帆', '匚', - '飞', '分', '丰', '覅', '仏', '紑', - '伕', '旮', '侅', '甘', '冈', '皋', - '戈', '给', '根', '刯', '工', '勾', - '估', '瓜', '乖', '关', '光', '归', - '丨', '呙', '哈', '咍', '佄', '夯', - '茠', '诃', '黒', '拫', '亨', '噷', - '叿', '齁', '乯', '花', '怀', '犿', - '巟', '灰', '昏', '吙', '丌', '加', - '戋', '江', '艽', '阶', '巾', '坕', - '冂', '丩', '凥', '姢', '噘', '军', - '咔', '开', '刊', '忼', '尻', '匼', - '肎', '劥', '空', '抠', '扝', '夸', - '蒯', '宽', '匡', '亏', '坤', '扩', - '垃', '来', '兰', '啷', '捞', '肋', - '勒', '崚', '刕', '俩', '奁', '良', - '撩', '列', '拎', '刢', '溜', '囖', - '龙', '瞜', '噜', '娈', '畧', '抡', - '罗', '呣', '妈', '埋', '嫚', '牤', - '猫', '么', '呅', '门', '甿', '咪', - '宀', '喵', '乜', '民', '名', '谬', - '摸', '哞', '毪', '嗯', '拏', '腉', - '囡', '囔', '孬', '疒', '娞', '恁', - '能', '妮', '拈', '嬢', '鸟', '捏', - '囜', '宁', '妞', '农', '羺', '奴', - '奻', '疟', '黁', '郍', '喔', '讴', - '妑', '拍', '眅', '乓', '抛', '呸', - '喷', '匉', '丕', '囨', '剽', '氕', - '姘', '乒', '钋', '剖', '仆', '七', - '掐', '千', '呛', '悄', '癿', '亲', - '狅', '芎', '丘', '区', '峑', '缺', - '夋', '呥', '穣', '娆', '惹', '人', - '扔', '日', '茸', '厹', '邚', '挼', - '堧', '婑', '瞤', '捼', '仨', '毢', - '三', '桒', '掻', '閪', '森', '僧', - '杀', '筛', '山', '伤', '弰', '奢', - '申', '莘', '敒', '升', '尸', '収', - '书', '刷', '衰', '闩', '双', '谁', - '吮', '说', '厶', '忪', '捜', '苏', - '狻', '夊', '孙', '唆', '他', '囼', - '坍', '汤', '夲', '忑', '熥', '剔', - '天', '旫', '帖', '厅', '囲', '偷', - '凸', '湍', '推', '吞', '乇', '穵', - '歪', '弯', '尣', '危', '昷', '翁', - '挝', '乌', '夕', '虲', '仚', '乡', - '灱', '些', '心', '星', '凶', '休', - '吁', '吅', '削', '坃', '丫', '恹', - '央', '幺', '倻', '一', '囙', '应', - '哟', '佣', '优', '扜', '囦', '曰', - '晕', '筠', '筼', '帀', '災', '兂', - '匨', '傮', '则', '贼', '怎', '増', - '扎', '捚', '沾', '张', '长', '長', - '佋', '蜇', '贞', '争', '之', '峙', - '庢', '中', '州', '朱', '抓', '拽', - '专', '妆', '隹', '宒', '卓', '乲', - '宗', '邹', '租', '钻', '厜', '尊', - '昨', '兙', '鿃', '鿄'}; - - /** - * Pinyin array. - *

- * Each pinyin is corresponding to unihans of same - * offset in the unihans array. - */ - public static final byte[][] PINYINS = { - {65, 0, 0, 0, 0, 0}, {65, 73, 0, 0, 0, 0}, - {65, 78, 0, 0, 0, 0}, {65, 78, 71, 0, 0, 0}, - {65, 79, 0, 0, 0, 0}, {66, 65, 0, 0, 0, 0}, - {66, 65, 73, 0, 0, 0}, {66, 65, 78, 0, 0, 0}, - {66, 65, 78, 71, 0, 0}, {66, 65, 79, 0, 0, 0}, - {66, 69, 73, 0, 0, 0}, {66, 69, 78, 0, 0, 0}, - {66, 69, 78, 71, 0, 0}, {66, 73, 0, 0, 0, 0}, - {66, 73, 65, 78, 0, 0}, {66, 73, 65, 79, 0, 0}, - {66, 73, 69, 0, 0, 0}, {66, 73, 78, 0, 0, 0}, - {66, 73, 78, 71, 0, 0}, {66, 79, 0, 0, 0, 0}, - {66, 85, 0, 0, 0, 0}, {67, 65, 0, 0, 0, 0}, - {67, 65, 73, 0, 0, 0}, {67, 65, 78, 0, 0, 0}, - {67, 65, 78, 71, 0, 0}, {67, 65, 79, 0, 0, 0}, - {67, 69, 0, 0, 0, 0}, {67, 69, 78, 0, 0, 0}, - {67, 69, 78, 71, 0, 0}, {90, 69, 78, 71, 0, 0}, - {67, 69, 78, 71, 0, 0}, {67, 72, 65, 0, 0, 0}, - {67, 72, 65, 73, 0, 0}, {67, 72, 65, 78, 0, 0}, - {67, 72, 65, 78, 71, 0}, {67, 72, 65, 79, 0, 0}, - {67, 72, 69, 0, 0, 0}, {67, 72, 69, 78, 0, 0}, - {83, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 0, 0}, - {67, 72, 69, 78, 71, 0}, {67, 72, 73, 0, 0, 0}, - {67, 72, 79, 78, 71, 0}, {67, 72, 79, 85, 0, 0}, - {67, 72, 85, 0, 0, 0}, {67, 72, 85, 65, 0, 0}, - {67, 72, 85, 65, 73, 0}, {67, 72, 85, 65, 78, 0}, - {67, 72, 85, 65, 78, 71}, {67, 72, 85, 73, 0, 0}, - {67, 72, 85, 78, 0, 0}, {67, 72, 85, 79, 0, 0}, - {67, 73, 0, 0, 0, 0}, {67, 79, 78, 71, 0, 0}, - {67, 79, 85, 0, 0, 0}, {67, 85, 0, 0, 0, 0}, - {67, 85, 65, 78, 0, 0}, {67, 85, 73, 0, 0, 0}, - {67, 85, 78, 0, 0, 0}, {67, 85, 79, 0, 0, 0}, - {68, 65, 0, 0, 0, 0}, {68, 65, 73, 0, 0, 0}, - {68, 65, 78, 0, 0, 0}, {68, 65, 78, 71, 0, 0}, - {68, 65, 79, 0, 0, 0}, {68, 69, 0, 0, 0, 0}, - {68, 69, 78, 0, 0, 0}, {68, 69, 78, 71, 0, 0}, - {68, 73, 0, 0, 0, 0}, {68, 73, 65, 0, 0, 0}, - {68, 73, 65, 78, 0, 0}, {68, 73, 65, 79, 0, 0}, - {68, 73, 69, 0, 0, 0}, {68, 73, 78, 71, 0, 0}, - {68, 73, 85, 0, 0, 0}, {68, 79, 78, 71, 0, 0}, - {68, 79, 85, 0, 0, 0}, {68, 85, 0, 0, 0, 0}, - {68, 85, 65, 78, 0, 0}, {68, 85, 73, 0, 0, 0}, - {68, 85, 78, 0, 0, 0}, {68, 85, 79, 0, 0, 0}, - {69, 0, 0, 0, 0, 0}, {69, 73, 0, 0, 0, 0}, - {69, 78, 0, 0, 0, 0}, {69, 78, 71, 0, 0, 0}, - {69, 82, 0, 0, 0, 0}, {70, 65, 0, 0, 0, 0}, - {70, 65, 78, 0, 0, 0}, {70, 65, 78, 71, 0, 0}, - {70, 69, 73, 0, 0, 0}, {70, 69, 78, 0, 0, 0}, - {70, 69, 78, 71, 0, 0}, {70, 73, 65, 79, 0, 0}, - {70, 79, 0, 0, 0, 0}, {70, 79, 85, 0, 0, 0}, - {70, 85, 0, 0, 0, 0}, {71, 65, 0, 0, 0, 0}, - {71, 65, 73, 0, 0, 0}, {71, 65, 78, 0, 0, 0}, - {71, 65, 78, 71, 0, 0}, {71, 65, 79, 0, 0, 0}, - {71, 69, 0, 0, 0, 0}, {71, 69, 73, 0, 0, 0}, - {71, 69, 78, 0, 0, 0}, {71, 69, 78, 71, 0, 0}, - {71, 79, 78, 71, 0, 0}, {71, 79, 85, 0, 0, 0}, - {71, 85, 0, 0, 0, 0}, {71, 85, 65, 0, 0, 0}, - {71, 85, 65, 73, 0, 0}, {71, 85, 65, 78, 0, 0}, - {71, 85, 65, 78, 71, 0}, {71, 85, 73, 0, 0, 0}, - {71, 85, 78, 0, 0, 0}, {71, 85, 79, 0, 0, 0}, - {72, 65, 0, 0, 0, 0}, {72, 65, 73, 0, 0, 0}, - {72, 65, 78, 0, 0, 0}, {72, 65, 78, 71, 0, 0}, - {72, 65, 79, 0, 0, 0}, {72, 69, 0, 0, 0, 0}, - {72, 69, 73, 0, 0, 0}, {72, 69, 78, 0, 0, 0}, - {72, 69, 78, 71, 0, 0}, {72, 77, 0, 0, 0, 0}, - {72, 79, 78, 71, 0, 0}, {72, 79, 85, 0, 0, 0}, - {72, 85, 0, 0, 0, 0}, {72, 85, 65, 0, 0, 0}, - {72, 85, 65, 73, 0, 0}, {72, 85, 65, 78, 0, 0}, - {72, 85, 65, 78, 71, 0}, {72, 85, 73, 0, 0, 0}, - {72, 85, 78, 0, 0, 0}, {72, 85, 79, 0, 0, 0}, - {74, 73, 0, 0, 0, 0}, {74, 73, 65, 0, 0, 0}, - {74, 73, 65, 78, 0, 0}, {74, 73, 65, 78, 71, 0}, - {74, 73, 65, 79, 0, 0}, {74, 73, 69, 0, 0, 0}, - {74, 73, 78, 0, 0, 0}, {74, 73, 78, 71, 0, 0}, - {74, 73, 79, 78, 71, 0}, {74, 73, 85, 0, 0, 0}, - {74, 85, 0, 0, 0, 0}, {74, 85, 65, 78, 0, 0}, - {74, 85, 69, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, - {75, 65, 0, 0, 0, 0}, {75, 65, 73, 0, 0, 0}, - {75, 65, 78, 0, 0, 0}, {75, 65, 78, 71, 0, 0}, - {75, 65, 79, 0, 0, 0}, {75, 69, 0, 0, 0, 0}, - {75, 69, 78, 0, 0, 0}, {75, 69, 78, 71, 0, 0}, - {75, 79, 78, 71, 0, 0}, {75, 79, 85, 0, 0, 0}, - {75, 85, 0, 0, 0, 0}, {75, 85, 65, 0, 0, 0}, - {75, 85, 65, 73, 0, 0}, {75, 85, 65, 78, 0, 0}, - {75, 85, 65, 78, 71, 0}, {75, 85, 73, 0, 0, 0}, - {75, 85, 78, 0, 0, 0}, {75, 85, 79, 0, 0, 0}, - {76, 65, 0, 0, 0, 0}, {76, 65, 73, 0, 0, 0}, - {76, 65, 78, 0, 0, 0}, {76, 65, 78, 71, 0, 0}, - {76, 65, 79, 0, 0, 0}, {76, 69, 0, 0, 0, 0}, - {76, 69, 73, 0, 0, 0}, {76, 69, 78, 71, 0, 0}, - {76, 73, 0, 0, 0, 0}, {76, 73, 65, 0, 0, 0}, - {76, 73, 65, 78, 0, 0}, {76, 73, 65, 78, 71, 0}, - {76, 73, 65, 79, 0, 0}, {76, 73, 69, 0, 0, 0}, - {76, 73, 78, 0, 0, 0}, {76, 73, 78, 71, 0, 0}, - {76, 73, 85, 0, 0, 0}, {76, 79, 0, 0, 0, 0}, - {76, 79, 78, 71, 0, 0}, {76, 79, 85, 0, 0, 0}, - {76, 85, 0, 0, 0, 0}, {76, 85, 65, 78, 0, 0}, - {76, 85, 69, 0, 0, 0}, {76, 85, 78, 0, 0, 0}, - {76, 85, 79, 0, 0, 0}, {77, 0, 0, 0, 0, 0}, - {77, 65, 0, 0, 0, 0}, {77, 65, 73, 0, 0, 0}, - {77, 65, 78, 0, 0, 0}, {77, 65, 78, 71, 0, 0}, - {77, 65, 79, 0, 0, 0}, {77, 69, 0, 0, 0, 0}, - {77, 69, 73, 0, 0, 0}, {77, 69, 78, 0, 0, 0}, - {77, 69, 78, 71, 0, 0}, {77, 73, 0, 0, 0, 0}, - {77, 73, 65, 78, 0, 0}, {77, 73, 65, 79, 0, 0}, - {77, 73, 69, 0, 0, 0}, {77, 73, 78, 0, 0, 0}, - {77, 73, 78, 71, 0, 0}, {77, 73, 85, 0, 0, 0}, - {77, 79, 0, 0, 0, 0}, {77, 79, 85, 0, 0, 0}, - {77, 85, 0, 0, 0, 0}, {78, 0, 0, 0, 0, 0}, - {78, 65, 0, 0, 0, 0}, {78, 65, 73, 0, 0, 0}, - {78, 65, 78, 0, 0, 0}, {78, 65, 78, 71, 0, 0}, - {78, 65, 79, 0, 0, 0}, {78, 69, 0, 0, 0, 0}, - {78, 69, 73, 0, 0, 0}, {78, 69, 78, 0, 0, 0}, - {78, 69, 78, 71, 0, 0}, {78, 73, 0, 0, 0, 0}, - {78, 73, 65, 78, 0, 0}, {78, 73, 65, 78, 71, 0}, - {78, 73, 65, 79, 0, 0}, {78, 73, 69, 0, 0, 0}, - {78, 73, 78, 0, 0, 0}, {78, 73, 78, 71, 0, 0}, - {78, 73, 85, 0, 0, 0}, {78, 79, 78, 71, 0, 0}, - {78, 79, 85, 0, 0, 0}, {78, 85, 0, 0, 0, 0}, - {78, 85, 65, 78, 0, 0}, {78, 85, 69, 0, 0, 0}, - {78, 85, 78, 0, 0, 0}, {78, 85, 79, 0, 0, 0}, - {79, 0, 0, 0, 0, 0}, {79, 85, 0, 0, 0, 0}, - {80, 65, 0, 0, 0, 0}, {80, 65, 73, 0, 0, 0}, - {80, 65, 78, 0, 0, 0}, {80, 65, 78, 71, 0, 0}, - {80, 65, 79, 0, 0, 0}, {80, 69, 73, 0, 0, 0}, - {80, 69, 78, 0, 0, 0}, {80, 69, 78, 71, 0, 0}, - {80, 73, 0, 0, 0, 0}, {80, 73, 65, 78, 0, 0}, - {80, 73, 65, 79, 0, 0}, {80, 73, 69, 0, 0, 0}, - {80, 73, 78, 0, 0, 0}, {80, 73, 78, 71, 0, 0}, - {80, 79, 0, 0, 0, 0}, {80, 79, 85, 0, 0, 0}, - {80, 85, 0, 0, 0, 0}, {81, 73, 0, 0, 0, 0}, - {81, 73, 65, 0, 0, 0}, {81, 73, 65, 78, 0, 0}, - {81, 73, 65, 78, 71, 0}, {81, 73, 65, 79, 0, 0}, - {81, 73, 69, 0, 0, 0}, {81, 73, 78, 0, 0, 0}, - {81, 73, 78, 71, 0, 0}, {81, 73, 79, 78, 71, 0}, - {81, 73, 85, 0, 0, 0}, {81, 85, 0, 0, 0, 0}, - {81, 85, 65, 78, 0, 0}, {81, 85, 69, 0, 0, 0}, - {81, 85, 78, 0, 0, 0}, {82, 65, 78, 0, 0, 0}, - {82, 65, 78, 71, 0, 0}, {82, 65, 79, 0, 0, 0}, - {82, 69, 0, 0, 0, 0}, {82, 69, 78, 0, 0, 0}, - {82, 69, 78, 71, 0, 0}, {82, 73, 0, 0, 0, 0}, - {82, 79, 78, 71, 0, 0}, {82, 79, 85, 0, 0, 0}, - {82, 85, 0, 0, 0, 0}, {82, 85, 65, 0, 0, 0}, - {82, 85, 65, 78, 0, 0}, {82, 85, 73, 0, 0, 0}, - {82, 85, 78, 0, 0, 0}, {82, 85, 79, 0, 0, 0}, - {83, 65, 0, 0, 0, 0}, {83, 65, 73, 0, 0, 0}, - {83, 65, 78, 0, 0, 0}, {83, 65, 78, 71, 0, 0}, - {83, 65, 79, 0, 0, 0}, {83, 69, 0, 0, 0, 0}, - {83, 69, 78, 0, 0, 0}, {83, 69, 78, 71, 0, 0}, - {83, 72, 65, 0, 0, 0}, {83, 72, 65, 73, 0, 0}, - {83, 72, 65, 78, 0, 0}, {83, 72, 65, 78, 71, 0}, - {83, 72, 65, 79, 0, 0}, {83, 72, 69, 0, 0, 0}, - {83, 72, 69, 78, 0, 0}, {88, 73, 78, 0, 0, 0}, - {83, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 71, 0}, - {83, 72, 73, 0, 0, 0}, {83, 72, 79, 85, 0, 0}, - {83, 72, 85, 0, 0, 0}, {83, 72, 85, 65, 0, 0}, - {83, 72, 85, 65, 73, 0}, {83, 72, 85, 65, 78, 0}, - {83, 72, 85, 65, 78, 71}, {83, 72, 85, 73, 0, 0}, - {83, 72, 85, 78, 0, 0}, {83, 72, 85, 79, 0, 0}, - {83, 73, 0, 0, 0, 0}, {83, 79, 78, 71, 0, 0}, - {83, 79, 85, 0, 0, 0}, {83, 85, 0, 0, 0, 0}, - {83, 85, 65, 78, 0, 0}, {83, 85, 73, 0, 0, 0}, - {83, 85, 78, 0, 0, 0}, {83, 85, 79, 0, 0, 0}, - {84, 65, 0, 0, 0, 0}, {84, 65, 73, 0, 0, 0}, - {84, 65, 78, 0, 0, 0}, {84, 65, 78, 71, 0, 0}, - {84, 65, 79, 0, 0, 0}, {84, 69, 0, 0, 0, 0}, - {84, 69, 78, 71, 0, 0}, {84, 73, 0, 0, 0, 0}, - {84, 73, 65, 78, 0, 0}, {84, 73, 65, 79, 0, 0}, - {84, 73, 69, 0, 0, 0}, {84, 73, 78, 71, 0, 0}, - {84, 79, 78, 71, 0, 0}, {84, 79, 85, 0, 0, 0}, - {84, 85, 0, 0, 0, 0}, {84, 85, 65, 78, 0, 0}, - {84, 85, 73, 0, 0, 0}, {84, 85, 78, 0, 0, 0}, - {84, 85, 79, 0, 0, 0}, {87, 65, 0, 0, 0, 0}, - {87, 65, 73, 0, 0, 0}, {87, 65, 78, 0, 0, 0}, - {87, 65, 78, 71, 0, 0}, {87, 69, 73, 0, 0, 0}, - {87, 69, 78, 0, 0, 0}, {87, 69, 78, 71, 0, 0}, - {87, 79, 0, 0, 0, 0}, {87, 85, 0, 0, 0, 0}, - {88, 73, 0, 0, 0, 0}, {88, 73, 65, 0, 0, 0}, - {88, 73, 65, 78, 0, 0}, {88, 73, 65, 78, 71, 0}, - {88, 73, 65, 79, 0, 0}, {88, 73, 69, 0, 0, 0}, - {88, 73, 78, 0, 0, 0}, {88, 73, 78, 71, 0, 0}, - {88, 73, 79, 78, 71, 0}, {88, 73, 85, 0, 0, 0}, - {88, 85, 0, 0, 0, 0}, {88, 85, 65, 78, 0, 0}, - {88, 85, 69, 0, 0, 0}, {88, 85, 78, 0, 0, 0}, - {89, 65, 0, 0, 0, 0}, {89, 65, 78, 0, 0, 0}, - {89, 65, 78, 71, 0, 0}, {89, 65, 79, 0, 0, 0}, - {89, 69, 0, 0, 0, 0}, {89, 73, 0, 0, 0, 0}, - {89, 73, 78, 0, 0, 0}, {89, 73, 78, 71, 0, 0}, - {89, 79, 0, 0, 0, 0}, {89, 79, 78, 71, 0, 0}, - {89, 79, 85, 0, 0, 0}, {89, 85, 0, 0, 0, 0}, - {89, 85, 65, 78, 0, 0}, {89, 85, 69, 0, 0, 0}, - {89, 85, 78, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, - {89, 85, 78, 0, 0, 0}, {90, 65, 0, 0, 0, 0}, - {90, 65, 73, 0, 0, 0}, {90, 65, 78, 0, 0, 0}, - {90, 65, 78, 71, 0, 0}, {90, 65, 79, 0, 0, 0}, - {90, 69, 0, 0, 0, 0}, {90, 69, 73, 0, 0, 0}, - {90, 69, 78, 0, 0, 0}, {90, 69, 78, 71, 0, 0}, - {90, 72, 65, 0, 0, 0}, {90, 72, 65, 73, 0, 0}, - {90, 72, 65, 78, 0, 0}, {90, 72, 65, 78, 71, 0}, - {67, 72, 65, 78, 71, 0}, {90, 72, 65, 78, 71, 0}, - {90, 72, 65, 79, 0, 0}, {90, 72, 69, 0, 0, 0}, - {90, 72, 69, 78, 0, 0}, {90, 72, 69, 78, 71, 0}, - {90, 72, 73, 0, 0, 0}, {83, 72, 73, 0, 0, 0}, - {90, 72, 73, 0, 0, 0}, {90, 72, 79, 78, 71, 0}, - {90, 72, 79, 85, 0, 0}, {90, 72, 85, 0, 0, 0}, - {90, 72, 85, 65, 0, 0}, {90, 72, 85, 65, 73, 0}, - {90, 72, 85, 65, 78, 0}, {90, 72, 85, 65, 78, 71}, - {90, 72, 85, 73, 0, 0}, {90, 72, 85, 78, 0, 0}, - {90, 72, 85, 79, 0, 0}, {90, 73, 0, 0, 0, 0}, - {90, 79, 78, 71, 0, 0}, {90, 79, 85, 0, 0, 0}, - {90, 85, 0, 0, 0, 0}, {90, 85, 65, 78, 0, 0}, - {90, 85, 73, 0, 0, 0}, {90, 85, 78, 0, 0, 0}, - {90, 85, 79, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, - {83, 72, 65, 78, 0, 0}, {0, 0, 0, 0, 0, 0}}; - - /** - * First and last Chinese character with known Pinyin according to zh collation - */ - private static final String FIRST_PINYIN_UNIHAN = "阿"; - private static final String LAST_PINYIN_UNIHAN = "鿿"; - - private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA); - - private static HanziToPinyin sInstance; - - public static class Token { - /** - * Separator between target string for each source char - */ - public static final String SEPARATOR = " "; - - public static final int LATIN = 1; - public static final int PINYIN = 2; - public static final int UNKNOWN = 3; - - public Token() { - } - - public Token(int type, String source, String target) { - this.type = type; - this.source = source; - this.target = target; - } - - /** - * Type of this token, ASCII, PINYIN or UNKNOWN. - */ - public int type; - /** - * Original string before translation. - */ - public String source; - /** - * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is - * original string in source. - */ - public String target; - } - - public static HanziToPinyin getInstance() { - synchronized (HanziToPinyin.class) { - if (sInstance != null) { - return sInstance; - } - // Check if zh_CN collation data is available - final Locale[] locale = Collator.getAvailableLocales(); - for (Locale value : locale) { - if (value.equals(Locale.CHINA) || value.getLanguage().contains("zh")) { - // Do self validation just once. - if (DEBUG) { - Log.d(TAG, "Self validation. Result: " + doSelfValidation()); - } - sInstance = new HanziToPinyin(true); - return sInstance; - } - } - if (sInstance == null) {//这个判断是用于处理国产ROM的兼容性问题 - if (Locale.CHINA.equals(Locale.getDefault())) { - sInstance = new HanziToPinyin(true); - return sInstance; - } - } - Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled"); - sInstance = new HanziToPinyin(false); - return sInstance; - } - } - - /** - * Validate if our internal table has some wrong value. - * - * @return true when the table looks correct. - */ - private static boolean doSelfValidation() { - char lastChar = UNIHANS[0]; - String lastString = Character.toString(lastChar); - for (char c : UNIHANS) { - if (lastChar == c) { - continue; - } - final String curString = Character.toString(c); - int cmp = COLLATOR.compare(lastString, curString); - if (cmp >= 0) { - Log.e(TAG, "Internal error in Unihan table. " + "The last string \"" + lastString - + "\" is greater than current string \"" + curString + "\"."); - return false; - } - lastString = curString; - } - return true; - } - - private Token getToken(char character) { - Token token = new Token(); - final String letter = Character.toString(character); - token.source = letter; - int offset = -1; - int cmp; - if (character < 256) { - token.type = Token.LATIN; - token.target = letter; - return token; - } else { - cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN); - if (cmp < 0) { - token.type = Token.UNKNOWN; - token.target = letter; - return token; - } else if (cmp == 0) { - token.type = Token.PINYIN; - offset = 0; - } else { - cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN); - if (cmp > 0) { - token.type = Token.UNKNOWN; - token.target = letter; - return token; - } else if (cmp == 0) { - token.type = Token.PINYIN; - offset = UNIHANS.length - 1; - } - } - } - - token.type = Token.PINYIN; - if (offset < 0) { - int begin = 0; - int end = UNIHANS.length - 1; - while (begin <= end) { - offset = (begin + end) / 2; - final String unihan = Character.toString(UNIHANS[offset]); - cmp = COLLATOR.compare(letter, unihan); - if (cmp == 0) { - break; - } else if (cmp > 0) { - begin = offset + 1; - } else { - end = offset - 1; - } - } - } - if (cmp < 0) { - offset--; - } - StringBuilder pinyin = new StringBuilder(); - for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) { - pinyin.append((char) PINYINS[offset][j]); - } - token.target = pinyin.toString(); - if (TextUtils.isEmpty(token.target)) { - token.type = Token.UNKNOWN; - token.target = token.source; - } - return token; - } - - /** - * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without - * space will be put into a Token, One Hanzi character which has pinyin will be treated as a - * Token. If these is no China collator, the empty token array is returned. - */ - public ArrayList get(final String input) { - ArrayList tokens = new ArrayList<>(); - if (!mHasChinaCollator || TextUtils.isEmpty(input)) { - // return empty tokens. - return tokens; - } - final int inputLength = input.length(); - final StringBuilder sb = new StringBuilder(); - int tokenType = Token.LATIN; - // Go through the input, create a new token when - // a. Token type changed - // b. Get the Pinyin of current charater. - // c. current character is space. - for (int i = 0; i < inputLength; i++) { - final char character = input.charAt(i); - if (character == ' ') { - if (sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - } else if (character < 256) { - if (tokenType != Token.LATIN && sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - tokenType = Token.LATIN; - sb.append(character); - } else { - Token t = getToken(character); - if (t.type == Token.PINYIN) { - if (sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - tokens.add(t); - tokenType = Token.PINYIN; - } else { - if (tokenType != t.type && sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - tokenType = t.type; - sb.append(character); - } - } - } - if (sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - return tokens; - } - - private void addToken( - final StringBuilder sb, final ArrayList tokens, final int tokenType) { - String str = sb.toString(); - tokens.add(new Token(tokenType, str, str)); - sb.setLength(0); - } - - public String toPinyinString(String string) { - if (string == null) { - return null; - } - StringBuilder sb = new StringBuilder(); - ArrayList tokens = get(string); - for (Token token : tokens) { - sb.append(token.target); - } - return sb.toString().toLowerCase(); - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.kt new file mode 100644 index 0000000..edfbdf5 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.kt @@ -0,0 +1,522 @@ +package com.sukisu.ultra.ui.util + +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.text.TextUtils +import android.util.Log +import java.text.Collator +import java.util.Locale + +class HanziToPinyin private constructor(val hasChinaCollator: Boolean) { + + class Token( + var type: Int = 0, + var source: String = "", + var target: String = "" + ) { + companion object { + const val LATIN = 1 + const val PINYIN = 2 + const val UNKNOWN = 3 + } + } + + private fun getToken(character: Char): Token { + val token = Token() + val letter = character.toString() + token.source = letter + var offset = -1 + var cmp: Int + + if (character < 256.toChar()) { + token.type = Token.LATIN + token.target = letter + return token + } else { + cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN) + if (cmp < 0) { + token.type = Token.UNKNOWN + token.target = letter + return token + } else if (cmp == 0) { + token.type = Token.PINYIN + offset = 0 + } else { + cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN) + if (cmp > 0) { + token.type = Token.UNKNOWN + token.target = letter + return token + } else if (cmp == 0) { + token.type = Token.PINYIN + offset = UNIHANS.size - 1 + } + } + } + + token.type = Token.PINYIN + if (offset < 0) { + var begin = 0 + var end = UNIHANS.size - 1 + while (begin <= end) { + offset = (begin + end) / 2 + val unihan = UNIHANS[offset].toString() + cmp = COLLATOR.compare(letter, unihan) + when { + cmp == 0 -> break + cmp > 0 -> begin = offset + 1 + else -> end = offset - 1 + } + } + } + if (cmp < 0) { + offset-- + } + + val pinyin = StringBuilder() + for (j in PINYINS[offset].indices) { + if (PINYINS[offset][j] == 0.toByte()) break + pinyin.append(PINYINS[offset][j].toInt().toChar()) + } + token.target = pinyin.toString() + if (TextUtils.isEmpty(token.target)) { + token.type = Token.UNKNOWN + token.target = token.source + } + return token + } + + fun get(input: String?): ArrayList { + val tokens = ArrayList() + if (!hasChinaCollator || TextUtils.isEmpty(input)) { + return tokens + } + + val inputLength = input!!.length + val sb = StringBuilder() + var tokenType = Token.LATIN + + for (i in 0 until inputLength) { + val character = input[i] + when { + character == ' ' -> { + if (sb.isNotEmpty()) { + addToken(sb, tokens, tokenType) + } + } + character < 256.toChar() -> { + if (tokenType != Token.LATIN && sb.isNotEmpty()) { + addToken(sb, tokens, tokenType) + } + tokenType = Token.LATIN + sb.append(character) + } + else -> { + val t = getToken(character) + if (t.type == Token.PINYIN) { + if (sb.isNotEmpty()) { + addToken(sb, tokens, tokenType) + } + tokens.add(t) + tokenType = Token.PINYIN + } else { + if (tokenType != t.type && sb.isNotEmpty()) { + addToken(sb, tokens, tokenType) + } + tokenType = t.type + sb.append(character) + } + } + } + } + if (sb.isNotEmpty()) { + addToken(sb, tokens, tokenType) + } + return tokens + } + + private fun addToken(sb: StringBuilder, tokens: ArrayList, tokenType: Int) { + val str = sb.toString() + tokens.add(Token(tokenType, str, str)) + sb.setLength(0) + } + + fun toPinyinString(string: String?): String? { + if (string == null) { + return null + } + val sb = StringBuilder() + val tokens = get(string) + for (token in tokens) { + sb.append(token.target) + } + return sb.toString().lowercase() + } + + companion object { + private const val TAG = "HanziToPinyin" + private const val DEBUG = false + + val UNIHANS = charArrayOf( + '阿', '哎', '安', '肮', '凹', '八', + '挀', '扳', '邦', '勹', '陂', '奔', + '伻', '屄', '边', '灬', '憋', '汃', + '冫', '癶', '峬', '嚓', '偲', '参', + '仓', '撡', '冊', '嵾', '曽', '曾', + '層', '叉', '芆', '辿', '伥', '抄', + '车', '抻', '沈', '沉', '阷', '吃', + '充', '抽', '出', '欻', '揣', '巛', + '刅', '吹', '旾', '逴', '呲', '匆', + '凑', '粗', '汆', '崔', '邨', '搓', + '咑', '呆', '丹', '当', '刀', '嘚', + '扥', '灯', '氐', '嗲', '甸', '刁', + '爹', '丁', '丟', '东', '吺', '厾', + '耑', '襨', '吨', '多', '妸', '诶', + '奀', '鞥', '儿', '发', '帆', '匚', + '飞', '分', '丰', '覅', '仏', '紑', + '伕', '旮', '侅', '甘', '冈', '皋', + '戈', '给', '根', '刯', '工', '勾', + '估', '瓜', '乖', '关', '光', '归', + '丨', '呙', '哈', '咍', '佄', '夯', + '茠', '诃', '黒', '拫', '亨', '噷', + '叿', '齁', '乯', '花', '怀', '犿', + '巟', '灰', '昏', '吙', '丌', '加', + '戋', '江', '艽', '阶', '巾', '坕', + '冂', '丩', '凥', '姢', '噘', '军', + '咔', '开', '刊', '忼', '尻', '匼', + '肎', '劥', '空', '抠', '扝', '夸', + '蒯', '宽', '匡', '亏', '坤', '扩', + '垃', '来', '兰', '啷', '捞', '肋', + '勒', '崚', '刕', '俩', '奁', '良', + '撩', '列', '拎', '刢', '溜', '囖', + '龙', '瞜', '噜', '娈', '畧', '抡', + '罗', '呣', '妈', '埋', '嫚', '牤', + '猫', '么', '呅', '门', '甿', '咪', + '宀', '喵', '乜', '民', '名', '谬', + '摸', '哞', '毪', '嗯', '拏', '腉', + '囡', '囔', '孬', '疒', '娞', '恁', + '能', '妮', '拈', '嬢', '鸟', '捏', + '囜', '宁', '妞', '农', '羺', '奴', + '奻', '疟', '黁', '郍', '喔', '讴', + '妑', '拍', '眅', '乓', '抛', '呸', + '喷', '匉', '丕', '囨', '剽', '氕', + '姘', '乒', '钋', '剖', '仆', '七', + '掐', '千', '呛', '悄', '癿', '亲', + '狅', '芎', '丘', '区', '峑', '缺', + '夋', '呥', '穣', '娆', '惹', '人', + '扔', '日', '茸', '厹', '邚', '挼', + '堧', '婑', '瞤', '捼', '仨', '毢', + '三', '桒', '掻', '閪', '森', '僧', + '杀', '筛', '山', '伤', '弰', '奢', + '申', '莘', '敒', '升', '尸', '収', + '书', '刷', '衰', '闩', '双', '谁', + '吮', '说', '厶', '忪', '捜', '苏', + '狻', '夊', '孙', '唆', '他', '囼', + '坍', '汤', '夲', '忑', '熥', '剔', + '天', '旫', '帖', '厅', '囲', '偷', + '凸', '湍', '推', '吞', '乇', '穵', + '歪', '弯', '尣', '危', '昷', '翁', + '挝', '乌', '夕', '虲', '仚', '乡', + '灱', '些', '心', '星', '凶', '休', + '吁', '吅', '削', '坃', '丫', '恹', + '央', '幺', '倻', '一', '囙', '应', + '哟', '佣', '优', '扜', '囦', '曰', + '晕', '筠', '筼', '帀', '災', '兂', + '匨', '傮', '则', '贼', '怎', '増', + '扎', '捚', '沾', '张', '长', '長', + '佋', '蜇', '贞', '争', '之', '峙', + '庢', '中', '州', '朱', '抓', '拽', + '专', '妆', '隹', '宒', '卓', '乲', + '宗', '邹', '租', '钻', '厜', '尊', + '昨', '兙', '鿃', '鿄' + ) + + val PINYINS = arrayOf( + byteArrayOf(65, 0, 0, 0, 0, 0), byteArrayOf(65, 73, 0, 0, 0, 0), + byteArrayOf(65, 78, 0, 0, 0, 0), byteArrayOf(65, 78, 71, 0, 0, 0), + byteArrayOf(65, 79, 0, 0, 0, 0), byteArrayOf(66, 65, 0, 0, 0, 0), + byteArrayOf(66, 65, 73, 0, 0, 0), byteArrayOf(66, 65, 78, 0, 0, 0), + byteArrayOf(66, 65, 78, 71, 0, 0), byteArrayOf(66, 65, 79, 0, 0, 0), + byteArrayOf(66, 69, 73, 0, 0, 0), byteArrayOf(66, 69, 78, 0, 0, 0), + byteArrayOf(66, 69, 78, 71, 0, 0), byteArrayOf(66, 73, 0, 0, 0, 0), + byteArrayOf(66, 73, 65, 78, 0, 0), byteArrayOf(66, 73, 65, 79, 0, 0), + byteArrayOf(66, 73, 69, 0, 0, 0), byteArrayOf(66, 73, 78, 0, 0, 0), + byteArrayOf(66, 73, 78, 71, 0, 0), byteArrayOf(66, 79, 0, 0, 0, 0), + byteArrayOf(66, 85, 0, 0, 0, 0), byteArrayOf(67, 65, 0, 0, 0, 0), + byteArrayOf(67, 65, 73, 0, 0, 0), byteArrayOf(67, 65, 78, 0, 0, 0), + byteArrayOf(67, 65, 78, 71, 0, 0), byteArrayOf(67, 65, 79, 0, 0, 0), + byteArrayOf(67, 69, 0, 0, 0, 0), byteArrayOf(67, 69, 78, 0, 0, 0), + byteArrayOf(67, 69, 78, 71, 0, 0), byteArrayOf(90, 69, 78, 71, 0, 0), + byteArrayOf(67, 69, 78, 71, 0, 0), byteArrayOf(67, 72, 65, 0, 0, 0), + byteArrayOf(67, 72, 65, 73, 0, 0), byteArrayOf(67, 72, 65, 78, 0, 0), + byteArrayOf(67, 72, 65, 78, 71, 0), byteArrayOf(67, 72, 65, 79, 0, 0), + byteArrayOf(67, 72, 69, 0, 0, 0), byteArrayOf(67, 72, 69, 78, 0, 0), + byteArrayOf(83, 72, 69, 78, 0, 0), byteArrayOf(67, 72, 69, 78, 0, 0), + byteArrayOf(67, 72, 69, 78, 71, 0), byteArrayOf(67, 72, 73, 0, 0, 0), + byteArrayOf(67, 72, 79, 78, 71, 0), byteArrayOf(67, 72, 79, 85, 0, 0), + byteArrayOf(67, 72, 85, 0, 0, 0), byteArrayOf(67, 72, 85, 65, 0, 0), + byteArrayOf(67, 72, 85, 65, 73, 0), byteArrayOf(67, 72, 85, 65, 78, 0), + byteArrayOf(67, 72, 85, 65, 78, 71), byteArrayOf(67, 72, 85, 73, 0, 0), + byteArrayOf(67, 72, 85, 78, 0, 0), byteArrayOf(67, 72, 85, 79, 0, 0), + byteArrayOf(67, 73, 0, 0, 0, 0), byteArrayOf(67, 79, 78, 71, 0, 0), + byteArrayOf(67, 79, 85, 0, 0, 0), byteArrayOf(67, 85, 0, 0, 0, 0), + byteArrayOf(67, 85, 65, 78, 0, 0), byteArrayOf(67, 85, 73, 0, 0, 0), + byteArrayOf(67, 85, 78, 0, 0, 0), byteArrayOf(67, 85, 79, 0, 0, 0), + byteArrayOf(68, 65, 0, 0, 0, 0), byteArrayOf(68, 65, 73, 0, 0, 0), + byteArrayOf(68, 65, 78, 0, 0, 0), byteArrayOf(68, 65, 78, 71, 0, 0), + byteArrayOf(68, 65, 79, 0, 0, 0), byteArrayOf(68, 69, 0, 0, 0, 0), + byteArrayOf(68, 69, 78, 0, 0, 0), byteArrayOf(68, 69, 78, 71, 0, 0), + byteArrayOf(68, 73, 0, 0, 0, 0), byteArrayOf(68, 73, 65, 0, 0, 0), + byteArrayOf(68, 73, 65, 78, 0, 0), byteArrayOf(68, 73, 65, 79, 0, 0), + byteArrayOf(68, 73, 69, 0, 0, 0), byteArrayOf(68, 73, 78, 71, 0, 0), + byteArrayOf(68, 73, 85, 0, 0, 0), byteArrayOf(68, 79, 78, 71, 0, 0), + byteArrayOf(68, 79, 85, 0, 0, 0), byteArrayOf(68, 85, 0, 0, 0, 0), + byteArrayOf(68, 85, 65, 78, 0, 0), byteArrayOf(68, 85, 73, 0, 0, 0), + byteArrayOf(68, 85, 78, 0, 0, 0), byteArrayOf(68, 85, 79, 0, 0, 0), + byteArrayOf(69, 0, 0, 0, 0, 0), byteArrayOf(69, 73, 0, 0, 0, 0), + byteArrayOf(69, 78, 0, 0, 0, 0), byteArrayOf(69, 78, 71, 0, 0, 0), + byteArrayOf(69, 82, 0, 0, 0, 0), byteArrayOf(70, 65, 0, 0, 0, 0), + byteArrayOf(70, 65, 78, 0, 0, 0), byteArrayOf(70, 65, 78, 71, 0, 0), + byteArrayOf(70, 69, 73, 0, 0, 0), byteArrayOf(70, 69, 78, 0, 0, 0), + byteArrayOf(70, 69, 78, 71, 0, 0), byteArrayOf(70, 73, 65, 79, 0, 0), + byteArrayOf(70, 79, 0, 0, 0, 0), byteArrayOf(70, 79, 85, 0, 0, 0), + byteArrayOf(70, 85, 0, 0, 0, 0), byteArrayOf(71, 65, 0, 0, 0, 0), + byteArrayOf(71, 65, 73, 0, 0, 0), byteArrayOf(71, 65, 78, 0, 0, 0), + byteArrayOf(71, 65, 78, 71, 0, 0), byteArrayOf(71, 65, 79, 0, 0, 0), + byteArrayOf(71, 69, 0, 0, 0, 0), byteArrayOf(71, 69, 73, 0, 0, 0), + byteArrayOf(71, 69, 78, 0, 0, 0), byteArrayOf(71, 69, 78, 71, 0, 0), + byteArrayOf(71, 79, 78, 71, 0, 0), byteArrayOf(71, 79, 85, 0, 0, 0), + byteArrayOf(71, 85, 0, 0, 0, 0), byteArrayOf(71, 85, 65, 0, 0, 0), + byteArrayOf(71, 85, 65, 73, 0, 0), byteArrayOf(71, 85, 65, 78, 0, 0), + byteArrayOf(71, 85, 65, 78, 71, 0), byteArrayOf(71, 85, 73, 0, 0, 0), + byteArrayOf(71, 85, 78, 0, 0, 0), byteArrayOf(71, 85, 79, 0, 0, 0), + byteArrayOf(72, 65, 0, 0, 0, 0), byteArrayOf(72, 65, 73, 0, 0, 0), + byteArrayOf(72, 65, 78, 0, 0, 0), byteArrayOf(72, 65, 78, 71, 0, 0), + byteArrayOf(72, 65, 79, 0, 0, 0), byteArrayOf(72, 69, 0, 0, 0, 0), + byteArrayOf(72, 69, 73, 0, 0, 0), byteArrayOf(72, 69, 78, 0, 0, 0), + byteArrayOf(72, 69, 78, 71, 0, 0), byteArrayOf(72, 77, 0, 0, 0, 0), + byteArrayOf(72, 79, 78, 71, 0, 0), byteArrayOf(72, 79, 85, 0, 0, 0), + byteArrayOf(72, 85, 0, 0, 0, 0), byteArrayOf(72, 85, 65, 0, 0, 0), + byteArrayOf(72, 85, 65, 73, 0, 0), byteArrayOf(72, 85, 65, 78, 0, 0), + byteArrayOf(72, 85, 65, 78, 71, 0), byteArrayOf(72, 85, 73, 0, 0, 0), + byteArrayOf(72, 85, 78, 0, 0, 0), byteArrayOf(72, 85, 79, 0, 0, 0), + byteArrayOf(74, 73, 0, 0, 0, 0), byteArrayOf(74, 73, 65, 0, 0, 0), + byteArrayOf(74, 73, 65, 78, 0, 0), byteArrayOf(74, 73, 65, 78, 71, 0), + byteArrayOf(74, 73, 65, 79, 0, 0), byteArrayOf(74, 73, 69, 0, 0, 0), + byteArrayOf(74, 73, 78, 0, 0, 0), byteArrayOf(74, 73, 78, 71, 0, 0), + byteArrayOf(74, 73, 79, 78, 71, 0), byteArrayOf(74, 73, 85, 0, 0, 0), + byteArrayOf(74, 85, 0, 0, 0, 0), byteArrayOf(74, 85, 65, 78, 0, 0), + byteArrayOf(74, 85, 69, 0, 0, 0), byteArrayOf(74, 85, 78, 0, 0, 0), + byteArrayOf(75, 65, 0, 0, 0, 0), byteArrayOf(75, 65, 73, 0, 0, 0), + byteArrayOf(75, 65, 78, 0, 0, 0), byteArrayOf(75, 65, 78, 71, 0, 0), + byteArrayOf(75, 65, 79, 0, 0, 0), byteArrayOf(75, 69, 0, 0, 0, 0), + byteArrayOf(75, 69, 78, 0, 0, 0), byteArrayOf(75, 69, 78, 71, 0, 0), + byteArrayOf(75, 79, 78, 71, 0, 0), byteArrayOf(75, 79, 85, 0, 0, 0), + byteArrayOf(75, 85, 0, 0, 0, 0), byteArrayOf(75, 85, 65, 0, 0, 0), + byteArrayOf(75, 85, 65, 73, 0, 0), byteArrayOf(75, 85, 65, 78, 0, 0), + byteArrayOf(75, 85, 65, 78, 71, 0), byteArrayOf(75, 85, 73, 0, 0, 0), + byteArrayOf(75, 85, 78, 0, 0, 0), byteArrayOf(75, 85, 79, 0, 0, 0), + byteArrayOf(76, 65, 0, 0, 0, 0), byteArrayOf(76, 65, 73, 0, 0, 0), + byteArrayOf(76, 65, 78, 0, 0, 0), byteArrayOf(76, 65, 78, 71, 0, 0), + byteArrayOf(76, 65, 79, 0, 0, 0), byteArrayOf(76, 69, 0, 0, 0, 0), + byteArrayOf(76, 69, 73, 0, 0, 0), byteArrayOf(76, 69, 78, 71, 0, 0), + byteArrayOf(76, 73, 0, 0, 0, 0), byteArrayOf(76, 73, 65, 0, 0, 0), + byteArrayOf(76, 73, 65, 78, 0, 0), byteArrayOf(76, 73, 65, 78, 71, 0), + byteArrayOf(76, 73, 65, 79, 0, 0), byteArrayOf(76, 73, 69, 0, 0, 0), + byteArrayOf(76, 73, 78, 0, 0, 0), byteArrayOf(76, 73, 78, 71, 0, 0), + byteArrayOf(76, 73, 85, 0, 0, 0), byteArrayOf(76, 79, 0, 0, 0, 0), + byteArrayOf(76, 79, 78, 71, 0, 0), byteArrayOf(76, 79, 85, 0, 0, 0), + byteArrayOf(76, 85, 0, 0, 0, 0), byteArrayOf(76, 85, 65, 78, 0, 0), + byteArrayOf(76, 85, 69, 0, 0, 0), byteArrayOf(76, 85, 78, 0, 0, 0), + byteArrayOf(76, 85, 79, 0, 0, 0), byteArrayOf(77, 0, 0, 0, 0, 0), + byteArrayOf(77, 65, 0, 0, 0, 0), byteArrayOf(77, 65, 73, 0, 0, 0), + byteArrayOf(77, 65, 78, 0, 0, 0), byteArrayOf(77, 65, 78, 71, 0, 0), + byteArrayOf(77, 65, 79, 0, 0, 0), byteArrayOf(77, 69, 0, 0, 0, 0), + byteArrayOf(77, 69, 73, 0, 0, 0), byteArrayOf(77, 69, 78, 0, 0, 0), + byteArrayOf(77, 69, 78, 71, 0, 0), byteArrayOf(77, 73, 0, 0, 0, 0), + byteArrayOf(77, 73, 65, 78, 0, 0), byteArrayOf(77, 73, 65, 79, 0, 0), + byteArrayOf(77, 73, 69, 0, 0, 0), byteArrayOf(77, 73, 78, 0, 0, 0), + byteArrayOf(77, 73, 78, 71, 0, 0), byteArrayOf(77, 73, 85, 0, 0, 0), + byteArrayOf(77, 79, 0, 0, 0, 0), byteArrayOf(77, 79, 85, 0, 0, 0), + byteArrayOf(77, 85, 0, 0, 0, 0), byteArrayOf(78, 0, 0, 0, 0, 0), + byteArrayOf(78, 65, 0, 0, 0, 0), byteArrayOf(78, 65, 73, 0, 0, 0), + byteArrayOf(78, 65, 78, 0, 0, 0), byteArrayOf(78, 65, 78, 71, 0, 0), + byteArrayOf(78, 65, 79, 0, 0, 0), byteArrayOf(78, 69, 0, 0, 0, 0), + byteArrayOf(78, 69, 73, 0, 0, 0), byteArrayOf(78, 69, 78, 0, 0, 0), + byteArrayOf(78, 69, 78, 71, 0, 0), byteArrayOf(78, 73, 0, 0, 0, 0), + byteArrayOf(78, 73, 65, 78, 0, 0), byteArrayOf(78, 73, 65, 78, 71, 0), + byteArrayOf(78, 73, 65, 79, 0, 0), byteArrayOf(78, 73, 69, 0, 0, 0), + byteArrayOf(78, 73, 78, 0, 0, 0), byteArrayOf(78, 73, 78, 71, 0, 0), + byteArrayOf(78, 73, 85, 0, 0, 0), byteArrayOf(78, 79, 78, 71, 0, 0), + byteArrayOf(78, 79, 85, 0, 0, 0), byteArrayOf(78, 85, 0, 0, 0, 0), + byteArrayOf(78, 85, 65, 78, 0, 0), byteArrayOf(78, 85, 69, 0, 0, 0), + byteArrayOf(78, 85, 78, 0, 0, 0), byteArrayOf(78, 85, 79, 0, 0, 0), + byteArrayOf(79, 0, 0, 0, 0, 0), byteArrayOf(79, 85, 0, 0, 0, 0), + byteArrayOf(80, 65, 0, 0, 0, 0), byteArrayOf(80, 65, 73, 0, 0, 0), + byteArrayOf(80, 65, 78, 0, 0, 0), byteArrayOf(80, 65, 78, 71, 0, 0), + byteArrayOf(80, 65, 79, 0, 0, 0), byteArrayOf(80, 69, 73, 0, 0, 0), + byteArrayOf(80, 69, 78, 0, 0, 0), byteArrayOf(80, 69, 78, 71, 0, 0), + byteArrayOf(80, 73, 0, 0, 0, 0), byteArrayOf(80, 73, 65, 78, 0, 0), + byteArrayOf(80, 73, 65, 79, 0, 0), byteArrayOf(80, 73, 69, 0, 0, 0), + byteArrayOf(80, 73, 78, 0, 0, 0), byteArrayOf(80, 73, 78, 71, 0, 0), + byteArrayOf(80, 79, 0, 0, 0, 0), byteArrayOf(80, 79, 85, 0, 0, 0), + byteArrayOf(80, 85, 0, 0, 0, 0), byteArrayOf(81, 73, 0, 0, 0, 0), + byteArrayOf(81, 73, 65, 0, 0, 0), byteArrayOf(81, 73, 65, 78, 0, 0), + byteArrayOf(81, 73, 65, 78, 71, 0), byteArrayOf(81, 73, 65, 79, 0, 0), + byteArrayOf(81, 73, 69, 0, 0, 0), byteArrayOf(81, 73, 78, 0, 0, 0), + byteArrayOf(81, 73, 78, 71, 0, 0), byteArrayOf(81, 73, 79, 78, 71, 0), + byteArrayOf(81, 73, 85, 0, 0, 0), byteArrayOf(81, 85, 0, 0, 0, 0), + byteArrayOf(81, 85, 65, 78, 0, 0), byteArrayOf(81, 85, 69, 0, 0, 0), + byteArrayOf(81, 85, 78, 0, 0, 0), byteArrayOf(82, 65, 78, 0, 0, 0), + byteArrayOf(82, 65, 78, 71, 0, 0), byteArrayOf(82, 65, 79, 0, 0, 0), + byteArrayOf(82, 69, 0, 0, 0, 0), byteArrayOf(82, 69, 78, 0, 0, 0), + byteArrayOf(82, 69, 78, 71, 0, 0), byteArrayOf(82, 73, 0, 0, 0, 0), + byteArrayOf(82, 79, 78, 71, 0, 0), byteArrayOf(82, 79, 85, 0, 0, 0), + byteArrayOf(82, 85, 0, 0, 0, 0), byteArrayOf(82, 85, 65, 0, 0, 0), + byteArrayOf(82, 85, 65, 78, 0, 0), byteArrayOf(82, 85, 73, 0, 0, 0), + byteArrayOf(82, 85, 78, 0, 0, 0), byteArrayOf(82, 85, 79, 0, 0, 0), + byteArrayOf(83, 65, 0, 0, 0, 0), byteArrayOf(83, 65, 73, 0, 0, 0), + byteArrayOf(83, 65, 78, 0, 0, 0), byteArrayOf(83, 65, 78, 71, 0, 0), + byteArrayOf(83, 65, 79, 0, 0, 0), byteArrayOf(83, 69, 0, 0, 0, 0), + byteArrayOf(83, 69, 78, 0, 0, 0), byteArrayOf(83, 69, 78, 71, 0, 0), + byteArrayOf(83, 72, 65, 0, 0, 0), byteArrayOf(83, 72, 65, 73, 0, 0), + byteArrayOf(83, 72, 65, 78, 0, 0), byteArrayOf(83, 72, 65, 78, 71, 0), + byteArrayOf(83, 72, 65, 79, 0, 0), byteArrayOf(83, 72, 69, 0, 0, 0), + byteArrayOf(83, 72, 69, 78, 0, 0), byteArrayOf(88, 73, 78, 0, 0, 0), + byteArrayOf(83, 72, 69, 78, 0, 0), byteArrayOf(83, 72, 69, 78, 71, 0), + byteArrayOf(83, 72, 73, 0, 0, 0), byteArrayOf(83, 72, 79, 85, 0, 0), + byteArrayOf(83, 72, 85, 0, 0, 0), byteArrayOf(83, 72, 85, 65, 0, 0), + byteArrayOf(83, 72, 85, 65, 73, 0), byteArrayOf(83, 72, 85, 65, 78, 0), + byteArrayOf(83, 72, 85, 65, 78, 71), byteArrayOf(83, 72, 85, 73, 0, 0), + byteArrayOf(83, 72, 85, 78, 0, 0), byteArrayOf(83, 72, 85, 79, 0, 0), + byteArrayOf(83, 73, 0, 0, 0, 0), byteArrayOf(83, 79, 78, 71, 0, 0), + byteArrayOf(83, 79, 85, 0, 0, 0), byteArrayOf(83, 85, 0, 0, 0, 0), + byteArrayOf(83, 85, 65, 78, 0, 0), byteArrayOf(83, 85, 73, 0, 0, 0), + byteArrayOf(83, 85, 78, 0, 0, 0), byteArrayOf(83, 85, 79, 0, 0, 0), + byteArrayOf(84, 65, 0, 0, 0, 0), byteArrayOf(84, 65, 73, 0, 0, 0), + byteArrayOf(84, 65, 78, 0, 0, 0), byteArrayOf(84, 65, 78, 71, 0, 0), + byteArrayOf(84, 65, 79, 0, 0, 0), byteArrayOf(84, 69, 0, 0, 0, 0), + byteArrayOf(84, 69, 78, 71, 0, 0), byteArrayOf(84, 73, 0, 0, 0, 0), + byteArrayOf(84, 73, 65, 78, 0, 0), byteArrayOf(84, 73, 65, 79, 0, 0), + byteArrayOf(84, 73, 69, 0, 0, 0), byteArrayOf(84, 73, 78, 71, 0, 0), + byteArrayOf(84, 79, 78, 71, 0, 0), byteArrayOf(84, 79, 85, 0, 0, 0), + byteArrayOf(84, 85, 0, 0, 0, 0), byteArrayOf(84, 85, 65, 78, 0, 0), + byteArrayOf(84, 85, 73, 0, 0, 0), byteArrayOf(84, 85, 78, 0, 0, 0), + byteArrayOf(84, 85, 79, 0, 0, 0), byteArrayOf(87, 65, 0, 0, 0, 0), + byteArrayOf(87, 65, 73, 0, 0, 0), byteArrayOf(87, 65, 78, 0, 0, 0), + byteArrayOf(87, 65, 78, 71, 0, 0), byteArrayOf(87, 69, 73, 0, 0, 0), + byteArrayOf(87, 69, 78, 0, 0, 0), byteArrayOf(87, 69, 78, 71, 0, 0), + byteArrayOf(87, 79, 0, 0, 0, 0), byteArrayOf(87, 85, 0, 0, 0, 0), + byteArrayOf(88, 73, 0, 0, 0, 0), byteArrayOf(88, 73, 65, 0, 0, 0), + byteArrayOf(88, 73, 65, 78, 0, 0), byteArrayOf(88, 73, 65, 78, 71, 0), + byteArrayOf(88, 73, 65, 79, 0, 0), byteArrayOf(88, 73, 69, 0, 0, 0), + byteArrayOf(88, 73, 78, 0, 0, 0), byteArrayOf(88, 73, 78, 71, 0, 0), + byteArrayOf(88, 73, 79, 78, 71, 0), byteArrayOf(88, 73, 85, 0, 0, 0), + byteArrayOf(88, 85, 0, 0, 0, 0), byteArrayOf(88, 85, 65, 78, 0, 0), + byteArrayOf(88, 85, 69, 0, 0, 0), byteArrayOf(88, 85, 78, 0, 0, 0), + byteArrayOf(89, 65, 0, 0, 0, 0), byteArrayOf(89, 65, 78, 0, 0, 0), + byteArrayOf(89, 65, 78, 71, 0, 0), byteArrayOf(89, 65, 79, 0, 0, 0), + byteArrayOf(89, 69, 0, 0, 0, 0), byteArrayOf(89, 73, 0, 0, 0, 0), + byteArrayOf(89, 73, 78, 0, 0, 0), byteArrayOf(89, 73, 78, 71, 0, 0), + byteArrayOf(89, 79, 0, 0, 0, 0), byteArrayOf(89, 79, 78, 71, 0, 0), + byteArrayOf(89, 79, 85, 0, 0, 0), byteArrayOf(89, 85, 0, 0, 0, 0), + byteArrayOf(89, 85, 65, 78, 0, 0), byteArrayOf(89, 85, 69, 0, 0, 0), + byteArrayOf(89, 85, 78, 0, 0, 0), byteArrayOf(74, 85, 78, 0, 0, 0), + byteArrayOf(89, 85, 78, 0, 0, 0), byteArrayOf(90, 65, 0, 0, 0, 0), + byteArrayOf(90, 65, 73, 0, 0, 0), byteArrayOf(90, 65, 78, 0, 0, 0), + byteArrayOf(90, 65, 78, 71, 0, 0), byteArrayOf(90, 65, 79, 0, 0, 0), + byteArrayOf(90, 69, 0, 0, 0, 0), byteArrayOf(90, 69, 73, 0, 0, 0), + byteArrayOf(90, 69, 78, 0, 0, 0), byteArrayOf(90, 69, 78, 71, 0, 0), + byteArrayOf(90, 72, 65, 0, 0, 0), byteArrayOf(90, 72, 65, 73, 0, 0), + byteArrayOf(90, 72, 65, 78, 0, 0), byteArrayOf(90, 72, 65, 78, 71, 0), + byteArrayOf(67, 72, 65, 78, 71, 0), byteArrayOf(90, 72, 65, 78, 71, 0), + byteArrayOf(90, 72, 65, 79, 0, 0), byteArrayOf(90, 72, 69, 0, 0, 0), + byteArrayOf(90, 72, 69, 78, 0, 0), byteArrayOf(90, 72, 69, 78, 71, 0), + byteArrayOf(90, 72, 73, 0, 0, 0), byteArrayOf(83, 72, 73, 0, 0, 0), + byteArrayOf(90, 72, 73, 0, 0, 0), byteArrayOf(90, 72, 79, 78, 71, 0), + byteArrayOf(90, 72, 79, 85, 0, 0), byteArrayOf(90, 72, 85, 0, 0, 0), + byteArrayOf(90, 72, 85, 65, 0, 0), byteArrayOf(90, 72, 85, 65, 73, 0), + byteArrayOf(90, 72, 85, 65, 78, 0), byteArrayOf(90, 72, 85, 65, 78, 71), + byteArrayOf(90, 72, 85, 73, 0, 0), byteArrayOf(90, 72, 85, 78, 0, 0), + byteArrayOf(90, 72, 85, 79, 0, 0), byteArrayOf(90, 73, 0, 0, 0, 0), + byteArrayOf(90, 79, 78, 71, 0, 0), byteArrayOf(90, 79, 85, 0, 0, 0), + byteArrayOf(90, 85, 0, 0, 0, 0), byteArrayOf(90, 85, 65, 78, 0, 0), + byteArrayOf(90, 85, 73, 0, 0, 0), byteArrayOf(90, 85, 78, 0, 0, 0), + byteArrayOf(90, 85, 79, 0, 0, 0), byteArrayOf(0, 0, 0, 0, 0, 0), + byteArrayOf(83, 72, 65, 78, 0, 0), byteArrayOf(0, 0, 0, 0, 0, 0) + ) + + private const val FIRST_PINYIN_UNIHAN = "阿" + private const val LAST_PINYIN_UNIHAN = "鿿" + + private val COLLATOR: Collator = Collator.getInstance(Locale.CHINA) + + private var sInstance: HanziToPinyin? = null + + fun getInstance(): HanziToPinyin { + synchronized(HanziToPinyin::class.java) { + if (sInstance != null) { + return sInstance!! + } + + val locale = Collator.getAvailableLocales() + for (value in locale) { + if (value == Locale.CHINA || value.language.contains("zh")) { + if (DEBUG) { + Log.d(TAG, "Self validation. Result: ${doSelfValidation()}") + } + sInstance = HanziToPinyin(true) + return sInstance!! + } + } + + if (sInstance == null) { + if (Locale.CHINA == Locale.getDefault()) { + sInstance = HanziToPinyin(true) + return sInstance!! + } + } + + Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled") + sInstance = HanziToPinyin(false) + return sInstance!! + } + } + + private fun doSelfValidation(): Boolean { + val lastChar = UNIHANS[0] + var lastString = lastChar.toString() + for (c in UNIHANS) { + if (lastChar == c) { + continue + } + val curString = c.toString() + val cmp = COLLATOR.compare(lastString, curString) + if (cmp >= 0) { + Log.e( + TAG, + "Internal error in Unihan table. The last string \"$lastString\" " + + "is greater than current string \"$curString\"." + ) + return false + } + lastString = curString + } + return true + } + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt index 1c1b070..d7398d6 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt @@ -30,11 +30,11 @@ import java.io.File private const val TAG = "KsuCli" private fun getKsuDaemonPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozako.so" + return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so" } object KsuCli { - val SHELL: Shell = createRootShell() + var SHELL: Shell = createRootShell() val GLOBAL_MNT_SHELL: Shell = createRootShell(true) } @@ -97,9 +97,16 @@ fun execKsud(args: String, newShell: Boolean = false): Boolean { } } +suspend fun getFeatureStatus(feature: String): String = withContext(Dispatchers.IO) { + val shell = getRootShell() + val out = shell.newJob() + .add("${getKsuDaemonPath()} feature check $feature").to(ArrayList(), null).exec().out + out.firstOrNull()?.trim().orEmpty() +} + fun install() { val start = SystemClock.elapsedRealtime() - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so").absolutePath + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so").absolutePath val result = execKsud("install --magiskboot $magiskboot", true) Log.w(TAG, "install result: $result, cost: ${SystemClock.elapsedRealtime() - start}ms") } @@ -107,8 +114,8 @@ fun install() { fun listModules(): String { val shell = getRootShell() - val out = - shell.newJob().add("${getKsuDaemonPath()} module list").to(ArrayList(), null).exec().out + val out = shell.newJob() + .add("${getKsuDaemonPath()} module list").to(ArrayList(), null).exec().out return out.joinToString("\n").ifBlank { "[]" } } @@ -149,6 +156,13 @@ fun restoreModule(id: String): Boolean { return result } +fun undoUninstallModule(id: String): Boolean { + val cmd = "module undo-uninstall $id" + val result = execKsud(cmd, true) + Log.i(TAG, "undo uninstall module $id result: $result") + return result +} + private fun flashWithIO( cmd: String, onStdout: (String) -> Unit, @@ -222,7 +236,7 @@ fun runModuleAction( fun restoreBoot( onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): Boolean { - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") val result = flashWithIO( "${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot", onStdout, @@ -235,7 +249,7 @@ fun restoreBoot( fun uninstallPermanently( onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): Boolean { - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") val result = flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr) onFinish(result.isSuccess, result.code) @@ -253,6 +267,7 @@ fun installBoot( bootUri: Uri?, lkm: LkmSelection, ota: Boolean, + partition: String?, onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit, @@ -270,7 +285,7 @@ fun installBoot( } } - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") var cmd = "boot-patch --magiskboot ${magiskboot.absolutePath}" cmd += if (bootFile == null) { @@ -312,6 +327,10 @@ fun installBoot( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) cmd += " -o $downloadsDir" + partition?.let { part -> + cmd += " --partition $part" + } + val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr) Log.i("KernelSU", "install boot result: ${result.isSuccess}") @@ -320,6 +339,11 @@ fun installBoot( // if boot uri is empty, it is direct install, when success, we should show reboot button onFinish(bootUri == null && result.isSuccess, result.code) + + if (bootUri == null && result.isSuccess) { + install() + } + return result.isSuccess } @@ -337,14 +361,6 @@ fun rootAvailable(): Boolean { return shell.isRoot } -fun isAbDevice(): Boolean { - val shell = getRootShell() - return ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim().toBoolean() -} - -fun isInitBoot(): Boolean { - return !Os.uname().release.contains("android12-") -} suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) { val shell = getRootShell() @@ -354,7 +370,40 @@ suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) { suspend fun getSupportedKmis(): List = withContext(Dispatchers.IO) { val shell = getRootShell() - val cmd = "boot-info supported-kmi" + val cmd = "boot-info supported-kmis" + val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out + out.filter { it.isNotBlank() }.map { it.trim() } +} + +suspend fun isAbDevice(): Boolean = withContext(Dispatchers.IO) { + val shell = getRootShell() + val cmd = "boot-info is-ab-device" + ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim().toBoolean() +} + +suspend fun getDefaultPartition(): String = withContext(Dispatchers.IO) { + val shell = getRootShell() + if (shell.isRoot) { + val cmd = "boot-info default-partition" + ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim() + } else { + if (!Os.uname().release.contains("android12-")) "init_boot" else "boot" + } +} + +suspend fun getSlotSuffix(ota: Boolean): String = withContext(Dispatchers.IO) { + val shell = getRootShell() + val cmd = if (ota) { + "boot-info slot-suffix --ota" + } else { + "boot-info slot-suffix" + } + ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim() +} + +suspend fun getAvailablePartitions(): List = withContext(Dispatchers.IO) { + val shell = getRootShell() + val cmd = "boot-info available-partitions" val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out out.filter { it.isNotBlank() }.map { it.trim() } } @@ -419,6 +468,69 @@ fun deleteAppProfileTemplate(id: String): Boolean { return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'") .to(ArrayList(), null).exec().isSuccess } +// KPM控制 +fun loadKpmModule(path: String, args: String? = null): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm load $path ${args ?: ""}" + return ShellUtils.fastCmd(shell, cmd) +} + +fun unloadKpmModule(name: String): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm unload $name" + return ShellUtils.fastCmd(shell, cmd) +} + +fun getKpmModuleCount(): Int { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm num" + val result = ShellUtils.fastCmd(shell, cmd) + return result.trim().toIntOrNull() ?: 0 +} + +fun runCmd(shell: Shell, cmd: String): String { + return shell.newJob() + .add(cmd) + .to(mutableListOf(), null) + .exec().out + .joinToString("\n") +} + +fun listKpmModules(): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm list" + return try { + runCmd(shell, cmd).trim() + } catch (e: Exception) { + Log.e(TAG, "Failed to list KPM modules", e) + "" + } +} + +fun getKpmModuleInfo(name: String): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm info $name" + return try { + runCmd(shell, cmd).trim() + } catch (e: Exception) { + Log.e(TAG, "Failed to get KPM module info: $name", e) + "" + } +} + +fun controlKpmModule(name: String, args: String? = null): Int { + val shell = getRootShell() + val cmd = """${getKsuDaemonPath()} kpm control $name "${args ?: ""}"""" + val result = runCmd(shell, cmd) + return result.trim().toIntOrNull() ?: -1 +} + +fun getKpmVersion(): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm version" + val result = ShellUtils.fastCmd(shell, cmd) + return result.trim() +} fun forceStopApp(packageName: String) { val shell = getRootShell() @@ -442,143 +554,59 @@ fun restartApp(packageName: String) { } fun getSuSFSDaemonPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozakozako.so" -} - -fun getSuSFS(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} support") - return result + return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksu_susfs.so" } fun getSuSFSVersion(): String { val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} version") + val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} show version") return result } fun getSuSFSVariant(): String { val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} variant") + val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} show variant") return result } fun getSuSFSFeatures(): String { val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} features") - return result -} - -fun susfsSUS_SU_0(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su 0") - return result -} - -fun susfsSUS_SU_2(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su 2") - return result -} - -fun susfsSUS_SU_Mode(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su mode") - return result -} - -fun getKpmmgrPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so" -} - - -fun loadKpmModule(path: String, args: String? = null): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}" - return ShellUtils.fastCmd(shell, cmd) -} - -fun unloadKpmModule(name: String): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} unload $name" - return ShellUtils.fastCmd(shell, cmd) -} - -fun getKpmModuleCount(): Int { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} num" - val result = ShellUtils.fastCmd(shell, cmd) - return result.trim().toIntOrNull() ?: 0 -} - -fun runCmd(shell: Shell, cmd: String): String { - return shell.newJob() - .add(cmd) - .to(mutableListOf(), null) - .exec().out - .joinToString("\n") -} - -fun listKpmModules(): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} list" - return try { - runCmd(shell, cmd).trim() - } catch (e: Exception) { - Log.e(TAG, "Failed to list KPM modules", e) - "" - } -} - -fun getKpmModuleInfo(name: String): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} info $name" - return try { - runCmd(shell, cmd).trim() - } catch (e: Exception) { - Log.e(TAG, "Failed to get KPM module info: $name", e) - "" - } -} - -fun controlKpmModule(name: String, args: String? = null): Int { - val shell = getRootShell() - val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}"""" - val result = runCmd(shell, cmd) - return result.trim().toIntOrNull() ?: -1 -} - -fun getKpmVersion(): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} version" - val result = ShellUtils.fastCmd(shell, cmd) - return result.trim() + val cmd = "${getSuSFSDaemonPath()} show enabled_features" + return runCmd(shell, cmd) } fun getZygiskImplement(): String { val shell = getRootShell() - val zygiskPath = "/data/adb/modules/zygisksu" - val rezygiskPath = "/data/adb/modules/rezygisk" - val result = if (ShellUtils.fastCmdResult(shell, "test -f $zygiskPath/module.prop && test ! -f $zygiskPath/disable")) { - ShellUtils.fastCmd(shell, "grep '^name=' $zygiskPath/module.prop | cut -d'=' -f2") - } else if (ShellUtils.fastCmdResult(shell, "test -f $rezygiskPath/module.prop && test ! -f $rezygiskPath/disable")) { - ShellUtils.fastCmd(shell, "grep '^name=' $rezygiskPath/module.prop | cut -d'=' -f2") - } else { - "None" + + val zygiskModuleIds = listOf( + "zygisksu", + "rezygisk", + "shirokozygisk" + ) + + for (moduleId in zygiskModuleIds) { + val modulePath = "/data/adb/modules/$moduleId" + when { + ShellUtils.fastCmdResult(shell, "test -f $modulePath/module.prop && test ! -f $modulePath/disable") -> { + val result = ShellUtils.fastCmd(shell, "grep '^name=' $modulePath/module.prop | cut -d'=' -f2") + Log.i(TAG, "Zygisk implement: $result") + return result + } + } } - Log.i(TAG, "Zygisk implement: $result") - return result + + Log.i(TAG, "Zygisk implement: None") + return "None" } fun getUidScannerDaemonPath(): String { return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libuid_scanner.so" } +private const val targetPath = "/data/adb/uid_scanner" fun ensureUidScannerExecutable(): Boolean { val shell = getRootShell() val uidScannerPath = getUidScannerDaemonPath() - val targetPath = "/data/adb/uid_scanner" - if (!ShellUtils.fastCmdResult(shell, "test -f $targetPath")) { val copyResult = ShellUtils.fastCmdResult(shell, "cp $uidScannerPath $targetPath") if (!copyResult) { @@ -589,7 +617,6 @@ fun ensureUidScannerExecutable(): Boolean { val result = ShellUtils.fastCmdResult(shell, "chmod 755 $targetPath") return result } -private const val targetPath = "/data/adb/uid_scanner" fun setUidAutoScan(enabled: Boolean): Boolean { val shell = getRootShell() @@ -600,7 +627,10 @@ fun setUidAutoScan(enabled: Boolean): Boolean { val enableValue = if (enabled) 1 else 0 val cmd = "$targetPath --auto-scan $enableValue && $targetPath reload" val result = ShellUtils.fastCmdResult(shell, cmd) - return result + + val throneResult = Natives.setUidScannerEnabled(enabled) + + return result && throneResult } fun setUidMultiUserScan(enabled: Boolean): Boolean { @@ -614,3 +644,95 @@ fun setUidMultiUserScan(enabled: Boolean): Boolean { val result = ShellUtils.fastCmdResult(shell, cmd) return result } + +fun getUidMultiUserScan(): Boolean { + val shell = getRootShell() + + val cmd = "grep 'multi_user_scan=' /data/misc/user_uid/uid_scanner.conf | cut -d'=' -f2" + val result = ShellUtils.fastCmd(shell, cmd).trim() + + return try { + result.toInt() == 1 + } catch (_: NumberFormatException) { + false + } +} + +fun cleanRuntimeEnvironment(): Boolean { + val shell = getRootShell() + return try { + try { + ShellUtils.fastCmd(shell, "/data/adb/uid_scanner stop") + } catch (_: Exception) { + } + ShellUtils.fastCmdResult(shell, "rm -rf /data/misc/user_uid") + ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/uid_scanner") + ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/ksu/bin/user_uid") + ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/service.d/uid_scanner.sh") + Natives.clearUidScannerEnvironment() + true + } catch (_: Exception) { + false + } +} + +fun readUidScannerFile(): Boolean { + val shell = getRootShell() + return try { + ShellUtils.fastCmd(shell, "cat /data/adb/ksu/.uid_scanner").trim() == "1" + } catch (_: Exception) { + false + } +} + +fun addUmountPath(path: String, flags: Int): Boolean { + val shell = getRootShell() + val flagsArg = if (flags >= 0) "--flags $flags" else "" + val cmd = "${getKsuDaemonPath()} umount add $path $flagsArg" + val result = ShellUtils.fastCmdResult(shell, cmd) + Log.i(TAG, "add umount path $path result: $result") + return result +} + +fun removeUmountPath(path: String): Boolean { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} umount remove $path" + val result = ShellUtils.fastCmdResult(shell, cmd) + Log.i(TAG, "remove umount path $path result: $result") + return result +} + +fun listUmountPaths(): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} umount list" + return try { + runCmd(shell, cmd).trim() + } catch (e: Exception) { + Log.e(TAG, "Failed to list umount paths", e) + "" + } +} + +fun clearCustomUmountPaths(): Boolean { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} umount clear-custom" + val result = ShellUtils.fastCmdResult(shell, cmd) + Log.i(TAG, "clear custom umount paths result: $result") + return result +} + +fun saveUmountConfig(): Boolean { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} umount save" + val result = ShellUtils.fastCmdResult(shell, cmd) + Log.i(TAG, "save umount config result: $result") + return result +} + +fun applyUmountConfigToKernel(): Boolean { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} umount apply" + val result = ShellUtils.fastCmdResult(shell, cmd) + Log.i(TAG, "apply umount config to kernel result: $result") + return result +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/RestartActivityUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/RestartActivityUtils.kt deleted file mode 100644 index ff8d881..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/RestartActivityUtils.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.sukisu.ultra.ui.util - -import android.app.Activity -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import com.sukisu.ultra.ui.MainActivity - -/** - * 重启应用程序 - **/ - -fun Context.restartApp( - activityClass: Class, - finishCurrent: Boolean = true, - clearTask: Boolean = true, - newTask: Boolean = true -) { - val intent = Intent(this, activityClass) - if (clearTask) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) - if (newTask) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - - if (finishCurrent && this is Activity) { - finish() - } -} - -/** - * 刷新启动器图标 - */ -fun toggleLauncherIcon(context: Context, useAlt: Boolean) { - val pm = context.packageManager - val main = ComponentName(context, MainActivity::class.java.name) - val alt = ComponentName(context, "${MainActivity::class.java.name}Alias") - if (useAlt) { - pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) - pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) - } else { - pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) - pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleModify.kt similarity index 92% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleModify.kt index 190ce9c..c0f52b1 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleModify.kt @@ -1,16 +1,20 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.util.module +import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext import com.sukisu.ultra.R +import com.sukisu.ultra.ui.util.reboot import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -159,7 +163,8 @@ object ModuleModify { val moduleDir = "/data/adb/modules" // 直接从用户选择的文件读取并解压 - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "$busyboxPath tar -xz -C $moduleDir")) + val process = Runtime.getRuntime() + .exec(arrayOf("su", "-c", "$busyboxPath tar -xz -C $moduleDir")) context.contentResolver.openInputStream(uri)?.use { input -> input.copyTo(process.outputStream) @@ -277,7 +282,11 @@ object ModuleModify { } } catch (e: Exception) { - Log.e("AllowlistRestore", context.getString(R.string.allowlist_restore_failed, ""), e) + Log.e( + "AllowlistRestore", + context.getString(R.string.allowlist_restore_failed, ""), + e + ) withContext(Dispatchers.Main) { snackBarHost.showSnackbar( context.getString(R.string.allowlist_restore_failed, e.message), @@ -292,11 +301,11 @@ object ModuleModify { fun rememberModuleBackupLauncher( context: Context, snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() + scope: CoroutineScope = rememberCoroutineScope() ) = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { + if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> scope.launch { backupModules(context, snackBarHost, uri) @@ -309,8 +318,8 @@ object ModuleModify { fun rememberModuleRestoreLauncher( context: Context, snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() - ): androidx.activity.result.ActivityResultLauncher { + scope: CoroutineScope = rememberCoroutineScope() + ): ActivityResultLauncher { var showRestoreDialog by remember { mutableStateOf(false) } var restoreConfirmResult by remember { mutableStateOf?>(null) } @@ -330,7 +339,7 @@ object ModuleModify { return rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { + if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> scope.launch { val confirmResult = CompletableDeferred() @@ -353,11 +362,11 @@ object ModuleModify { fun rememberAllowlistBackupLauncher( context: Context, snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() + scope: CoroutineScope = rememberCoroutineScope() ) = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { + if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> scope.launch { backupAllowlist(context, snackBarHost, uri) @@ -370,10 +379,14 @@ object ModuleModify { fun rememberAllowlistRestoreLauncher( context: Context, snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() - ): androidx.activity.result.ActivityResultLauncher { + scope: CoroutineScope = rememberCoroutineScope() + ): ActivityResultLauncher { var showAllowlistRestoreDialog by remember { mutableStateOf(false) } - var allowlistRestoreConfirmResult by remember { mutableStateOf?>(null) } + var allowlistRestoreConfirmResult by remember { + mutableStateOf?>( + null + ) + } // 显示允许列表恢复确认对话框 AllowlistRestoreConfirmationDialog( @@ -391,7 +404,7 @@ object ModuleModify { return rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { + if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> scope.launch { val confirmResult = CompletableDeferred() diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleUtils.kt similarity index 99% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleUtils.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleUtils.kt index 230b99f..5113b4a 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleUtils.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleUtils.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.util.module import android.content.Context import android.content.Intent diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleVerificationManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleVerificationManager.kt similarity index 98% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleVerificationManager.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleVerificationManager.kt index 9e70c40..4194829 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleVerificationManager.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleVerificationManager.kt @@ -1,9 +1,10 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.util.module import android.content.Context import android.net.Uri import android.util.Log import com.sukisu.ultra.Natives +import com.sukisu.ultra.ui.util.getRootShell import java.io.File import java.io.FileOutputStream diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt index 838c229..72e069f 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt @@ -4,16 +4,11 @@ import android.annotation.SuppressLint import android.content.Context import android.os.Build import android.system.Os -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.content.edit import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.dergoogler.mmrl.platform.Platform.Companion.context -import com.google.gson.Gson -import com.google.gson.JsonSyntaxException import com.sukisu.ultra.KernelVersion import com.sukisu.ultra.Natives import com.sukisu.ultra.getKernelVersion @@ -21,20 +16,14 @@ import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ui.util.* import com.sukisu.ultra.ui.util.module.LatestVersionInfo import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class HomeViewModel : ViewModel() { - companion object { - private const val TAG = "HomeViewModel" - private const val PREFS_NAME = "home_cache" - private const val KEY_SYSTEM_STATUS = "system_status" - private const val KEY_SYSTEM_INFO = "system_info" - private const val KEY_VERSION_INFO = "version_info" - private const val KEY_LAST_UPDATE = "last_update_time" - private const val KEY_ERROR_COUNT = "error_count" - private const val MAX_ERROR_COUNT = 2 - } // 系统状态 data class SystemStatus( @@ -60,7 +49,6 @@ class HomeViewModel : ViewModel() { val suSFSVersion: String = "", val suSFSVariant: String = "", val suSFSFeatures: String = "", - val susSUMode: String = "", val superuserCount: Int = 0, val moduleCount: Int = 0, val kpmModuleCount: Int = 0, @@ -69,9 +57,7 @@ class HomeViewModel : ViewModel() { val zygiskImplement: String = "" ) - private val gson = Gson() - private val prefs by lazy { ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) } - + // 状态变量 var systemStatus by mutableStateOf(SystemStatus()) private set @@ -98,199 +84,52 @@ class HomeViewModel : ViewModel() { var showKpmInfo by mutableStateOf(false) private set - private fun clearAllCache() { - try { - prefs.edit { clear() } - Log.i(TAG, "All cache cleared successfully") - } catch (e: Exception) { - Log.e(TAG, "Error clearing cache", e) - } - } + var isCoreDataLoaded by mutableStateOf(false) + private set + var isExtendedDataLoaded by mutableStateOf(false) + private set + var isRefreshing by mutableStateOf(false) + private set - private fun resetToDefaults() { - systemStatus = SystemStatus() - systemInfo = SystemInfo() - latestVersionInfo = LatestVersionInfo() - isSimpleMode = false - isKernelSimpleMode = false - isHideVersion = false - isHideOtherInfo = false - isHideSusfsStatus = false - isHideZygiskImplement = false - isHideLinkCard = false - showKpmInfo = false - } - - private fun handleError(error: Exception, operation: String) { - Log.e(TAG, "Error in $operation", error) + // 数据刷新状态流,用于监听变化 + private val _dataRefreshTrigger = MutableStateFlow(0L) + val dataRefreshTrigger: StateFlow = _dataRefreshTrigger - val errorCount = prefs.getInt(KEY_ERROR_COUNT, 0) - val newErrorCount = errorCount + 1 - - if (newErrorCount >= MAX_ERROR_COUNT) { - Log.w(TAG, "Too many errors ($newErrorCount), clearing cache and resetting") - clearAllCache() - resetToDefaults() - } else { - prefs.edit { - putInt(KEY_ERROR_COUNT, newErrorCount) - } - } - } - - private fun String?.orSafe(default: String = ""): String { - return if (this.isNullOrBlank()) default else this - } - - private fun Pair?.orSafe(default: Pair): Pair { - return if (this?.first == null || this.second == null) default else Pair(this.first!!, this.second!!) - } + private var loadingJobs = mutableListOf() + private var lastRefreshTime = 0L + private val refreshCooldown = 2000L fun loadUserSettings(context: Context) { viewModelScope.launch(Dispatchers.IO) { - try { - val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - isSimpleMode = settingsPrefs.getBoolean("is_simple_mode", false) - isKernelSimpleMode = settingsPrefs.getBoolean("is_kernel_simple_mode", false) - isHideVersion = settingsPrefs.getBoolean("is_hide_version", false) - isHideOtherInfo = settingsPrefs.getBoolean("is_hide_other_info", false) - isHideSusfsStatus = settingsPrefs.getBoolean("is_hide_susfs_status", false) - isHideLinkCard = settingsPrefs.getBoolean("is_hide_link_card", false) - isHideZygiskImplement = settingsPrefs.getBoolean("is_hide_zygisk_Implement", false) - showKpmInfo = settingsPrefs.getBoolean("show_kpm_info", false) - } catch (e: Exception) { - handleError(e, "loadUserSettings") - } + val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + isSimpleMode = settingsPrefs.getBoolean("is_simple_mode", false) + isKernelSimpleMode = settingsPrefs.getBoolean("is_kernel_simple_mode", false) + isHideVersion = settingsPrefs.getBoolean("is_hide_version", false) + isHideOtherInfo = settingsPrefs.getBoolean("is_hide_other_info", false) + isHideSusfsStatus = settingsPrefs.getBoolean("is_hide_susfs_status", false) + isHideLinkCard = settingsPrefs.getBoolean("is_hide_link_card", false) + isHideZygiskImplement = settingsPrefs.getBoolean("is_hide_zygisk_Implement", false) + showKpmInfo = settingsPrefs.getBoolean("show_kpm_info", false) } } - fun initializeData() { - viewModelScope.launch { - try { - loadCachedData() - // 成功加载后重置错误计数 - prefs.edit { - putInt(KEY_ERROR_COUNT, 0) - } - } catch(e: Exception) { - handleError(e, "initializeData") - } - } - } + fun loadCoreData() { + if (isCoreDataLoaded) return - private fun loadCachedData() { - try { - prefs.getString(KEY_SYSTEM_STATUS, null)?.let { statusJson -> - try { - val cachedStatus = gson.fromJson(statusJson, SystemStatus::class.java) - if (cachedStatus != null) { - systemStatus = cachedStatus - } - } catch (e: JsonSyntaxException) { - Log.w(TAG, "Invalid system status JSON, using defaults", e) - } - } - - prefs.getString(KEY_SYSTEM_INFO, null)?.let { infoJson -> - try { - val cachedInfo = gson.fromJson(infoJson, SystemInfo::class.java) - if (cachedInfo != null) { - systemInfo = cachedInfo - } - } catch (e: JsonSyntaxException) { - Log.w(TAG, "Invalid system info JSON, using defaults", e) - } - } - - prefs.getString(KEY_VERSION_INFO, null)?.let { versionJson -> - try { - val cachedVersion = gson.fromJson(versionJson, LatestVersionInfo::class.java) - if (cachedVersion != null) { - latestVersionInfo = cachedVersion - } - } catch (e: JsonSyntaxException) { - Log.w(TAG, "Invalid version info JSON, using defaults", e) - } - } - } catch (e: Exception) { - Log.e(TAG, "Error loading cached data", e) - throw e - } - } - - private suspend fun fetchAndSaveData() { - try { - fetchSystemStatus() - fetchSystemInfo() - withContext(Dispatchers.IO) { - prefs.edit { - putString(KEY_SYSTEM_STATUS, gson.toJson(systemStatus)) - putString(KEY_SYSTEM_INFO, gson.toJson(systemInfo)) - putString(KEY_VERSION_INFO, gson.toJson(latestVersionInfo)) - putLong(KEY_LAST_UPDATE, System.currentTimeMillis()) - putInt(KEY_ERROR_COUNT, 0) - } - } - } catch (e: Exception) { - handleError(e, "fetchAndSaveData") - } - } - - fun checkForUpdates(context: Context) { - viewModelScope.launch(Dispatchers.IO) { - try { - val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val checkUpdate = settingsPrefs.getBoolean("check_update", true) - - if (checkUpdate) { - val newVersionInfo = checkNewVersion() - latestVersionInfo = newVersionInfo - prefs.edit { - putString(KEY_VERSION_INFO, gson.toJson(newVersionInfo)) - putLong(KEY_LAST_UPDATE, System.currentTimeMillis()) - } - } - } catch (e: Exception) { - handleError(e, "checkForUpdates") - } - } - } - - fun refreshAllData(context: Context) { - viewModelScope.launch { - try { - fetchAndSaveData() - checkForUpdates(context) - } catch (e: Exception) { - handleError(e, "refreshAllData") - } - } - } - - private suspend fun fetchSystemStatus() { - withContext(Dispatchers.IO) { + val job = viewModelScope.launch(Dispatchers.IO) { try { val kernelVersion = getKernelVersion() val isManager = try { - Natives.becomeManager(ksuApp.packageName.orSafe("com.sukisu.ultra")) - } catch (e: Exception) { - Log.w(TAG, "Failed to become manager", e) + Natives.isManager + } catch (_: Exception) { false } - val ksuVersion = if (isManager) { - try { - Natives.version - } catch (e: Exception) { - Log.w(TAG, "Failed to get KSU version", e) - null - } - } else null + val ksuVersion = if (isManager) Natives.version else null val fullVersion = try { - Natives.getFullVersion().orSafe("Unknown") - } catch (e: Exception) { - Log.w(TAG, "Failed to get full version", e) + Natives.getFullVersion() + } catch (_: Exception) { "Unknown" } @@ -309,8 +148,7 @@ class HomeViewModel : ViewModel() { } else { fullVersion } - } catch (e: Exception) { - Log.w(TAG, "Failed to process full version", e) + } catch (_: Exception) { fullVersion } } else { @@ -318,34 +156,24 @@ class HomeViewModel : ViewModel() { } val lkmMode = ksuVersion?.let { - try { - if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) { - Natives.isLkmMode - } else null - } catch (e: Exception) { - Log.w(TAG, "Failed to get LKM mode", e) - null - } + if (kernelVersion.isGKI()) Natives.isLkmMode else null } val isRootAvailable = try { rootAvailable() - } catch (e: Exception) { - Log.w(TAG, "Failed to check root availability", e) + } catch (_: Exception) { false } val isKpmConfigured = try { Natives.isKPMEnabled() - } catch (e: Exception) { - Log.w(TAG, "Failed to check KPM status", e) + } catch (_: Exception) { false } val requireNewKernel = try { isManager && Natives.requireNewKernel() - } catch (e: Exception) { - Log.w(TAG, "Failed to check kernel requirement", e) + } catch (_: Exception) { false } @@ -359,198 +187,321 @@ class HomeViewModel : ViewModel() { isKpmConfigured = isKpmConfigured, requireNewKernel = requireNewKernel ) - } catch (e: Exception) { - Log.e(TAG, "Error fetching system status", e) - throw e + + isCoreDataLoaded = true + } catch (_: Exception) { } } + loadingJobs.add(job) } - @SuppressLint("RestrictedApi") - private suspend fun fetchSystemInfo() { - withContext(Dispatchers.IO) { + fun loadExtendedData(context: Context) { + if (isExtendedDataLoaded) return + + val job = viewModelScope.launch(Dispatchers.IO) { try { - val uname = try { - Os.uname() - } catch (e: Exception) { - Log.w(TAG, "Failed to get uname", e) - null - } + // 分批加载 + delay(50) - val kpmVersion = try { - getKpmVersion().orSafe("Unknown") - } catch (e: Exception) { - Log.w(TAG, "Failed to get kpm version", e) - "Unknown" - } - - val suSFS = try { - getSuSFS().orSafe("Unknown") - } catch (e: Exception) { - Log.w(TAG, "Failed to get SuSFS", e) - "Unknown" - } - - var suSFSVersion = "" - var suSFSVariant = "" - var suSFSFeatures = "" - var susSUMode = "" - - if (suSFS == "Supported") { - suSFSVersion = try { - getSuSFSVersion().orSafe("") - } catch (e: Exception) { - Log.w(TAG, "Failed to get SuSFS version", e) - "" - } - - if (suSFSVersion.isNotEmpty()) { - suSFSVariant = try { - getSuSFSVariant().orSafe("") - } catch (e: Exception) { - Log.w(TAG, "Failed to get SuSFS variant", e) - "" - } - - suSFSFeatures = try { - getSuSFSFeatures().orSafe("") - } catch (e: Exception) { - Log.w(TAG, "Failed to get SuSFS features", e) - "" - } - - val isSUS_SU = suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU" - if (isSUS_SU) { - susSUMode = try { - susfsSUS_SU_Mode() - } catch (e: Exception) { - Log.w(TAG, "Failed to get SUS SU mode", e) - "" - } - } - } - } - - // 获取动态管理器状态和管理器列表 - val dynamicSignConfig = try { - Natives.getDynamicManager() - } catch (e: Exception) { - Log.w(TAG, "Failed to get dynamic manager config", e) - null - } - - val isDynamicSignEnabled = try { - dynamicSignConfig?.isValid() == true - } catch (e: Exception) { - Log.w(TAG, "Failed to check dynamic manager validity", e) - false - } - - val managersList = if (isDynamicSignEnabled) { - try { - Natives.getManagersList() - } catch (e: Exception) { - Log.w(TAG, "Failed to get managers list", e) - null - } - } else { - null - } - - val deviceModel = try { - getDeviceModel().orSafe("Unknown") - } catch (e: Exception) { - Log.w(TAG, "Failed to get device model", e) - "Unknown" - } - - val managerVersion = try { - getManagerVersion(ksuApp.applicationContext).orSafe(Pair("Unknown", 0L)) - } catch (e: Exception) { - Log.w(TAG, "Failed to get manager version", e) - Pair("Unknown", 0L) - } - - val seLinuxStatus = try { - getSELinuxStatus(context).orSafe("Unknown") - } catch (e: Exception) { - Log.w(TAG, "Failed to get SELinux status", e) - "Unknown" - } - - val superuserCount = try { - getSuperuserCount() - } catch (e: Exception) { - Log.w(TAG, "Failed to get superuser count", e) - 0 - } - - val moduleCount = try { - getModuleCount() - } catch (e: Exception) { - Log.w(TAG, "Failed to get module count", e) - 0 - } - - val kpmModuleCount = try { - getKpmModuleCount() - } catch (e: Exception) { - Log.w(TAG, "Failed to get kpm module count", e) - 0 - } - - val zygiskImplement = try { - getZygiskImplement().orSafe("None") - } catch (e: Exception) { - Log.w(TAG, "Failed to get Zygisk implement", e) - "None" - } - - systemInfo = SystemInfo( - kernelRelease = uname?.release.orSafe("Unknown"), - androidVersion = Build.VERSION.RELEASE.orSafe("Unknown"), - deviceModel = deviceModel, - managerVersion = managerVersion, - seLinuxStatus = seLinuxStatus, - kpmVersion = kpmVersion, - suSFSStatus = suSFS, - suSFSVersion = suSFSVersion, - suSFSVariant = suSFSVariant, - suSFSFeatures = suSFSFeatures, - susSUMode = susSUMode, - superuserCount = superuserCount, - moduleCount = moduleCount, - kpmModuleCount = kpmModuleCount, - managersList = managersList, - isDynamicSignEnabled = isDynamicSignEnabled, - zygiskImplement = zygiskImplement + val basicInfo = loadBasicSystemInfo(context) + systemInfo = systemInfo.copy( + kernelRelease = basicInfo.first, + androidVersion = basicInfo.second, + deviceModel = basicInfo.third, + managerVersion = basicInfo.fourth, + seLinuxStatus = basicInfo.fifth ) - } catch (e: Exception) { - Log.e(TAG, "Error fetching system info", e) - throw e + + delay(100) + + // 加载模块信息 + if (!isSimpleMode) { + val moduleInfo = loadModuleInfo() + systemInfo = systemInfo.copy( + kpmVersion = moduleInfo.first, + superuserCount = moduleInfo.second, + moduleCount = moduleInfo.third, + kpmModuleCount = moduleInfo.fourth, + zygiskImplement = moduleInfo.fifth + ) + } + + delay(100) + + // 加载SuSFS信息 + if (!isHideSusfsStatus) { + val suSFSInfo = loadSuSFSInfo() + systemInfo = systemInfo.copy( + suSFSStatus = suSFSInfo.first, + suSFSVersion = suSFSInfo.second, + suSFSVariant = suSFSInfo.third, + suSFSFeatures = suSFSInfo.fourth, + ) + } + + delay(100) + + // 加载管理器列表 + val managerInfo = loadManagerInfo() + systemInfo = systemInfo.copy( + managersList = managerInfo.first, + isDynamicSignEnabled = managerInfo.second + ) + + isExtendedDataLoaded = true + } catch (_: Exception) { + // 静默处理错误 + } + } + loadingJobs.add(job) + } + + fun refreshData(context: Context, forceRefresh: Boolean = false) { + val currentTime = System.currentTimeMillis() + + // 如果不是强制刷新,检查冷却时间 + if (!forceRefresh && currentTime - lastRefreshTime < refreshCooldown) { + return + } + + lastRefreshTime = currentTime + + viewModelScope.launch { + isRefreshing = true + + try { + // 取消正在进行的加载任务 + loadingJobs.forEach { it.cancel() } + loadingJobs.clear() + + // 重置状态 + isCoreDataLoaded = false + isExtendedDataLoaded = false + + // 触发数据刷新状态流 + _dataRefreshTrigger.value = currentTime + + // 重新加载用户设置 + loadUserSettings(context) + + // 重新加载核心数据 + loadCoreData() + delay(100) + + // 重新加载扩展数据 + loadExtendedData(context) + + // 检查更新 + val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val checkUpdate = settingsPrefs.getBoolean("check_update", true) + if (checkUpdate) { + try { + val newVersionInfo = withContext(Dispatchers.IO) { + checkNewVersion() + } + latestVersionInfo = newVersionInfo + } catch (_: Exception) { + } + } + } catch (_: Exception) { + // 静默处理错误 + } finally { + isRefreshing = false } } } - private fun getDeviceInfo(): String { - return try { - var manufacturer = Build.MANUFACTURER.orSafe("Unknown") - manufacturer = manufacturer[0].uppercaseChar().toString() + manufacturer.substring(1) + // 手动触发刷新(下拉刷新使用) + fun onPullRefresh(context: Context) { + refreshData(context, forceRefresh = true) + } - val brand = Build.BRAND.orSafe("") - if (brand.isNotEmpty() && !brand.equals(Build.MANUFACTURER, ignoreCase = true)) { - manufacturer += " " + brand[0].uppercaseChar() + brand.substring(1) + // 自动刷新数据(当检测到变化时) + fun autoRefreshIfNeeded(context: Context) { + viewModelScope.launch { + // 检查是否需要刷新数据 + val needsRefresh = checkIfDataNeedsRefresh() + if (needsRefresh) { + refreshData(context) + } + } + } + + private suspend fun checkIfDataNeedsRefresh(): Boolean { + return withContext(Dispatchers.IO) { + try { + // 检查KSU状态是否发生变化 + val currentKsuVersion = try { + if (Natives.isManager) { + Natives.version + } else null + } catch (_: Exception) { + null + } + + // 如果KSU版本发生变化,需要刷新 + if (currentKsuVersion != systemStatus.ksuVersion) { + return@withContext true + } + + // 检查模块数量是否发生变化 + val currentModuleCount = try { + getModuleCount() + } catch (_: Exception) { + systemInfo.moduleCount + } + + if (currentModuleCount != systemInfo.moduleCount) { + return@withContext true + } + + false + } catch (_: Exception) { + false + } + } + } + + private suspend fun loadBasicSystemInfo(context: Context): Tuple5, String> { + return withContext(Dispatchers.IO) { + val uname = try { + Os.uname() + } catch (_: Exception) { + null } - val model = Build.MODEL.orSafe("") - if (model.isNotEmpty()) { - manufacturer += " $model " + val deviceModel = try { + getDeviceModel() + } catch (_: Exception) { + "Unknown" } - manufacturer - } catch (e: Exception) { - Log.w(TAG, "Failed to get device info", e) - "Unknown Device" + val managerVersion = try { + getManagerVersion(context) + } catch (_: Exception) { + Pair("Unknown", 0L) + } + + val seLinuxStatus = try { + getSELinuxStatus(ksuApp.applicationContext) + } catch (_: Exception) { + "Unknown" + } + + Tuple5( + uname?.release ?: "Unknown", + Build.VERSION.RELEASE ?: "Unknown", + deviceModel, + managerVersion, + seLinuxStatus + ) + } + } + + private suspend fun loadModuleInfo(): Tuple5 { + return withContext(Dispatchers.IO) { + val kpmVersion = try { + getKpmVersion() + } catch (_: Exception) { + "Unknown" + } + + val superuserCount = try { + getSuperuserCount() + } catch (_: Exception) { + 0 + } + + val moduleCount = try { + getModuleCount() + } catch (_: Exception) { + 0 + } + + val kpmModuleCount = try { + getKpmModuleCount() + } catch (_: Exception) { + 0 + } + + val zygiskImplement = try { + getZygiskImplement() + } catch (_: Exception) { + "None" + } + + Tuple5(kpmVersion, superuserCount, moduleCount, kpmModuleCount, zygiskImplement) + } + } + + private suspend fun loadSuSFSInfo(): Tuple4 { + return withContext(Dispatchers.IO) { + val suSFS = try { + val rawFeature = getSuSFSFeatures() + if (rawFeature.isNotEmpty() && !rawFeature.startsWith("[-]")) { + "Supported" + } else { + rawFeature + } + } catch (_: Exception) { + "Unknown" + } + + if (suSFS != "Supported") { + return@withContext Tuple4(suSFS, "", "", "") + } + + val suSFSVersion = try { + getSuSFSVersion() + } catch (_: Exception) { + "" + } + + if (suSFSVersion.isEmpty()) { + return@withContext Tuple4(suSFS, "", "", "") + } + + val suSFSVariant = try { + getSuSFSVariant() + } catch (_: Exception) { + "" + } + + val suSFSFeatures = try { + getSuSFSFeatures() + } catch (_: Exception) { + "" + } + + Tuple4(suSFS, suSFSVersion, suSFSVariant, suSFSFeatures) + } + } + + private suspend fun loadManagerInfo(): Pair { + return withContext(Dispatchers.IO) { + val dynamicSignConfig = try { + Natives.getDynamicManager() + } catch (_: Exception) { + null + } + + val isDynamicSignEnabled = try { + dynamicSignConfig?.isValid() == true + } catch (_: Exception) { + false + } + + val managersList = if (isDynamicSignEnabled) { + try { + Natives.getManagersList() + } catch (_: Exception) { + null + } + } else { + null + } + + Pair(managersList, isDynamicSignEnabled) } } @@ -560,10 +511,10 @@ class HomeViewModel : ViewModel() { val systemProperties = Class.forName("android.os.SystemProperties") val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java) val marketNameKeys = listOf( - "ro.product.marketname", // Xiaomi - "ro.vendor.oplus.market.name", // Oppo, OnePlus, Realme - "ro.vivo.market.name", // Vivo - "ro.config.marketing_name" // Huawei + "ro.product.marketname", + "ro.vendor.oplus.market.name", + "ro.vivo.market.name", + "ro.config.marketing_name" ) var result = getDeviceInfo() for (key in marketNameKeys) { @@ -573,26 +524,67 @@ class HomeViewModel : ViewModel() { result = marketName break } - } catch (e: Exception) { - Log.w(TAG, "Failed to get market name for key: $key", e) + } catch (_: Exception) { } } result - } catch (e: Exception) { - Log.w(TAG, "Error getting device model", e) + } catch ( + + _: Exception) { getDeviceInfo() } } + private fun getDeviceInfo(): String { + return try { + var manufacturer = Build.MANUFACTURER ?: "Unknown" + manufacturer = manufacturer[0].uppercaseChar().toString() + manufacturer.substring(1) + + val brand = Build.BRAND ?: "" + if (brand.isNotEmpty() && !brand.equals(Build.MANUFACTURER, ignoreCase = true)) { + manufacturer += " " + brand[0].uppercaseChar() + brand.substring(1) + } + + val model = Build.MODEL ?: "" + if (model.isNotEmpty()) { + manufacturer += " $model " + } + + manufacturer + } catch (_: Exception) { + "Unknown Device" + } + } + private fun getManagerVersion(context: Context): Pair { return try { val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val versionCode = androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(packageInfo) - val versionName = packageInfo.versionName.orSafe("Unknown") + val versionName = packageInfo.versionName ?: "Unknown" Pair(versionName, versionCode) - } catch (e: Exception) { - Log.w(TAG, "Error getting manager version", e) + } catch (_: Exception) { Pair("Unknown", 0L) } } + + data class Tuple5( + val first: T1, + val second: T2, + val third: T3, + val fourth: T4, + val fifth: T5 + ) + + data class Tuple4( + val first: T1, + val second: T2, + val third: T3, + val fourth: T4 + ) + + override fun onCleared() { + super.onCleared() + loadingJobs.forEach { it.cancel() } + loadingJobs.clear() + } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt index f8dfe2e..7a5fd9b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.launch import com.sukisu.ultra.ui.util.HanziToPinyin import com.sukisu.ultra.ui.util.listModules import com.sukisu.ultra.ui.util.getRootShell -import com.sukisu.ultra.ui.util.ModuleVerificationManager +import com.sukisu.ultra.ui.util.module.ModuleVerificationManager import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONObject @@ -105,7 +105,7 @@ class ModuleViewModel : ViewModel() { ).thenBy(Collator.getInstance(Locale.getDefault()), ModuleInfo::id) modules.filter { it.id.contains(search, true) || it.name.contains(search, true) || HanziToPinyin.getInstance() - .toPinyinString(it.name).contains(search, true) + .toPinyinString(it.name)?.contains(search, true) == true }.sortedWith(comparator).also { isRefreshing = false } @@ -143,15 +143,15 @@ class ModuleViewModel : ViewModel() { obj.optString("name"), obj.optString("author", "Unknown"), obj.optString("version", "Unknown"), - obj.optInt("versionCode", 0), + obj.getIntCompat("versionCode", 0), obj.optString("description"), - obj.getBoolean("enabled"), - obj.getBoolean("update"), - obj.getBoolean("remove"), + obj.getBooleanCompat("enabled"), + obj.getBooleanCompat("update"), + obj.getBooleanCompat("remove"), obj.optString("updateJson"), - obj.optBoolean("web"), - obj.optBoolean("action"), - obj.getString("dir_id") + obj.getBooleanCompat("web"), + obj.getBooleanCompat("action"), + obj.optString("dir_id", obj.getString("id")) ) }.toList() @@ -469,6 +469,26 @@ class ModuleSizeCache(context: Context) { } } +private fun JSONObject.getBooleanCompat(key: String, default: Boolean = false): Boolean { + if (!has(key)) return default + return when (val value = opt(key)) { + is Boolean -> value + is String -> value.equals("true", ignoreCase = true) || value == "1" + is Number -> value.toInt() != 0 + else -> default + } +} + +private fun JSONObject.getIntCompat(key: String, default: Int = 0): Int { + if (!has(key)) return default + return when (val value = opt(key)) { + is Int -> value + is Number -> value.toInt() + is String -> value.toIntOrNull() ?: default + else -> default + } +} + /** * 格式化文件大小的工具函数 */ diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt index 0bd0320..128c238 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt @@ -3,9 +3,9 @@ package com.sukisu.ultra.ui.viewmodel import android.content.* import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo +import android.graphics.drawable.Drawable import android.os.IBinder import android.os.Parcelable -import android.os.SystemClock import android.util.Log import androidx.compose.runtime.* import androidx.core.content.edit @@ -13,12 +13,11 @@ import androidx.lifecycle.ViewModel import com.sukisu.ultra.Natives import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ui.KsuService -import com.sukisu.ultra.ui.util.HanziToPinyin +import com.sukisu.ultra.ui.util.* import com.topjohnwu.superuser.Shell import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.parcelize.Parcelize import java.text.Collator import java.util.* import java.util.concurrent.LinkedBlockingQueue @@ -26,8 +25,12 @@ import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +import com.sukisu.zako.IKsuInterface +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize -// 应用分类 enum class AppCategory(val displayNameRes: Int, val persistKey: String) { ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"), ROOT(com.sukisu.ultra.R.string.category_root_apps, "ROOT"), @@ -35,13 +38,10 @@ enum class AppCategory(val displayNameRes: Int, val persistKey: String) { DEFAULT(com.sukisu.ultra.R.string.category_default_apps, "DEFAULT"); companion object { - fun fromPersistKey(key: String): AppCategory { - return entries.find { it.persistKey == key } ?: ALL - } + fun fromPersistKey(key: String): AppCategory = entries.find { it.persistKey == key } ?: ALL } } -// 排序方式 enum class SortType(val displayNameRes: Int, val persistKey: String) { NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"), NAME_DESC(com.sukisu.ultra.R.string.sort_name_desc, "NAME_DESC"), @@ -52,20 +52,27 @@ enum class SortType(val displayNameRes: Int, val persistKey: String) { USAGE_FREQ(com.sukisu.ultra.R.string.sort_usage_freq, "USAGE_FREQ"); companion object { - fun fromPersistKey(key: String): SortType { - return entries.find { it.persistKey == key } ?: NAME_ASC - } + fun fromPersistKey(key: String): SortType = entries.find { it.persistKey == key } ?: NAME_ASC } } -/** - * @author ShirkNeko - * @date 2025/5/31. - */ class SuperUserViewModel : ViewModel() { companion object { private const val TAG = "SuperUserViewModel" + private val appsLock = Any() var apps by mutableStateOf>(emptyList()) + private val _isAppListLoaded = MutableStateFlow(false) + val isAppListLoaded = _isAppListLoaded.asStateFlow() + + @JvmStatic + fun getAppIconDrawable(context: Context, packageName: String): Drawable? { + val appList = synchronized(appsLock) { apps } + return appList.find { it.packageName == packageName } + ?.packageInfo?.applicationInfo?.loadIcon(context.packageManager) + } + + var appGroups by mutableStateOf>(emptyList()) + private const val PREFS_NAME = "settings" private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps" private const val KEY_SELECTED_CATEGORY = "selected_category" @@ -76,37 +83,40 @@ class SuperUserViewModel : ViewModel() { private const val BATCH_SIZE = 20 } + @Immutable @Parcelize data class AppInfo( val label: String, val packageInfo: PackageInfo, val profile: Natives.Profile?, ) : Parcelable { - val packageName: String - get() = packageInfo.packageName - val uid: Int - get() = packageInfo.applicationInfo!!.uid + @IgnoredOnParcel + val packageName: String = packageInfo.packageName + @IgnoredOnParcel + val uid: Int = packageInfo.applicationInfo!!.uid + } - val allowSu: Boolean - get() = profile != null && profile.allowSu - val hasCustomProfile: Boolean - get() { - if (profile == null) { - return false - } - return if (profile.allowSu) { - !profile.rootUseDefault - } else { - !profile.nonRootUseDefault - } - } + @Immutable + @Parcelize + data class AppGroup( + val uid: Int, + val apps: List, + val profile: Natives.Profile? + ) : Parcelable { + @IgnoredOnParcel + val mainApp: AppInfo = apps.first() + @IgnoredOnParcel + val packageNames: List = apps.map { it.packageName } + @IgnoredOnParcel + val allowSu: Boolean = profile?.allowSu == true + @IgnoredOnParcel + val userName: String? = Natives.getUserName(uid) + @IgnoredOnParcel + val hasCustomProfile : Boolean = profile?.let { if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault } ?: false } private val appProcessingThreadPool = ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, + CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, LinkedBlockingQueue() ) { runnable -> Thread(runnable, "AppProcessing-${System.currentTimeMillis()}").apply { @@ -116,65 +126,40 @@ class SuperUserViewModel : ViewModel() { }.asCoroutineDispatcher() private val appListMutex = Mutex() - private val configChangeListeners = mutableSetOf<(String) -> Unit>() - - private val prefs: SharedPreferences = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + private val prefs = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) var search by mutableStateOf("") - - var showSystemApps by mutableStateOf(loadShowSystemApps()) + var showSystemApps by mutableStateOf(prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false)) private set - var selectedCategory by mutableStateOf(loadSelectedCategory()) private set - var currentSortType by mutableStateOf(loadCurrentSortType()) private set var isRefreshing by mutableStateOf(false) private set - - // 批量操作相关状态 var showBatchActions by mutableStateOf(false) internal set var selectedApps by mutableStateOf>(emptySet()) internal set - - // 加载进度状态 var loadingProgress by mutableFloatStateOf(0f) private set - var loadingMessage by mutableStateOf("") - private set - /** - * 从SharedPreferences加载显示系统应用设置 - */ - private fun loadShowSystemApps(): Boolean { - return prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false) - } - - /** - * 从SharedPreferences加载选择的应用分类 - */ private fun loadSelectedCategory(): AppCategory { - val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) ?: AppCategory.ALL.persistKey + val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) + ?: AppCategory.ALL.persistKey return AppCategory.fromPersistKey(categoryKey) } - /** - * 从SharedPreferences加载当前排序方式 - */ private fun loadCurrentSortType(): SortType { - val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) ?: SortType.NAME_ASC.persistKey + val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) + ?: SortType.NAME_ASC.persistKey return SortType.fromPersistKey(sortKey) } - /** - * 更新显示系统应用设置并保存到SharedPreferences - */ fun updateShowSystemApps(newValue: Boolean) { showSystemApps = newValue - saveShowSystemApps(newValue) + prefs.edit { putBoolean(KEY_SHOW_SYSTEM_APPS, newValue) } notifyAppListChanged() } @@ -184,88 +169,21 @@ class SuperUserViewModel : ViewModel() { apps = currentApps } - /** - * 更新选择的应用分类并保存到SharedPreferences - */ fun updateSelectedCategory(newCategory: AppCategory) { selectedCategory = newCategory - saveSelectedCategory(newCategory) + prefs.edit { putString(KEY_SELECTED_CATEGORY, newCategory.persistKey) } } - /** - * 更新当前排序方式并保存到SharedPreferences - */ fun updateCurrentSortType(newSortType: SortType) { currentSortType = newSortType - saveCurrentSortType(newSortType) + prefs.edit { putString(KEY_CURRENT_SORT_TYPE, newSortType.persistKey) } } - /** - * 保存显示系统应用设置到SharedPreferences - */ - private fun saveShowSystemApps(value: Boolean) { - prefs.edit { - putBoolean(KEY_SHOW_SYSTEM_APPS, value) - } - Log.d(TAG, "Saved show system apps: $value") - } - - /** - * 保存选择的应用分类到SharedPreferences - */ - private fun saveSelectedCategory(category: AppCategory) { - prefs.edit { - putString(KEY_SELECTED_CATEGORY, category.persistKey) - } - Log.d(TAG, "Saved selected category: ${category.persistKey}") - } - - /** - * 保存当前排序方式到SharedPreferences - */ - private fun saveCurrentSortType(sortType: SortType) { - prefs.edit { - putString(KEY_CURRENT_SORT_TYPE, sortType.persistKey) - } - Log.d(TAG, "Saved current sort type: ${sortType.persistKey}") - } - - private val sortedList by derivedStateOf { - val comparator = compareBy { - when { - it.allowSu -> 0 - it.hasCustomProfile -> 1 - else -> 2 - } - }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) - apps.sortedWith(comparator).also { - isRefreshing = false - } - } - - val appList by derivedStateOf { - val filtered = sortedList.filter { - it.label.contains(search, true) || it.packageName.contains( - search, - true - ) || HanziToPinyin.getInstance() - .toPinyinString(it.label).contains(search, true) - }.filter { - it.uid == 2000 || showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 - } - - filtered - } - - // 切换批量操作模式 fun toggleBatchMode() { showBatchActions = !showBatchActions - if (!showBatchActions) { - clearSelection() - } + if (!showBatchActions) clearSelection() } - // 切换应用选择状态 fun toggleAppSelection(packageName: String) { selectedApps = if (selectedApps.contains(packageName)) { selectedApps - packageName @@ -274,35 +192,14 @@ class SuperUserViewModel : ViewModel() { } } - // 清除所有选择 fun clearSelection() { selectedApps = emptySet() } - // 批量更新权限 - suspend fun updateBatchPermissions(allowSu: Boolean) { - selectedApps.forEach { packageName -> - val app = apps.find { it.packageName == packageName } - app?.let { - val profile = Natives.getAppProfile(packageName, it.uid) - val updatedProfile = profile.copy(allowSu = allowSu) - if (Natives.setAppProfile(updatedProfile)) { - updateAppProfileLocally(packageName, updatedProfile) - notifyConfigChange(packageName) - } - } - } - clearSelection() - showBatchActions = false - refreshAppConfigurations() - } - - // 批量更新权限和umount模块设置 suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) { selectedApps.forEach { packageName -> - val app = apps.find { it.packageName == packageName } - app?.let { - val profile = Natives.getAppProfile(packageName, it.uid) + apps.find { it.packageName == packageName }?.let { app -> + val profile = Natives.getAppProfile(packageName, app.uid) val updatedProfile = profile.copy( allowSu = allowSu, umountModules = umountModules ?: profile.umountModules, @@ -319,7 +216,6 @@ class SuperUserViewModel : ViewModel() { refreshAppConfigurations() } - // 更新本地应用配置 fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) { appListMutex.tryLock().let { locked -> if (locked) { @@ -327,9 +223,7 @@ class SuperUserViewModel : ViewModel() { apps = apps.map { app -> if (app.packageName == packageName) { app.copy(profile = updatedProfile) - } else { - app - } + } else app } } finally { appListMutex.unlock() @@ -348,15 +242,11 @@ class SuperUserViewModel : ViewModel() { } } - /** - * 刷新应用配置状态 - */ suspend fun refreshAppConfigurations() { withContext(appProcessingThreadPool) { supervisorScope { val currentApps = apps.toList() val batches = currentApps.chunked(BATCH_SIZE) - loadingProgress = 0f val updatedApps = batches.mapIndexed { batchIndex, batch -> @@ -370,59 +260,45 @@ class SuperUserViewModel : ViewModel() { app } } - - val progress = (batchIndex + 1).toFloat() / batches.size - loadingProgress = progress - + loadingProgress = (batchIndex + 1).toFloat() / batches.size batchResult } }.awaitAll().flatten() - appListMutex.withLock { - apps = updatedApps - } - + appListMutex.withLock { apps = updatedApps } loadingProgress = 1f - - Log.i(TAG, "Refreshed configurations for ${updatedApps.size} apps") } } } private var serviceConnection: ServiceConnection? = null - private suspend fun connectKsuService( - onDisconnect: () -> Unit = {} - ): IBinder? = suspendCoroutine { continuation -> - val connection = object : ServiceConnection { - override fun onServiceDisconnected(name: ComponentName?) { - onDisconnect() - serviceConnection = null + private suspend fun connectKsuService(onDisconnect: () -> Unit = {}): IBinder? = + suspendCoroutine { continuation -> + val connection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) { + onDisconnect() + serviceConnection = null + } + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + continuation.resume(binder) + } } - - override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { - continuation.resume(binder) + serviceConnection = connection + val intent = Intent(ksuApp, KsuService::class.java) + try { + val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask( + intent, Shell.EXECUTOR, connection + ) + task?.let { Shell.getShell().execTask(it) } + } catch (e: Exception) { + Log.e(TAG, "Failed to bind KsuService", e) + continuation.resume(null) } } - serviceConnection = connection - val intent = Intent(ksuApp, KsuService::class.java) - - try { - val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask( - intent, - Shell.EXECUTOR, - connection - ) - task?.let { Shell.getShell().execTask(it) } - } catch (e: Exception) { - Log.e(TAG, "Failed to bind KsuService", e) - continuation.resume(null) - } - } - private fun stopKsuService() { - serviceConnection?.let { connection -> + serviceConnection?.let { try { val intent = Intent(ksuApp, KsuService::class.java) com.topjohnwu.superuser.ipc.RootService.stop(intent) @@ -437,60 +313,81 @@ class SuperUserViewModel : ViewModel() { isRefreshing = true loadingProgress = 0f - val result = connectKsuService { - Log.w(TAG, "KsuService disconnected") - } - - if (result == null) { - Log.e(TAG, "Failed to connect to KsuService") - isRefreshing = false - return - } + val binder = connectKsuService() ?: run { isRefreshing = false; return } withContext(Dispatchers.IO) { val pm = ksuApp.packageManager - val start = SystemClock.elapsedRealtime() + val allPackages = IKsuInterface.Stub.asInterface(binder) + val total = allPackages.packageCount + val pageSize = 100 + val result = mutableListOf() - try { - val service = KsuService.Stub.asInterface(result) - val allPackages = service?.getPackages(0) + var start = 0 + while (start < total) { + val page = allPackages.getPackages(start, pageSize) + if (page.isEmpty()) break - withContext(Dispatchers.Main) { - stopKsuService() + result += page.mapNotNull { packageInfo -> + packageInfo.applicationInfo?.let { appInfo -> + AppInfo( + label = appInfo.loadLabel(pm).toString(), + packageInfo = packageInfo, + profile = Natives.getAppProfile(packageInfo.packageName, appInfo.uid) + ) + } } - loadingProgress = 0.3f - - val packages = allPackages?.list ?: emptyList() - - apps = packages.map { packageInfo -> - val appInfo = packageInfo.applicationInfo!! - val uid = appInfo.uid - val profile = Natives.getAppProfile(packageInfo.packageName, uid) - AppInfo( - label = appInfo.loadLabel(pm).toString(), - packageInfo = packageInfo, - profile = profile, - ) - }.filter { it.packageName != ksuApp.packageName } - - loadingProgress = 1f - - Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}") - } catch (e: Exception) { - Log.e(TAG, "Error fetching app list", e) - withContext(Dispatchers.Main) { - stopKsuService() - } - } finally { - isRefreshing = false - loadingProgress = 0f - loadingMessage = "" + start += page.size + loadingProgress = start.toFloat() / total } + + stopKsuService() + + synchronized(appsLock) { + _isAppListLoaded.value = true + } + + appListMutex.withLock { + val filteredApps = result.filter { it.packageName != ksuApp.packageName } + apps = filteredApps + appGroups = groupAppsByUid(filteredApps) + } + loadingProgress = 1f + } + isRefreshing = false + } + + val appGroupList by derivedStateOf { + appGroups.filter { group -> + group.apps.any { app -> + app.label.contains(search, true) || + app.packageName.contains(search, true) || + HanziToPinyin.getInstance().toPinyinString(app.label)?.contains(search, true) == true + } + }.filter { group -> + group.uid == 2000 || showSystemApps || + group.apps.any { it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 } } } - /** - * 清理资源 - */ + + private fun groupAppsByUid(appList: List): List { + return appList.groupBy { it.uid } + .map { (uid, apps) -> + val sortedApps = apps.sortedBy { it.label } + val profile = apps.firstOrNull()?.let { Natives.getAppProfile(it.packageName, uid) } + AppGroup(uid = uid, apps = sortedApps, profile = profile) + } + .sortedWith( + compareBy { + when { + it.allowSu -> 0 + it.hasCustomProfile -> 1 + else -> 2 + } + }.thenBy(Collator.getInstance(Locale.getDefault())) { + it.userName?.takeIf { name -> name.isNotBlank() } ?: it.uid.toString() + }.thenBy(Collator.getInstance(Locale.getDefault())) { it.mainApp.label } + ) +} override fun onCleared() { super.onCleared() try { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.kt new file mode 100644 index 0000000..361a976 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.kt @@ -0,0 +1,46 @@ +package com.sukisu.ultra.ui.webui + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.util.LruCache +import androidx.core.graphics.createBitmap +import androidx.core.graphics.scale +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel.Companion.getAppIconDrawable + +object AppIconUtil { + // Limit cache size to 200 icons + private const val CACHE_SIZE = 200 + private val iconCache = LruCache(CACHE_SIZE) + + @Synchronized + fun loadAppIconSync(context: Context, packageName: String, sizePx: Int): Bitmap? { + val cached = iconCache.get(packageName) + if (cached != null) return cached + + try { + val drawable = getAppIconDrawable(context, packageName) ?: return null + val raw = drawableToBitmap(drawable, sizePx) + val icon = raw.scale(sizePx, sizePx) + iconCache.put(packageName, icon) + return icon + } catch (_: Exception) { + return null + } + } + + private fun drawableToBitmap(drawable: Drawable, size: Int): Bitmap { + if (drawable is BitmapDrawable) return drawable.bitmap + + val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else size + val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else size + + val bmp = createBitmap(width, height) + val canvas = Canvas(bmp) + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + return bmp + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/Insets.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/Insets.kt new file mode 100644 index 0000000..aabdbe2 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/Insets.kt @@ -0,0 +1,40 @@ +package com.sukisu.ultra.ui.webui + +/** + * Insets data class from GitHub@MMRLApp/WebUI-X-Portable + * + * Data class representing insets (top, bottom, left, right) for a view. + * + * This class provides methods to generate CSS code that can be injected into a WebView + * to apply these insets as CSS variables. This is useful for adapting web content + * to the safe areas of a device screen, considering notches, status bars, and navigation bars. + * + * @property top The top inset value in pixels. + * @property bottom The bottom inset value in pixels. + * @property left The left inset value in pixels. + * @property right The right inset value in pixels. + */ +data class Insets( + val top: Int, + val bottom: Int, + val left: Int, + val right: Int, +) { + val css + get() = buildString { + appendLine(":root {") + appendLine("\t--safe-area-inset-top: ${top}px;") + appendLine("\t--safe-area-inset-right: ${right}px;") + appendLine("\t--safe-area-inset-bottom: ${bottom}px;") + appendLine("\t--safe-area-inset-left: ${left}px;") + appendLine("\t--window-inset-top: var(--safe-area-inset-top, 0px);") + appendLine("\t--window-inset-bottom: var(--safe-area-inset-bottom, 0px);") + appendLine("\t--window-inset-left: var(--safe-area-inset-left, 0px);") + appendLine("\t--window-inset-right: var(--safe-area-inset-right, 0px);") + appendLine("\t--f7-safe-area-top: var(--window-inset-top, 0px) !important;") + appendLine("\t--f7-safe-area-bottom: var(--window-inset-bottom, 0px) !important;") + appendLine("\t--f7-safe-area-left: var(--window-inset-left, 0px) !important;") + appendLine("\t--f7-safe-area-right: var(--window-inset-right, 0px) !important;") + append("}") + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt index 7650def..dad41e3 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt @@ -1,7 +1,6 @@ package com.sukisu.ultra.ui.webui import android.content.ServiceConnection -import android.content.pm.PackageInfo import android.util.Log import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.model.IProvider @@ -18,7 +17,7 @@ class KsuLibSuProvider : IProvider { override fun isAvailable() = true - override suspend fun isAuthorized() = Natives.becomeManager(ksuApp.packageName) + override suspend fun isAuthorized() = Natives.isManager private val serviceIntent get() = PlatformIntent( @@ -54,19 +53,4 @@ suspend fun initPlatform() = withContext(Dispatchers.IO) { Log.e("KsuLibSu", "Failed to initialize platform", e) return@withContext false } -} - -fun Platform.Companion.getInstalledPackagesAll(catch: (Exception) -> Unit = {}): List = - try { - val packages = mutableListOf() - val userInfos = userManager.getUsers() - - for (userInfo in userInfos) { - packages.addAll(packageManager.getInstalledPackages(0, userInfo.id)) - } - - packages - } catch (e: Exception) { - catch(e) - packageManager.getInstalledPackages(0, userManager.myUserId) - } \ No newline at end of file +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java deleted file mode 100644 index 5a80103..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.sukisu.ultra.ui.webui; - -import java.net.URLConnection; - -class MimeUtil { - - public static String getMimeFromFileName(String fileName) { - if (fileName == null) { - return null; - } - - // Copying the logic and mapping that Chromium follows. - // First we check against the OS (this is a limited list by default) - // but app developers can extend this. - // We then check against a list of hardcoded mime types above if the - // OS didn't provide a result. - String mimeType = URLConnection.guessContentTypeFromName(fileName); - - if (mimeType != null) { - return mimeType; - } - - return guessHardcodedMime(fileName); - } - - // We should keep this map in sync with the lists under - // //net/base/mime_util.cc in Chromium. - // A bunch of the mime types don't really apply to Android land - // like word docs so feel free to filter out where necessary. - private static String guessHardcodedMime(String fileName) { - int finalFullStop = fileName.lastIndexOf('.'); - if (finalFullStop == -1) { - return null; - } - - final String extension = fileName.substring(finalFullStop + 1).toLowerCase(); - - return switch (extension) { - case "webm" -> "video/webm"; - case "mpeg", "mpg" -> "video/mpeg"; - case "mp3" -> "audio/mpeg"; - case "wasm" -> "application/wasm"; - case "xhtml", "xht", "xhtm" -> "application/xhtml+xml"; - case "flac" -> "audio/flac"; - case "ogg", "oga", "opus" -> "audio/ogg"; - case "wav" -> "audio/wav"; - case "m4a" -> "audio/x-m4a"; - case "gif" -> "image/gif"; - case "jpeg", "jpg", "jfif", "pjpeg", "pjp" -> "image/jpeg"; - case "png" -> "image/png"; - case "apng" -> "image/apng"; - case "svg", "svgz" -> "image/svg+xml"; - case "webp" -> "image/webp"; - case "mht", "mhtml" -> "multipart/related"; - case "css" -> "text/css"; - case "html", "htm", "shtml", "shtm", "ehtml" -> "text/html"; - case "js", "mjs" -> "application/javascript"; - case "xml" -> "text/xml"; - case "mp4", "m4v" -> "video/mp4"; - case "ogv", "ogm" -> "video/ogg"; - case "ico" -> "image/x-icon"; - case "woff" -> "application/font-woff"; - case "gz", "tgz" -> "application/gzip"; - case "json" -> "application/json"; - case "pdf" -> "application/pdf"; - case "zip" -> "application/zip"; - case "bmp" -> "image/bmp"; - case "tiff", "tif" -> "image/tiff"; - default -> null; - }; - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.kt new file mode 100644 index 0000000..b9adcfa --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.sukisu.ultra.ui.webui + +import java.net.URLConnection + +internal object MimeUtil { + fun getMimeFromFileName(fileName: String?): String? { + if (fileName == null) { + return null + } + + val mimeType = URLConnection.guessContentTypeFromName(fileName) + if (mimeType != null) { + return mimeType + } + + return guessHardcodedMime(fileName) + } + + private fun guessHardcodedMime(fileName: String): String? { + val finalFullStop = fileName.lastIndexOf('.') + if (finalFullStop == -1) { + return null + } + + val extension = fileName.substring(finalFullStop + 1).lowercase() + + return when (extension) { + "webm" -> "video/webm" + "mpeg", "mpg" -> "video/mpeg" + "mp3" -> "audio/mpeg" + "wasm" -> "application/wasm" + "xhtml", "xht", "xhtm" -> "application/xhtml+xml" + "flac" -> "audio/flac" + "ogg", "oga", "opus" -> "audio/ogg" + "wav" -> "audio/wav" + "m4a" -> "audio/x-m4a" + "gif" -> "image/gif" + "jpeg", "jpg", "jfif", "pjpeg", "pjp" -> "image/jpeg" + "png" -> "image/png" + "apng" -> "image/apng" + "svg", "svgz" -> "image/svg+xml" + "webp" -> "image/webp" + "mht", "mhtml" -> "multipart/related" + "css" -> "text/css" + "html", "htm", "shtml", "shtm", "ehtml" -> "text/html" + "js", "mjs" -> "application/javascript" + "xml" -> "text/xml" + "mp4", "m4v" -> "video/mp4" + "ogv", "ogm" -> "video/ogg" + "ico" -> "image/x-icon" + "woff" -> "application/font-woff" + "gz", "tgz" -> "application/gzip" + "json" -> "application/json" + "pdf" -> "application/pdf" + "zip" -> "application/zip" + "bmp" -> "image/bmp" + "tiff", "tif" -> "image/tiff" + else -> null + } + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java deleted file mode 100644 index 263aa95..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java +++ /dev/null @@ -1,188 +0,0 @@ -package com.sukisu.ultra.ui.webui; - -import android.content.Context; -import android.util.Log; -import android.webkit.WebResourceResponse; -import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; -import androidx.webkit.WebViewAssetLoader; -import com.topjohnwu.superuser.Shell; -import com.topjohnwu.superuser.io.SuFile; -import com.topjohnwu.superuser.io.SuFileInputStream; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.GZIPInputStream; - -/** - * Handler class to open files from file system by root access - * For more information about android storage please refer to - * Android Developers - * Docs: Data and file storage overview. - *

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

- * A typical usage would be like: - *

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

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

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

- * The application should typically use a dedicated subdirectory for the files it intends to - * expose and keep them separate from other files. - * - * @param directory the absolute path of the exposed app internal storage directory from - * which files can be loaded. - * @throws IllegalArgumentException if the directory is not allowed. - */ - public SuFilePathHandler(@NonNull File directory, Shell rootShell) { - try { - mDirectory = new File(getCanonicalDirPath(directory)); - if (!isAllowedInternalStorageDir()) { - throw new IllegalArgumentException("The given directory \"" + directory - + "\" doesn't exist under an allowed app internal storage directory"); - } - mShell = rootShell; - } catch (IOException e) { - throw new IllegalArgumentException( - "Failed to resolve the canonical path for the given directory: " - + directory.getPath(), e); - } - } - - private boolean isAllowedInternalStorageDir() throws IOException { - String dir = getCanonicalDirPath(mDirectory); - - for (String forbiddenPath : FORBIDDEN_DATA_DIRS) { - if (dir.startsWith(forbiddenPath)) { - return false; - } - } - return true; - } - - /** - * Opens the requested file from the exposed data directory. - *

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

- * The MIME type for the file will be determined from the file's extension using - * {@link java.net.URLConnection#guessContentTypeFromName}. Developers should ensure that - * files are named using standard file extensions. If the file does not have a - * recognised extension, {@code "text/plain"} will be used by default. - * - * @param path the suffix path to be handled. - * @return {@link WebResourceResponse} for the requested file. - */ - @Override - @WorkerThread - @NonNull - public WebResourceResponse handle(@NonNull String path) { - try { - File file = getCanonicalFileIfChild(mDirectory, path); - if (file != null) { - InputStream is = openFile(file, mShell); - String mimeType = guessMimeType(path); - return new WebResourceResponse(mimeType, null, is); - } else { - Log.e(TAG, String.format( - "The requested file: %s is outside the mounted directory: %s", path, - mDirectory)); - } - } catch (IOException e) { - Log.e(TAG, "Error opening the requested path: " + path, e); - } - return new WebResourceResponse(null, null, null); - } - - public static String getCanonicalDirPath(@NonNull File file) throws IOException { - String canonicalPath = file.getCanonicalPath(); - if (!canonicalPath.endsWith("/")) canonicalPath += "/"; - return canonicalPath; - } - - public static File getCanonicalFileIfChild(@NonNull File parent, @NonNull String child) - throws IOException { - String parentCanonicalPath = getCanonicalDirPath(parent); - String childCanonicalPath = new File(parent, child).getCanonicalPath(); - if (childCanonicalPath.startsWith(parentCanonicalPath)) { - return new File(childCanonicalPath); - } - return null; - } - - @NonNull - private static InputStream handleSvgzStream(@NonNull String path, - @NonNull InputStream stream) throws IOException { - return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream; - } - - public static InputStream openFile(@NonNull File file, @NonNull Shell shell) throws IOException { - SuFile suFile = new SuFile(file.getAbsolutePath()); - suFile.setShell(shell); - InputStream fis = SuFileInputStream.open(suFile); - return handleSvgzStream(file.getPath(), fis); - } - - /** - * Use {@link MimeUtil#getMimeFromFileName} to guess MIME type or return the - * {@link #DEFAULT_MIME_TYPE} if it can't guess. - * - * @param filePath path of the file to guess its MIME type. - * @return MIME type guessed from file extension or {@link #DEFAULT_MIME_TYPE}. - */ - @NonNull - public static String guessMimeType(@NonNull String filePath) { - String mimeType = MimeUtil.getMimeFromFileName(filePath); - return mimeType == null ? DEFAULT_MIME_TYPE : mimeType; - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt new file mode 100644 index 0000000..c0f7930 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.kt @@ -0,0 +1,192 @@ +package com.sukisu.ultra.ui.webui + +import android.content.Context +import android.util.Log +import android.webkit.WebResourceResponse +import androidx.annotation.WorkerThread +import androidx.webkit.WebViewAssetLoader +import com.topjohnwu.superuser.Shell +import com.topjohnwu.superuser.io.SuFile +import com.topjohnwu.superuser.io.SuFileInputStream +import java.io.ByteArrayInputStream +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.nio.charset.StandardCharsets +import java.util.zip.GZIPInputStream + +/** + * Handler class to open files from file system by root access + * For more information about android storage please refer to + * [Android Developers Docs: Data and file storage overview](https://developer.android.com/guide/topics/data/data-storage). + * + * To avoid leaking user or app data to the web, make sure to choose [directory] + * carefully, and assume any file under this directory could be accessed by any web page subject + * to same-origin rules. + * + * A typical usage would be like: + * ``` + * val publicDir = File(context.filesDir, "public") + * // Host "files/public/" in app's data directory under: + * // http://appassets.androidplatform.net/public/... + * val assetLoader = WebViewAssetLoader.Builder() + * .addPathHandler("/public/", SuFilePathHandler(context, publicDir, shell, insetsSupplier)) + * .build() + * ``` + */ +class SuFilePathHandler( + directory: File, + private val shell: Shell, + private val insetsSupplier: InsetsSupplier +) : WebViewAssetLoader.PathHandler { + + private val directory: File + + init { + try { + this.directory = File(getCanonicalDirPath(directory)) + if (!isAllowedInternalStorageDir()) { + throw IllegalArgumentException( + "The given directory \"$directory\" doesn't exist under an allowed app internal storage directory" + ) + } + } catch (e: IOException) { + throw IllegalArgumentException( + "Failed to resolve the canonical path for the given directory: ${directory.path}", + e + ) + } + } + + fun interface InsetsSupplier { + fun get(): Insets + } + + private fun isAllowedInternalStorageDir(): Boolean { + return try { + val dir = getCanonicalDirPath(directory) + FORBIDDEN_DATA_DIRS.none { dir.startsWith(it) } + } catch (_: IOException) { + false + } + } + + /** + * Opens the requested file from the exposed data directory. + * + * The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the + * requested file cannot be found or is outside the mounted directory a + * [WebResourceResponse] object with a `null` [InputStream] will be + * returned instead of `null`. This saves the time of falling back to network and + * trying to resolve a path that doesn't exist. A [WebResourceResponse] with + * `null` [InputStream] will be received as an HTTP response with status code + * `404` and no body. + * + * The MIME type for the file will be determined from the file's extension using + * [java.net.URLConnection.guessContentTypeFromName]. Developers should ensure that + * files are named using standard file extensions. If the file does not have a + * recognised extension, `"text/plain"` will be used by default. + * + * @param path the suffix path to be handled. + * @return [WebResourceResponse] for the requested file. + */ + @WorkerThread + override fun handle(path: String): WebResourceResponse { + if (path == "internal/insets.css") { + val css = insetsSupplier.get().css + return WebResourceResponse( + "text/css", + "utf-8", + ByteArrayInputStream(css.toByteArray(StandardCharsets.UTF_8)) + ) + } + + try { + val file = getCanonicalFileIfChild(directory, path) + if (file != null) { + val inputStream = openFile(file, shell) + val mimeType = guessMimeType(path) + return WebResourceResponse(mimeType, null, inputStream) + } else { + Log.e( + TAG, + "The requested file: $path is outside the mounted directory: $directory" + ) + } + } catch (e: IOException) { + Log.e(TAG, "Error opening the requested path: $path", e) + } + + return WebResourceResponse(null, null, null) + } + + companion object { + private const val TAG = "SuFilePathHandler" + + /** + * Default value to be used as MIME type if guessing MIME type failed. + */ + const val DEFAULT_MIME_TYPE = "text/plain" + + /** + * Forbidden subdirectories of [Context.getDataDir] that cannot be exposed by this + * handler. They are forbidden as they often contain sensitive information. + * + * Note: Any future addition to this list will be considered breaking changes to the API. + */ + private val FORBIDDEN_DATA_DIRS = arrayOf("/data/data", "/data/system") + + @JvmStatic + @Throws(IOException::class) + fun getCanonicalDirPath(file: File): String { + var canonicalPath = file.canonicalPath + if (!canonicalPath.endsWith("/")) { + canonicalPath += "/" + } + return canonicalPath + } + + @JvmStatic + @Throws(IOException::class) + fun getCanonicalFileIfChild(parent: File, child: String): File? { + val parentCanonicalPath = getCanonicalDirPath(parent) + val childCanonicalPath = File(parent, child).canonicalPath + return if (childCanonicalPath.startsWith(parentCanonicalPath)) { + File(childCanonicalPath) + } else { + null + } + } + + @Throws(IOException::class) + private fun handleSvgzStream(path: String, stream: InputStream): InputStream { + return if (path.endsWith(".svgz")) { + GZIPInputStream(stream) + } else { + stream + } + } + + @JvmStatic + @Throws(IOException::class) + fun openFile(file: File, shell: Shell): InputStream { + val suFile = SuFile(file.absolutePath).apply { + setShell(shell) + } + val fis = SuFileInputStream.open(suFile) + return handleSvgzStream(file.path, fis) + } + + /** + * Use [MimeUtil.getMimeFromFileName] to guess MIME type or return the + * [DEFAULT_MIME_TYPE] if it can't guess. + * + * @param filePath path of the file to guess its MIME type. + * @return MIME type guessed from file extension or [DEFAULT_MIME_TYPE]. + */ + @JvmStatic + fun guessMimeType(filePath: String): String { + return MimeUtil.getMimeFromFileName(filePath) ?: DEFAULT_MIME_TYPE + } + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt index f1b7773..91ecd6c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt @@ -2,27 +2,38 @@ package com.sukisu.ultra.ui.webui import android.annotation.SuppressLint import android.app.ActivityManager +import android.graphics.Color import android.os.Build import android.os.Bundle -import android.view.ViewGroup.MarginLayoutParams import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope import androidx.webkit.WebViewAssetLoader import com.dergoogler.mmrl.platform.model.ModId import com.dergoogler.mmrl.webui.interfaces.WXOptions import com.sukisu.ultra.ui.util.createRootShell +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import java.io.File @SuppressLint("SetJavaScriptEnabled") class WebUIActivity : ComponentActivity() { private val rootShell by lazy { createRootShell(true) } + + private lateinit var insets: Insets private var webView = null as WebView? override fun onCreate(savedInstanceState: Bundle?) { @@ -35,6 +46,21 @@ class WebUIActivity : ComponentActivity() { super.onCreate(savedInstanceState) + setContent { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + lifecycleScope.launch { + SuperUserViewModel.isAppListLoaded.first { it } + setupWebView() + } + } + private fun setupWebView() { val moduleId = intent.getStringExtra("id") ?: finishAndRemoveTask().let { return } val name = intent.getStringExtra("name") ?: finishAndRemoveTask().let { return } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { @@ -51,11 +77,12 @@ class WebUIActivity : ComponentActivity() { val moduleDir = "/data/adb/modules/${moduleId}" val webRoot = File("${moduleDir}/webroot") + insets = Insets(0, 0, 0, 0) val webViewAssetLoader = WebViewAssetLoader.Builder() .setDomain("mui.kernelsu.org") .addPathHandler( "/", - SuFilePathHandler(webRoot, rootShell) + SuFilePathHandler(webRoot, rootShell) { insets } ) .build() @@ -64,22 +91,39 @@ class WebUIActivity : ComponentActivity() { view: WebView, request: WebResourceRequest ): WebResourceResponse? { - return webViewAssetLoader.shouldInterceptRequest(request.url) + val url = request.url + // Handle ksu://icon/[packageName] to serve app icon via WebView + if (url.scheme.equals("ksu", ignoreCase = true) && url.host.equals("icon", ignoreCase = true)) { + val packageName = url.path?.substring(1) + if (!packageName.isNullOrEmpty()) { + val icon = AppIconUtil.loadAppIconSync(this@WebUIActivity, packageName, 512) + if (icon != null) { + val stream = java.io.ByteArrayOutputStream() + icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, stream) + val inputStream = java.io.ByteArrayInputStream(stream.toByteArray()) + return WebResourceResponse("image/png", null, inputStream) + } + } + } + return webViewAssetLoader.shouldInterceptRequest(url) } } val webView = WebView(this).apply { webView = this - ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets -> - val inset = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - view.updateLayoutParams { - leftMargin = inset.left - rightMargin = inset.right - topMargin = inset.top - bottomMargin = inset.bottom - } - return@setOnApplyWindowInsetsListener insets + setBackgroundColor(Color.TRANSPARENT) + val density = resources.displayMetrics.density + + ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsets -> + val inset = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout()) + insets = Insets( + top = (inset.top / density).toInt(), + bottom = (inset.bottom / density).toInt(), + left = (inset.left / density).toInt(), + right = (inset.right / density).toInt() + ) + WindowInsetsCompat.CONSUMED } settings.javaScriptEnabled = true settings.domStorageEnabled = true diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt index 85781a9..0761274 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt @@ -13,6 +13,7 @@ import androidx.lifecycle.lifecycleScope import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.model.ModId import com.dergoogler.mmrl.ui.component.Loading +import com.dergoogler.mmrl.webui.model.WebUIConfig import com.dergoogler.mmrl.webui.screen.WebUIScreen import com.dergoogler.mmrl.webui.util.rememberWebUIOptions import com.sukisu.ultra.BuildConfig @@ -95,6 +96,13 @@ class WebUIXActivity : ComponentActivity() { userAgentString = userAgent ) + // idk why webuix not allow root impl change webuiConfig + // so we use magic to force exitConfirm shutdown + val field = WebUIConfig::class.java.getDeclaredField("exitConfirm") + field.isAccessible = true + field.set(options.config, false) + field.isAccessible = false + WebUIScreen( webView = webView, options = options, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt index 2335104..1e27104 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt @@ -1,17 +1,20 @@ package com.sukisu.ultra.ui.webui import android.app.Activity +import android.content.pm.ApplicationInfo import android.os.Handler import android.os.Looper import android.text.TextUtils import android.view.Window import android.webkit.JavascriptInterface import android.widget.Toast +import androidx.core.content.pm.PackageInfoCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.dergoogler.mmrl.webui.interfaces.WXInterface import com.dergoogler.mmrl.webui.interfaces.WXOptions import com.dergoogler.mmrl.webui.model.JavaScriptInterface +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import com.sukisu.ultra.ui.util.* import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.ShellUtils @@ -138,7 +141,7 @@ class WebViewInterface( completableFuture.thenAccept { result -> val emitExitCode = - "(function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();" + $$"(function() { try { $${callbackFunc}.emit('exit', $${result.code}); } catch(e) { console.error(`emitExit error: ${e}`); } })();" webView.post { webView.evaluateJavascript(emitExitCode, null) } @@ -203,6 +206,56 @@ class WebViewInterface( return currentModuleInfo.toString() } + @JavascriptInterface + fun listPackages(type: String): String { + val packageNames = SuperUserViewModel.apps + .filter { appInfo -> + val flags = appInfo.packageInfo.applicationInfo?.flags ?: 0 + when (type.lowercase()) { + "system" -> (flags and ApplicationInfo.FLAG_SYSTEM) != 0 + "user" -> (flags and ApplicationInfo.FLAG_SYSTEM) == 0 + else -> true + } + } + .map { it.packageName } + .sorted() + + val jsonArray = JSONArray() + for (pkgName in packageNames) { + jsonArray.put(pkgName) + } + return jsonArray.toString() + } + + @JavascriptInterface + fun getPackagesInfo(packageNamesJson: String): String { + val packageNames = JSONArray(packageNamesJson) + val jsonArray = JSONArray() + val appMap = SuperUserViewModel.apps.associateBy { it.packageName } + for (i in 0 until packageNames.length()) { + val pkgName = packageNames.getString(i) + val appInfo = appMap[pkgName] + if (appInfo != null) { + val pkg = appInfo.packageInfo + val app = pkg.applicationInfo + val obj = JSONObject() + obj.put("packageName", pkg.packageName) + obj.put("versionName", pkg.versionName ?: "") + obj.put("versionCode", PackageInfoCompat.getLongVersionCode(pkg)) + obj.put("appLabel", appInfo.label) + obj.put("isSystem", if (app != null) ((app.flags and ApplicationInfo.FLAG_SYSTEM) != 0) else JSONObject.NULL) + obj.put("uid", app?.uid ?: JSONObject.NULL) + jsonArray.put(obj) + } else { + val obj = JSONObject() + obj.put("packageName", pkgName) + obj.put("error", "Package not found or inaccessible") + jsonArray.put(obj) + } + } + return jsonArray.toString() + } + // =================== KPM支持 ============================= @JavascriptInterface diff --git a/manager/app/src/main/java/io/sukisu/ultra/UltraShellHelper.java b/manager/app/src/main/java/io/sukisu/ultra/UltraShellHelper.java deleted file mode 100644 index f829f61..0000000 --- a/manager/app/src/main/java/io/sukisu/ultra/UltraShellHelper.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.sukisu.ultra; - -import java.util.ArrayList; - -import com.sukisu.ultra.ui.util.KsuCli; - -public class UltraShellHelper { - public static String runCmd(String cmds) { - StringBuilder sb = new StringBuilder(); - for(String str : KsuCli.INSTANCE.getGLOBAL_MNT_SHELL() - .newJob() - .add(cmds) - .to(new ArrayList<>(), null) - .exec() - .getOut()) { - sb.append(str).append("\n"); - } - return sb.toString(); - } - - public static boolean isPathExists(String path) { - String result = runCmd("test -f '" + path + "' && echo 'exists'"); - return result.contains("exists"); - } - - public static void CopyFileTo(String path, String target) { - runCmd("cp -f '" + path + "' '" + target + "' 2>&1"); - } -} diff --git a/manager/app/src/main/java/io/sukisu/ultra/UltraToolInstall.java b/manager/app/src/main/java/io/sukisu/ultra/UltraToolInstall.java deleted file mode 100644 index 84fddda..0000000 --- a/manager/app/src/main/java/io/sukisu/ultra/UltraToolInstall.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.sukisu.ultra; - -import static com.sukisu.ultra.ui.util.KsuCliKt.*; -import android.annotation.SuppressLint; - -public class UltraToolInstall { - private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr"; - private static final String OUTSIDE_SUSFSD_PATH = "/data/adb/ksu/bin/susfsd"; - - @SuppressLint("SetWorldReadable") - public static void tryToInstall() { - String kpmmgrPath = getKpmmgrPath(); - if (UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) { - UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH); - UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH); - } - String SuSFSDaemonPath = getSuSFSDaemonPath(); - if (UltraShellHelper.isPathExists(OUTSIDE_SUSFSD_PATH)) { - UltraShellHelper.CopyFileTo(SuSFSDaemonPath, OUTSIDE_SUSFSD_PATH); - UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_SUSFSD_PATH); - } - } -} diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AnimatedBottomBar.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AnimatedBottomBar.kt deleted file mode 100644 index 3d364e4..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AnimatedBottomBar.kt +++ /dev/null @@ -1,20 +0,0 @@ -package zako.zako.zako.zakoui.activity.util - -import androidx.compose.animation.* -import androidx.compose.runtime.Composable - -object AnimatedBottomBar { - @Composable - fun AnimatedBottomBarWrapper( - showBottomBar: Boolean, - content: @Composable () -> Unit - ) { - AnimatedVisibility( - visible = showBottomBar, - enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), - exit = slideOutVertically(targetOffsetY = { it }) + fadeOut() - ) { - content() - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AppData.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AppData.kt deleted file mode 100644 index 50f5136..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AppData.kt +++ /dev/null @@ -1,90 +0,0 @@ -package zako.zako.zako.zakoui.activity.util - -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ui.util.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow - -object AppData { - object DataRefreshManager { - // 私有状态流 - private val _superuserCount = MutableStateFlow(0) - private val _moduleCount = MutableStateFlow(0) - private val _kpmModuleCount = MutableStateFlow(0) - - // 公开的只读状态流 - val superuserCount: StateFlow = _superuserCount.asStateFlow() - val moduleCount: StateFlow = _moduleCount.asStateFlow() - val kpmModuleCount: StateFlow = _kpmModuleCount.asStateFlow() - - /** - * 刷新所有数据计数 - */ - fun refreshData() { - _superuserCount.value = getSuperuserCountUse() - _moduleCount.value = getModuleCountUse() - _kpmModuleCount.value = getKpmModuleCountUse() - } - - } - - /** - * 获取超级用户应用计数 - */ - fun getSuperuserCountUse(): Int { - return try { - if (!rootAvailable()) return 0 - getSuperuserCount() - } catch (_: Exception) { - 0 - } - } - - /** - * 获取模块计数 - */ - fun getModuleCountUse(): Int { - return try { - if (!rootAvailable()) return 0 - getModuleCount() - } catch (_: Exception) { - 0 - } - } - - /** - * 获取KPM模块计数 - */ - fun getKpmModuleCountUse(): Int { - return try { - if (!rootAvailable()) return 0 - val kpmVersion = getKpmVersionUse() - if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) return 0 - getKpmModuleCount() - } catch (_: Exception) { - 0 - } - } - - /** - * 获取KPM版本 - */ - fun getKpmVersionUse(): String { - return try { - if (!rootAvailable()) return "" - val version = getKpmVersion() - version.ifEmpty { "" } - } catch (e: Exception) { - "Error: ${e.message}" - } - } - - /** - * 检查是否是完整功能模式 - */ - fun isFullFeatured(packageName: String): Boolean { - val isManager = Natives.becomeManager(packageName) - return isManager && !Natives.requireNewKernel() && rootAvailable() - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DataRefreshUtils.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DataRefreshUtils.kt deleted file mode 100644 index e09b194..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DataRefreshUtils.kt +++ /dev/null @@ -1,46 +0,0 @@ -package zako.zako.zako.zakoui.activity.util - -import android.content.Context -import androidx.lifecycle.LifecycleCoroutineScope -import com.sukisu.ultra.ui.MainActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager - -object DataRefreshUtils { - - fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) { - scope.launch(Dispatchers.IO) { - while (isActive) { - DataRefreshManager.refreshData() - delay(5000) - } - } - } - - fun startSettingsMonitorCoroutine( - scope: LifecycleCoroutineScope, - activity: MainActivity, - settingsStateFlow: MutableStateFlow - ) { - scope.launch(Dispatchers.IO) { - while (isActive) { - val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE) - settingsStateFlow.value = MainActivity.SettingsState( - isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false), - showKpmInfo = prefs.getBoolean("show_kpm_info", false) - ) - delay(1000) - } - } - } - - fun refreshData(scope: LifecycleCoroutineScope) { - scope.launch { - DataRefreshManager.refreshData() - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DisplayUtils.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DisplayUtils.kt deleted file mode 100644 index bd6c128..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DisplayUtils.kt +++ /dev/null @@ -1,24 +0,0 @@ -package zako.zako.zako.zakoui.activity.util - -import android.content.Context - -object DisplayUtils { - - fun applyCustomDpi(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val customDpi = prefs.getInt("app_dpi", 0) - - if (customDpi > 0) { - try { - val resources = context.resources - val metrics = resources.displayMetrics - metrics.density = customDpi / 160f - @Suppress("DEPRECATION") - metrics.scaledDensity = customDpi / 160f - metrics.densityDpi = customDpi - } catch (e: Exception) { - e.printStackTrace() - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/LocaleUtils.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/LocaleUtils.kt deleted file mode 100644 index 50debbc..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/LocaleUtils.kt +++ /dev/null @@ -1,48 +0,0 @@ -package zako.zako.zako.zakoui.activity.util - -import android.annotation.SuppressLint -import android.content.Context -import android.content.res.Configuration -import android.os.Build -import java.util.* - -object LocaleUtils { - - @SuppressLint("ObsoleteSdkInt") - fun applyLanguageSetting(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val languageCode = prefs.getString("app_language", "") ?: "" - - if (languageCode.isNotEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - Locale.setDefault(locale) - - val resources = context.resources - val config = Configuration(resources.configuration) - config.setLocale(locale) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - context.createConfigurationContext(config) - } else { - @Suppress("DEPRECATION") - resources.updateConfiguration(config, resources.displayMetrics) - } - } - } - - fun applyLocale(context: Context): Context { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val languageCode = prefs.getString("app_language", "") ?: "" - - var newContext = context - if (languageCode.isNotEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - Locale.setDefault(locale) - - val config = Configuration(context.resources.configuration) - config.setLocale(locale) - newContext = context.createConfigurationContext(config) - } - - return newContext - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt deleted file mode 100644 index 3dbd4f2..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt +++ /dev/null @@ -1,1711 +0,0 @@ -package zako.zako.zako.zakoui.screen - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.content.res.Configuration -import android.net.Uri -import android.os.Build -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.* -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.NavigateNext -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.content.edit -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ksuApp -import com.sukisu.ultra.ui.component.ImageEditorDialog -import com.sukisu.ultra.ui.component.KsuIsValid -import com.sukisu.ultra.ui.theme.* -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation -import com.sukisu.ultra.ui.util.* -import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.util.* -import kotlin.math.roundToInt - -/** - * @author ShirkNeko - * @date 2025/5/31. - */ -private val SETTINGS_ITEM_HEIGHT = 56.dp -private val SETTINGS_GROUP_SPACING = 16.dp - -/** - * 保存卡片配置 - */ -fun saveCardConfig(context: Context) { - CardConfig.save(context) -} - -/** - * 更多设置屏幕 - */ -@SuppressLint("LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt") -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun MoreSettingsScreen( - navigator: DestinationsNavigator -) { - // 顶部滚动行为 - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } - val systemIsDark = isSystemInDarkTheme() - - - // 主题模式选择 - var themeMode by remember { - mutableIntStateOf( - when(ThemeConfig.forceDarkMode) { - true -> 2 // 深色 - false -> 1 // 浅色 - null -> 0 // 跟随系统 - } - ) - } - - // 动态颜色开关状态 - var useDynamicColor by remember { - mutableStateOf(ThemeConfig.useDynamicColor) - } - - // 对话框显示状态 - var showThemeModeDialog by remember { mutableStateOf(false) } - var showLanguageDialog by remember { mutableStateOf(false) } - var showThemeColorDialog by remember { mutableStateOf(false) } - var showDpiConfirmDialog by remember { mutableStateOf(false) } - var showImageEditor by remember { mutableStateOf(false) } - - // 动态管理器配置状态 - var dynamicSignConfig by remember { mutableStateOf(null) } - var isDynamicSignEnabled by remember { mutableStateOf(false) } - var dynamicSignSize by remember { mutableStateOf("") } - var dynamicSignHash by remember { mutableStateOf("") } - var showDynamicSignDialog by remember { mutableStateOf(false) } - - // 主题模式选项 - val themeOptions = listOf( - stringResource(R.string.theme_follow_system), - stringResource(R.string.theme_light), - stringResource(R.string.theme_dark) - ) - - // 获取当前语言设置 - var currentLanguage by remember { - mutableStateOf(prefs.getString("app_language", "") ?: "") - } - - // 获取支持的语言列表 - val supportedLanguages = remember { - val languages = mutableListOf>() - languages.add("" to context.getString(R.string.language_follow_system)) - val locales = context.resources.configuration.locales - for (i in 0 until locales.size()) { - val locale = locales.get(i) - val code = locale.toLanguageTag() - if (!languages.any { it.first == code }) { - languages.add(code to locale.getDisplayName(locale)) - } - } - - val commonLocales = listOf( - Locale.forLanguageTag("en"), // 英语 - Locale.forLanguageTag("zh-CN"), // 简体中文 - Locale.forLanguageTag("zh-HK"), // 繁体中文(香港) - Locale.forLanguageTag("zh-TW"), // 繁体中文(台湾) - Locale.forLanguageTag("ja"), // 日语 - Locale.forLanguageTag("fr"), // 法语 - Locale.forLanguageTag("de"), // 德语 - Locale.forLanguageTag("es"), // 西班牙语 - Locale.forLanguageTag("it"), // 意大利语 - Locale.forLanguageTag("ru"), // 俄语 - Locale.forLanguageTag("pt"), // 葡萄牙语 - Locale.forLanguageTag("ko"), // 韩语 - Locale.forLanguageTag("vi") // 越南语 - ) - - for (locale in commonLocales) { - val code = locale.toLanguageTag() - if (!languages.any { it.first == code }) { - val config = Configuration(context.resources.configuration) - config.setLocale(locale) - try { - val testContext = context.createConfigurationContext(config) - testContext.getString(R.string.language_follow_system) - languages.add(code to locale.getDisplayName(locale)) - } catch (_: Exception) { - } - } - } - languages - } - - // 简洁模式开关状态 - var isSimpleMode by remember { - mutableStateOf(prefs.getBoolean("is_simple_mode", false)) - } - - // 隐藏内核版本号开关状态 - var isHideVersion by remember { - mutableStateOf(prefs.getBoolean("is_hide_version", false)) - } - - // 隐藏模块数量等信息开关状态 - var isHideOtherInfo by remember { - mutableStateOf(prefs.getBoolean("is_hide_other_info", false)) - } - - // 显示KPM开关状态 - var isShowKpmInfo by remember { - mutableStateOf(prefs.getBoolean("show_kpm_info", false)) - } - - // 隐藏 Zygisk 状态开关状态 - var isHideZygiskImplement by remember { - mutableStateOf(prefs.getBoolean("is_hide_zygisk_Implement", false)) - } - - // 隐藏SuSFS状态开关状态 - var isHideSusfsStatus by remember { - mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false)) - } - - // 隐藏链接状态开关状态 - var isHideLinkCard by remember { - mutableStateOf(prefs.getBoolean("is_hide_link_card", false)) - } - - // 隐藏标签行开关状态 - var isHideTagRow by remember { - mutableStateOf(prefs.getBoolean("is_hide_tag_row", false)) - } - - // 内核版本简洁模式开关状态 - var isKernelSimpleMode by remember { - mutableStateOf(prefs.getBoolean("is_kernel_simple_mode", false)) - } - - // 显示更多模块信息开关状态 - var showMoreModuleInfo by remember { - mutableStateOf(prefs.getBoolean("show_more_module_info", false)) - } - - // SELinux状态 - var selinuxEnabled by remember { - mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing") - } - - // 卡片配置状态 - var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) } - var cardDim by rememberSaveable { mutableFloatStateOf(CardConfig.cardDim) } - var isCustomBackgroundEnabled by rememberSaveable { - mutableStateOf(ThemeConfig.customBackgroundUri != null) - } - - // 备用图标状态 - var useAltIcon by remember { mutableStateOf(prefs.getBoolean("use_alt_icon", false)) } - - // 图片选择状态 - var selectedImageUri by remember { mutableStateOf(null) } - - // DPI 设置 - val systemDpi = remember { context.resources.displayMetrics.densityDpi } - var currentDpi by remember { - mutableIntStateOf(prefs.getInt("app_dpi", systemDpi)) - } - var tempDpi by remember { mutableIntStateOf(currentDpi) } - var isDpiCustom by remember { mutableStateOf(true) } - - // 预设 DPI 选项 - val dpiPresets = mapOf( - stringResource(R.string.dpi_size_small) to 240, - stringResource(R.string.dpi_size_medium) to 320, - stringResource(R.string.dpi_size_large) to 420, - stringResource(R.string.dpi_size_extra_large) to 560 - ) - - // 主题色选项 - val themeColorOptions = listOf( - stringResource(R.string.color_default) to ThemeColors.Default, - stringResource(R.string.color_green) to ThemeColors.Green, - stringResource(R.string.color_purple) to ThemeColors.Purple, - stringResource(R.string.color_orange) to ThemeColors.Orange, - stringResource(R.string.color_pink) to ThemeColors.Pink, - stringResource(R.string.color_gray) to ThemeColors.Gray, - stringResource(R.string.color_yellow) to ThemeColors.Yellow - ) - - - // 更新简洁模式开关状态 - val onSimpleModeChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_simple_mode", newValue) } - isSimpleMode = newValue - } - - // 内核版本简洁模式开关状态 - val onKernelSimpleModeChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_kernel_simple_mode", newValue) } - isKernelSimpleMode = newValue - } - - // 隐藏内核版本号开关状态 - val onHideVersionChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_version", newValue) } - isHideVersion = newValue - } - - // 隐藏模块数量等信息开关状态 - val onHideOtherInfoChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_other_info", newValue) } - isHideOtherInfo = newValue - } - - // 更新显示KPM开关状态 - val onShowKpmInfoChange = { newValue: Boolean -> - prefs.edit { putBoolean("show_kpm_info", newValue) } - isShowKpmInfo = newValue - } - - // 隐藏SuSFS状态开关状态 - val onHideSusfsStatusChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_susfs_status", newValue) } - isHideSusfsStatus = newValue - } - - val onHideZygiskImplement = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_zygisk_Implement", newValue) } - isHideZygiskImplement = newValue - - } - - // 隐藏链接状态开关状态 - val onHideLinkCardChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_link_card", newValue) } - isHideLinkCard = newValue - } - - // 隐藏标签行开关状态 - val onHideTagRowChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_tag_row", newValue) } - isHideTagRow = newValue - } - - // 显示更多模块信息开关状态 - val onShowMoreModuleInfoChange = { newValue: Boolean -> - prefs.edit { putBoolean("show_more_module_info", newValue) } - showMoreModuleInfo = newValue - } - - // 备用图标开关状态 - val onUseAltIconChange = { newValue: Boolean -> - prefs.edit { putBoolean("use_alt_icon", newValue) } - useAltIcon = newValue - toggleLauncherIcon(context, newValue) - Toast.makeText(context, context.getString(R.string.icon_switched), Toast.LENGTH_SHORT).show() - } - - - // 获取DPI大小友好名称 - @Composable - fun getDpiFriendlyName(dpi: Int): String { - return when (dpi) { - 240 -> stringResource(R.string.dpi_size_small) - 320 -> stringResource(R.string.dpi_size_medium) - 420 -> stringResource(R.string.dpi_size_large) - 560 -> stringResource(R.string.dpi_size_extra_large) - else -> stringResource(R.string.dpi_size_custom) - } - } - - // 应用 DPI 设置 - val applyDpiSetting = { dpi: Int -> - if (dpi != currentDpi) { - // 保存到 SharedPreferences - prefs.edit { - putInt("app_dpi", dpi) - } - - // 只修改应用级别的DPI设置 - currentDpi = dpi - tempDpi = dpi - Toast.makeText( - context, - context.getString(R.string.dpi_applied_success, dpi), - Toast.LENGTH_SHORT - ).show() - - val restartIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) - restartIntent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(restartIntent) - - showDpiConfirmDialog = false - } - } - - // 应用语言设置 - val applyLanguageSetting = { code: String -> - if (currentLanguage != code) { - prefs.edit { - putString("app_language", code) - commit() - } - - currentLanguage = code - - Toast.makeText( - context, - context.getString(R.string.language_changed), - Toast.LENGTH_SHORT - ).show() - - val locale = if (code.isEmpty()) Locale.getDefault() else Locale.forLanguageTag(code) - Locale.setDefault(locale) - val config = Configuration(context.resources.configuration) - config.setLocale(locale) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - context.createConfigurationContext(config) - } else { - @Suppress("DEPRECATION") - context.resources.updateConfiguration(config, context.resources.displayMetrics) - } - ksuApp.refreshCurrentActivity() - } - } - - // ========== 初始化 ========== - - // 初始化卡片配置 - LaunchedEffect(Unit) { - // 加载设置 - CardConfig.load(context) - cardAlpha = CardConfig.cardAlpha - cardDim = CardConfig.cardDim - isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null - - // 设置主题模式 - themeMode = when (ThemeConfig.forceDarkMode) { - true -> 2 - false -> 1 - null -> 0 - } - - // 确保卡片样式跟随主题模式 - when (themeMode) { - 2 -> { // 深色 - CardConfig.isUserDarkModeEnabled = true - CardConfig.isUserLightModeEnabled = false - } - 1 -> { // 浅色 - CardConfig.isUserDarkModeEnabled = false - CardConfig.isUserLightModeEnabled = true - } - 0 -> { // 跟随系统 - CardConfig.isUserDarkModeEnabled = false - CardConfig.isUserLightModeEnabled = false - } - } - - // 如果启用了系统跟随且系统是深色模式,应用深色模式默认值 - if (themeMode == 0 && systemIsDark) { - CardConfig.setThemeDefaults(true) - } - - currentDpi = prefs.getInt("app_dpi", systemDpi) - tempDpi = currentDpi - - CardConfig.save(context) - } - - // 图片选择器 - val pickImageLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.GetContent() - ) { uri: Uri? -> - uri?.let { - selectedImageUri = it - showImageEditor = true - } - } - - // ========== UI 构建 ========== - - // 显示图片编辑对话框 - if (showImageEditor && selectedImageUri != null) { - ImageEditorDialog( - imageUri = selectedImageUri!!, - onDismiss = { - showImageEditor = false - selectedImageUri = null - }, - onConfirm = { transformedUri -> - context.saveAndApplyCustomBackground(transformedUri) - isCustomBackgroundEnabled = true - cardElevation = 0.dp - CardConfig.isCustomBackgroundEnabled = true - saveCardConfig(context) - showImageEditor = false - selectedImageUri = null - - // 显示成功提示 - Toast.makeText( - context, - context.getString(R.string.background_set_success), - Toast.LENGTH_SHORT - ).show() - } - ) - } - - // 主题模式选择对话框 - if (showThemeModeDialog) { - SingleChoiceDialog( - title = stringResource(R.string.theme_mode), - options = themeOptions, - selectedIndex = themeMode, - onOptionSelected = { index -> - themeMode = index - val newThemeMode = when(index) { - 0 -> null // 跟随系统 - 1 -> false // 浅色 - 2 -> true // 深色 - else -> null - } - context.saveThemeMode(newThemeMode) - when (index) { - 2 -> { // 深色 - ThemeConfig.forceDarkMode = true - CardConfig.isUserDarkModeEnabled = true - CardConfig.isUserLightModeEnabled = false - CardConfig.setThemeDefaults(true) - CardConfig.save(context) - } - 1 -> { // 浅色 - ThemeConfig.forceDarkMode = false - CardConfig.isUserLightModeEnabled = true - CardConfig.isUserDarkModeEnabled = false - CardConfig.setThemeDefaults(false) - CardConfig.save(context) - } - 0 -> { // 跟随系统 - ThemeConfig.forceDarkMode = null - CardConfig.isUserLightModeEnabled = false - CardConfig.isUserDarkModeEnabled = false - val isNightModeActive = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES - CardConfig.setThemeDefaults(isNightModeActive) - CardConfig.save(context) - } - } - }, - onDismiss = { showThemeModeDialog = false } - ) - } - - // 语言切换对话框 - if (showLanguageDialog) { - KeyValueChoiceDialog( - title = stringResource(R.string.language_setting), - options = supportedLanguages, - selectedCode = currentLanguage, - onOptionSelected = { code -> - applyLanguageSetting(code) - }, - onDismiss = { showLanguageDialog = false } - ) - } - - // DPI 设置确认对话框 - if (showDpiConfirmDialog) { - ConfirmDialog( - title = stringResource(R.string.dpi_confirm_title), - message = stringResource(R.string.dpi_confirm_message, currentDpi, tempDpi), - summaryText = stringResource(R.string.dpi_confirm_summary), - confirmText = stringResource(R.string.confirm), - dismissText = stringResource(R.string.cancel), - onConfirm = { applyDpiSetting(tempDpi) }, - onDismiss = { - showDpiConfirmDialog = false - tempDpi = currentDpi - } - ) - } - - // 主题色选择对话框 - if (showThemeColorDialog) { - AlertDialog( - onDismissRequest = { showThemeColorDialog = false }, - title = { Text(stringResource(R.string.choose_theme_color)) }, - text = { - Column { - themeColorOptions.forEach { (name, theme) -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - context.saveThemeColors(when (theme) { - ThemeColors.Green -> "green" - ThemeColors.Purple -> "purple" - ThemeColors.Orange -> "orange" - ThemeColors.Pink -> "pink" - ThemeColors.Gray -> "gray" - ThemeColors.Yellow -> "yellow" - else -> "default" - }) - showThemeColorDialog = false - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val isDark = isSystemInDarkTheme() - Box( - modifier = Modifier.padding(end = 12.dp) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - ColorCircle( - color = if (isDark) theme.primaryDark else theme.primaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - ColorCircle( - color = if (isDark) theme.secondaryDark else theme.secondaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - ColorCircle( - color = if (isDark) theme.tertiaryDark else theme.tertiaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - } - } - Text(name) - Spacer(modifier = Modifier.weight(1f)) - // 当前选中的主题显示选中标记 - if (ThemeConfig.currentTheme::class == theme::class) { - Icon( - Icons.Default.Check, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - } - } - } - } - }, - confirmButton = { - Button( - onClick = { showThemeColorDialog = false } - ) { - Text(stringResource(R.string.cancel)) - } - } - ) - } - - LaunchedEffect(Unit) { - // 初始化动态管理器配置 - dynamicSignConfig = Natives.getDynamicManager() - dynamicSignConfig?.let { config -> - if (config.isValid()) { - isDynamicSignEnabled = true - dynamicSignSize = config.size.toString() - dynamicSignHash = config.hash - } - } - } - - fun parseDynamicSignSize(input: String): Int? { - return try { - when { - input.startsWith("0x", true) -> input.substring(2).toInt(16) - else -> input.toInt() - } - } catch (_: NumberFormatException) { - null - } - } - - // 动态管理器配置对话框 - if (showDynamicSignDialog) { - AlertDialog( - onDismissRequest = { showDynamicSignDialog = false }, - title = { Text(stringResource(R.string.dynamic_manager_title)) }, - text = { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - // 启用开关 - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { isDynamicSignEnabled = !isDynamicSignEnabled } - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Switch( - checked = isDynamicSignEnabled, - onCheckedChange = { isDynamicSignEnabled = it } - ) - Spacer(modifier = Modifier.width(12.dp)) - Text(stringResource(R.string.enable_dynamic_manager)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // 签名大小输入 - OutlinedTextField( - value = dynamicSignSize, - onValueChange = { input -> - val isValid = when { - input.isEmpty() -> true - input.matches(Regex("^\\d+$")) -> true - input.matches(Regex("^0[xX][0-9a-fA-F]*$")) -> true - else -> false - } - if (isValid) { - dynamicSignSize = input - } - }, - label = { Text(stringResource(R.string.signature_size)) }, - enabled = isDynamicSignEnabled, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text - ) - ) - - Spacer(modifier = Modifier.height(12.dp)) - - // 签名哈希输入 - OutlinedTextField( - value = dynamicSignHash, - onValueChange = { hash -> - if (hash.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) { - dynamicSignHash = hash - } - }, - label = { Text(stringResource(R.string.signature_hash)) }, - enabled = isDynamicSignEnabled, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - supportingText = { - Text(stringResource(R.string.hash_must_be_64_chars)) - }, - isError = isDynamicSignEnabled && dynamicSignHash.isNotEmpty() && dynamicSignHash.length != 64 - ) - } - }, - confirmButton = { - Button( - onClick = { - if (isDynamicSignEnabled) { - val size = parseDynamicSignSize(dynamicSignSize) - if (size != null && size > 0 && dynamicSignHash.length == 64) { - val success = Natives.setDynamicManager(size, dynamicSignHash) - if (success) { - dynamicSignConfig = Natives.DynamicManagerConfig(size, dynamicSignHash) - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_set_success), - Toast.LENGTH_SHORT - ).show() - } else { - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_set_failed), - Toast.LENGTH_SHORT - ).show() - } - } else { - Toast.makeText( - context, - context.getString(R.string.invalid_sign_config), - Toast.LENGTH_SHORT - ).show() - return@Button - } - } else { - val success = Natives.clearDynamicManager() - if (success) { - dynamicSignConfig = null - dynamicSignSize = "" - dynamicSignHash = "" - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_disabled_success), - Toast.LENGTH_SHORT - ).show() - } else { - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_clear_failed), - Toast.LENGTH_SHORT - ).show() - return@Button - } - } - showDynamicSignDialog = false - }, - enabled = if (isDynamicSignEnabled) { - parseDynamicSignSize(dynamicSignSize)?.let { it > 0 } == true && - dynamicSignHash.length == 64 - } else true - ) { - Text(stringResource(R.string.confirm)) - } - }, - dismissButton = { - TextButton(onClick = { showDynamicSignDialog = false }) { - Text(stringResource(R.string.cancel)) - } - } - ) - } - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopAppBar( - title = { - Text( - text = stringResource(R.string.more_settings), - style = MaterialTheme.typography.titleLarge - ) - }, - navigationIcon = { - IconButton(onClick = { - navigator.popBackStack() - }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back) - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha), - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha) - ), - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) - }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp) - .padding(top = 8.dp) - ) { - // ========== 外观设置部分 ========== - SettingsCard( - title = stringResource(R.string.appearance_settings), - ) { - // 语言设置 - SettingItem( - icon = Icons.Default.Language, - title = stringResource(R.string.language_setting), - subtitle = supportedLanguages.find { it.first == currentLanguage }?.second - ?: stringResource(R.string.language_follow_system), - onClick = { showLanguageDialog = true } - ) - - // 主题模式 - SettingItem( - icon = Icons.Default.DarkMode, - title = stringResource(R.string.theme_mode), - subtitle = themeOptions[themeMode], - onClick = { showThemeModeDialog = true } - ) - - // 动态颜色开关 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - SwitchSettingItem( - icon = Icons.Filled.ColorLens, - title = stringResource(R.string.dynamic_color_title), - summary = stringResource(R.string.dynamic_color_summary), - checked = useDynamicColor, - onChange = { enabled -> - useDynamicColor = enabled - context.saveDynamicColorState(enabled) - } - ) - } - - // 只在未启用动态颜色时显示主题色选择 - AnimatedVisibility( - visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !useDynamicColor, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - SettingItem( - icon = Icons.Default.Palette, - title = stringResource(R.string.theme_color), - subtitle = when (ThemeConfig.currentTheme) { - is ThemeColors.Green -> stringResource(R.string.color_green) - is ThemeColors.Purple -> stringResource(R.string.color_purple) - is ThemeColors.Orange -> stringResource(R.string.color_orange) - is ThemeColors.Pink -> stringResource(R.string.color_pink) - is ThemeColors.Gray -> stringResource(R.string.color_gray) - is ThemeColors.Yellow -> stringResource(R.string.color_yellow) - else -> stringResource(R.string.color_default) - }, - onClick = { showThemeColorDialog = true }, - trailingContent = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = 8.dp) - ) { - // 显示当前主题的三种主题色调 - val theme = ThemeConfig.currentTheme - val isDark = isSystemInDarkTheme() - - // Primary color - ColorCircle( - color = if (isDark) theme.primaryDark else theme.primaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - - // Secondary color - ColorCircle( - color = if (isDark) theme.secondaryDark else theme.secondaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - - // Tertiary color - ColorCircle( - color = if (isDark) theme.tertiaryDark else theme.tertiaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - } - } - ) - } - - SettingsDivider() - - // DPI 设置 - SettingItem( - icon = Icons.Default.FormatSize, - title = stringResource(R.string.app_dpi_title), - subtitle = stringResource(R.string.app_dpi_summary), - onClick = {}, - trailingContent = { - Text( - text = getDpiFriendlyName(tempDpi), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - } - ) - - // DPI 滑动条 - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - val sliderValue by animateFloatAsState( - targetValue = tempDpi.toFloat(), - label = "DPI Slider Animation" - ) - - Slider( - value = sliderValue, - onValueChange = { - tempDpi = it.toInt() - isDpiCustom = !dpiPresets.containsValue(tempDpi) - }, - valueRange = 160f..600f, - steps = 11, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - ) { - dpiPresets.forEach { (name, dpi) -> - val isSelected = tempDpi == dpi - val buttonColor = if (isSelected) - MaterialTheme.colorScheme.primaryContainer - else - MaterialTheme.colorScheme.surfaceVariant - - Box( - modifier = Modifier - .weight(1f) - .padding(horizontal = 2.dp) - .clip(RoundedCornerShape(8.dp)) - .background(buttonColor) - .clickable { - tempDpi = dpi - isDpiCustom = false - } - .padding(vertical = 8.dp, horizontal = 4.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = name, - style = MaterialTheme.typography.labelMedium, - color = if (isSelected) - MaterialTheme.colorScheme.onPrimaryContainer - else - MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } - - Text( - text = if (isDpiCustom) - "${stringResource(R.string.dpi_size_custom)}: $tempDpi" - else - "${getDpiFriendlyName(tempDpi)}: $tempDpi", - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(top = 8.dp) - ) - - Button( - onClick = { - if (tempDpi != currentDpi) { - showDpiConfirmDialog = true - } - }, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - enabled = tempDpi != currentDpi - ) { - Icon( - Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.dpi_apply_settings)) - } - } - - SettingsDivider() - - // 自定义背景开关 - SwitchSettingItem( - icon = Icons.Filled.Wallpaper, - title = stringResource(id = R.string.settings_custom_background), - summary = stringResource(id = R.string.settings_custom_background_summary), - checked = isCustomBackgroundEnabled, - onChange = { isChecked -> - if (isChecked) { - pickImageLauncher.launch("image/*") - } else { - context.saveCustomBackground(null) - isCustomBackgroundEnabled = false - CardConfig.cardAlpha = 1f - CardConfig.cardDim = 0f - CardConfig.isCustomAlphaSet = false - CardConfig.isCustomDimSet = false - CardConfig.isCustomBackgroundEnabled = false - saveCardConfig(context) - - // 重置其他相关设置 - ThemeConfig.needsResetOnThemeChange = true - ThemeConfig.preventBackgroundRefresh = false - - context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putBoolean( - "prevent_background_refresh", - false - ) - } - - Toast.makeText( - context, - context.getString(R.string.background_removed), - Toast.LENGTH_SHORT - ).show() - } - } - ) - - // 透明度和亮度调节滑动条 - AnimatedVisibility( - visible = ThemeConfig.customBackgroundUri != null, - enter = fadeIn() + slideInVertically(), - exit = fadeOut() + slideOutVertically() - ) { - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - // 透明度滑动条 - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 4.dp) - ) { - Icon( - Icons.Filled.Opacity, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.settings_card_alpha), - style = MaterialTheme.typography.titleSmall - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "${(cardAlpha * 100).roundToInt()}%", - style = MaterialTheme.typography.labelMedium, - ) - } - - val alphaSliderValue by animateFloatAsState( - targetValue = cardAlpha, - label = "Alpha Slider Animation" - ) - - Slider( - value = alphaSliderValue, - onValueChange = { newValue -> - cardAlpha = newValue - CardConfig.cardAlpha = newValue - CardConfig.isCustomAlphaSet = true - prefs.edit { - putBoolean("is_custom_alpha_set", true) - putFloat("card_alpha", newValue) - } - }, - onValueChangeFinished = { - coroutineScope.launch(Dispatchers.IO) { - saveCardConfig(context) - } - }, - valueRange = 0f..1f, - steps = 20, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - - // 亮度调节滑动条 - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 16.dp, bottom = 4.dp) - ) { - Icon( - Icons.Filled.LightMode, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.settings_card_dim), - style = MaterialTheme.typography.titleSmall - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "${(cardDim * 100).roundToInt()}%", - style = MaterialTheme.typography.labelMedium, - ) - } - - val dimSliderValue by animateFloatAsState( - targetValue = cardDim, - label = "Dim Slider Animation" - ) - - Slider( - value = dimSliderValue, - onValueChange = { newValue -> - cardDim = newValue - CardConfig.cardDim = newValue - CardConfig.isCustomDimSet = true - prefs.edit { - putBoolean("is_custom_dim_set", true) - putFloat("card_dim", newValue) - } - }, - onValueChangeFinished = { - coroutineScope.launch(Dispatchers.IO) { - saveCardConfig(context) - } - }, - valueRange = 0f..1f, - steps = 20, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - } - } - } - - // 自定义设置 - SettingsCard( - title = stringResource(R.string.custom_settings) - ) { - // 图标切换 - SwitchSettingItem( - icon = Icons.Default.Android, - title = stringResource(R.string.icon_switch_title), - summary = stringResource(R.string.icon_switch_summary), - checked = useAltIcon, - onChange = onUseAltIconChange - ) - - // 显示更多模块信息开关 - SwitchSettingItem( - icon = Icons.Filled.Info, - title = stringResource(R.string.show_more_module_info), - summary = stringResource(R.string.show_more_module_info_summary), - checked = showMoreModuleInfo, - onChange = onShowMoreModuleInfoChange - ) - - // 添加简洁模式开关 - SwitchSettingItem( - icon = Icons.Filled.Brush, - title = stringResource(R.string.simple_mode), - summary = stringResource(R.string.simple_mode_summary), - checked = isSimpleMode, - onChange = onSimpleModeChange - ) - - SwitchSettingItem( - icon = Icons.Filled.Brush, - title = stringResource(R.string.kernel_simple_kernel), - summary = stringResource(R.string.kernel_simple_kernel_summary), - checked = isKernelSimpleMode, - onChange = onKernelSimpleModeChange - ) - - // 隐藏内核部分版本号 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_kernel_kernelsu_version), - summary = stringResource(R.string.hide_kernel_kernelsu_version_summary), - checked = isHideVersion, - onChange = onHideVersionChange - ) - - // 模块数量等信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_other_info), - summary = stringResource(R.string.hide_other_info_summary), - checked = isHideOtherInfo, - onChange = onHideOtherInfoChange - ) - - // SuSFS 状态信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_susfs_status), - summary = stringResource(R.string.hide_susfs_status_summary), - checked = isHideSusfsStatus, - onChange = onHideSusfsStatusChange - ) - - // Zygsik 实现状态信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_zygisk_implement), - summary = stringResource(R.string.hide_zygisk_implement_summary), - checked = isHideZygiskImplement, - onChange = onHideZygiskImplement - ) - - if (Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) { - // 隐藏KPM开关 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.show_kpm_info), - summary = stringResource(R.string.show_kpm_info_summary), - checked = isShowKpmInfo, - onChange = onShowKpmInfoChange - ) - } - - // 隐藏链接信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_link_card), - summary = stringResource(R.string.hide_link_card_summary), - checked = isHideLinkCard, - onChange = onHideLinkCardChange - ) - - // 隐藏标签行 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_tag_card), - summary = stringResource(R.string.hide_tag_card_summary), - checked = isHideTagRow, - onChange = onHideTagRowChange - ) - } - KsuIsValid { - // 高级设置 - SettingsCard( - title = stringResource(R.string.advanced_settings) - ) { - // SELinux 开关 - SwitchSettingItem( - icon = Icons.Filled.Security, - title = stringResource(R.string.selinux), - summary = if (selinuxEnabled) - stringResource(R.string.selinux_enabled) else - stringResource(R.string.selinux_disabled), - checked = selinuxEnabled, - onChange = { enabled -> - val command = if (enabled) "setenforce 1" else "setenforce 0" - Shell.getShell().newJob().add(command).exec().let { result -> - if (result.isSuccess) { - selinuxEnabled = enabled - // 显示成功提示 - val message = if (enabled) - context.getString(R.string.selinux_enabled_toast) - else - context.getString(R.string.selinux_disabled_toast) - - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - } else { - // 显示失败提示 - Toast.makeText( - context, - context.getString(R.string.selinux_change_failed), - Toast.LENGTH_SHORT - ).show() - } - } - } - ) - - // SuSFS 开关(仅在支持时显示) - val suSFS = getSuSFS() - val isSUS_SU = getSuSFSFeatures() - if (suSFS == "Supported" && isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") { - // 默认启用 - var isEnabled by rememberSaveable { - mutableStateOf(true) - } - - // 在启动时检查状态 - LaunchedEffect(Unit) { - // 如果当前模式不是2就强制启用 - val currentMode = susfsSUS_SU_Mode() - val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true) - if (currentMode != "2" && wasManuallyDisabled) { - susfsSUS_SU_2() // 强制切换到模式2 - prefs.edit { putBoolean("enable_sus_su", true) } - } - isEnabled = currentMode == "2" - } - - SwitchSettingItem( - icon = Icons.Filled.Security, - title = stringResource(id = R.string.settings_susfs_toggle), - summary = stringResource(id = R.string.settings_susfs_toggle_summary), - checked = isEnabled, - onChange = { - if (it) { - // 手动启用 - susfsSUS_SU_2() - prefs.edit { putBoolean("enable_sus_su", true) } - Toast.makeText( - context, - context.getString(R.string.susfs_enabled), - Toast.LENGTH_SHORT - ).show() - } else { - // 手动关闭 - susfsSUS_SU_0() - prefs.edit { putBoolean("enable_sus_su", false) } - Toast.makeText( - context, - context.getString(R.string.susfs_disabled), - Toast.LENGTH_SHORT - ).show() - } - isEnabled = it - } - ) - } - // 动态管理器设置 - if (Natives.version >= Natives.MINIMAL_SUPPORTED_DYNAMIC_MANAGER) { - SettingItem( - icon = Icons.Filled.Security, - title = stringResource(R.string.dynamic_manager_title), - subtitle = if (isDynamicSignEnabled) { - stringResource( - R.string.dynamic_manager_enabled_summary, - dynamicSignSize - ) - } else { - stringResource(R.string.dynamic_manager_disabled) - }, - onClick = { showDynamicSignDialog = true } - ) - } - } - } - } - } -} - - - -@Composable -fun SettingsCard( - title: String, - icon: ImageVector? = null, - content: @Composable () -> Unit -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = SETTINGS_GROUP_SPACING), - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), - elevation = getCardElevation(), - shape = MaterialTheme.shapes.medium - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .padding(horizontal = 16.dp) - ) { - if (icon != null) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - } - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - ) - } - content() - } - } -} - -@Composable -fun SettingItem( - icon: ImageVector, - title: String, - subtitle: String? = null, - onClick: () -> Unit, - iconTint: Color = MaterialTheme.colorScheme.primary, - trailingContent: @Composable (() -> Unit)? = { - Icon( - Icons.AutoMirrored.Filled.NavigateNext, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } -) { - Row( - modifier = Modifier - .fillMaxWidth() -// .height(if (subtitle != null) SETTINGS_ITEM_HEIGHT + 12.dp else SETTINGS_ITEM_HEIGHT) - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 5.dp), - verticalAlignment = Alignment.Top - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = iconTint, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - maxLines = Int.MAX_VALUE, - overflow = TextOverflow.Visible - ) - if (subtitle != null) { - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = subtitle, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = Int.MAX_VALUE, - overflow = TextOverflow.Visible - ) - } - } - - trailingContent?.invoke() - } -} - -@Composable -fun SwitchSettingItem( - icon: ImageVector, - title: String, - summary: String? = null, - checked: Boolean, - onChange: (Boolean) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() -// .height(if (summary != null) SETTINGS_ITEM_HEIGHT + 12.dp else SETTINGS_ITEM_HEIGHT) - .clickable { onChange(!checked) } - .padding(horizontal = 16.dp, vertical = 10.dp), - verticalAlignment = Alignment.Top - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - lineHeight = 20.sp, -// maxLines = 1, -// overflow = TextOverflow.Ellipsis - ) - if (summary != null) { - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 16.sp, -// maxLines = 2, -// overflow = TextOverflow.Ellipsis - ) - } - } - - Switch( - checked = checked, - onCheckedChange = onChange - ) - } -} - -@Composable -fun SettingsDivider() { - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp) - ) -} - -@Composable -fun ColorCircle( - color: Color, - isSelected: Boolean, - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .size(20.dp) - .clip(CircleShape) - .background(color) - .then( - if (isSelected) { - Modifier.border( - width = 2.dp, - color = MaterialTheme.colorScheme.primary, - shape = CircleShape - ) - } else { - Modifier - } - ) - ) -} - -@Composable -fun SingleChoiceDialog( - title: String, - options: List, - selectedIndex: Int, - onOptionSelected: (Int) -> Unit, - onDismiss: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - options.forEachIndexed { index, option -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onOptionSelected(index) - onDismiss() - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedIndex == index, - onClick = null - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(option) - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) - } - } - ) -} - -@Composable -fun ConfirmDialog( - title: String, - message: String, - summaryText: String? = null, - confirmText: String = stringResource(R.string.confirm), - dismissText: String = stringResource(R.string.cancel), - onConfirm: () -> Unit, - onDismiss: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column { - Text(message) - if (summaryText != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - summaryText, - style = MaterialTheme.typography.bodySmall - ) - } - } - }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(confirmText) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(dismissText) - } - } - ) -} - -@Composable -fun KeyValueChoiceDialog( - title: String, - options: List>, - selectedCode: String, - onOptionSelected: (String) -> Unit, - onDismiss: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - options.forEach { (code, name) -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onOptionSelected(code) - onDismiss() - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedCode == code, - onClick = null - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(name) - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) - } - } - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/KernelFlash.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/KernelFlash.kt similarity index 91% rename from manager/app/src/main/java/zako/zako/zako/zakoui/screen/KernelFlash.kt rename to manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/KernelFlash.kt index 75ff72f..a87558f 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/KernelFlash.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/KernelFlash.kt @@ -1,7 +1,9 @@ -package zako.zako.zako.zakoui.screen +package zako.zako.zako.zakoui.screen.kernelFlash +import android.content.Context import android.net.Uri import android.os.Environment +import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background @@ -27,6 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.core.content.edit import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator @@ -39,9 +42,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import zako.zako.zako.zakoui.flash.FlashState -import zako.zako.zako.zakoui.flash.HorizonKernelState -import zako.zako.zako.zakoui.flash.HorizonKernelWorker +import zako.zako.zako.zakoui.screen.kernelFlash.state.FlashState +import zako.zako.zako.zakoui.screen.kernelFlash.state.HorizonKernelState +import zako.zako.zako.zakoui.screen.kernelFlash.state.HorizonKernelWorker import java.io.File import java.text.SimpleDateFormat import java.util.* @@ -73,6 +76,12 @@ fun KernelFlashScreen( kpmUndoPatch: Boolean = false ) { val context = LocalContext.current + + val shouldAutoExit = remember { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.getBoolean("auto_exit_after_flash", false) + } + val scrollState = rememberScrollState() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val snackBarHost = LocalSnackbarHost.current @@ -105,6 +114,16 @@ fun KernelFlashScreen( val onFlashComplete = { showFloatAction = true KernelFlashStateHolder.isFlashing = false + + // 如果需要自动退出,延迟1.5秒后退出 + if (shouldAutoExit) { + scope.launch { + delay(1500) + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + (context as? ComponentActivity)?.finish() + } + } } // 开始刷写 @@ -165,6 +184,19 @@ fun KernelFlashScreen( } } + DisposableEffect(shouldAutoExit) { + onDispose { + if (shouldAutoExit) { + KernelFlashStateHolder.currentState = null + KernelFlashStateHolder.currentUri = null + KernelFlashStateHolder.currentSlot = null + KernelFlashStateHolder.currentKpmPatchEnabled = false + KernelFlashStateHolder.currentKpmUndoPatch = false + KernelFlashStateHolder.isFlashing = false + } + } + } + BackHandler(enabled = true) { onBack() } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/component/SlotSelectionDialog.kt similarity index 99% rename from manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt rename to manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/component/SlotSelectionDialog.kt index 2f43c96..26da72c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/component/SlotSelectionDialog.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.component +package zako.zako.zako.zakoui.screen.kernelFlash.component import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/flash/KernelFlash.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/state/KernelFlashState.kt similarity index 98% rename from manager/app/src/main/java/zako/zako/zako/zakoui/flash/KernelFlash.kt rename to manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/state/KernelFlashState.kt index b577a6a..8cfa76b 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/flash/KernelFlash.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/state/KernelFlashState.kt @@ -1,4 +1,4 @@ -package zako.zako.zako.zakoui.flash +package zako.zako.zako.zakoui.screen.kernelFlash.state import android.annotation.SuppressLint import android.app.Activity @@ -7,6 +7,7 @@ import android.net.Uri import androidx.documentfile.provider.DocumentFile import com.sukisu.ultra.R import com.sukisu.ultra.network.RemoteToolsDownloader +import com.sukisu.ultra.ui.util.install import com.sukisu.ultra.ui.util.rootAvailable import com.sukisu.ultra.utils.AssetsUtil import com.topjohnwu.superuser.Shell @@ -171,6 +172,12 @@ class HorizonKernelWorker( runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot") } + try { + install() + } catch (e: Exception) { + state.updateStep("ksud update skipped: ${e.message}") + } + state.updateStep(context.getString(R.string.horizon_flash_complete_status)) state.completeFlashing() diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt new file mode 100644 index 0000000..1540d3a --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt @@ -0,0 +1,757 @@ +package zako.zako.zako.zakoui.screen.moreSettings + +import android.annotation.SuppressLint +import android.content.Context +import android.net.Uri +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.* +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.getValue +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.core.content.edit +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper +import com.sukisu.ultra.Natives +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.theme.component.ImageEditorDialog +import com.sukisu.ultra.ui.component.KsuIsValid +import com.sukisu.ultra.ui.screen.SwitchItem +import com.sukisu.ultra.ui.theme.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import zako.zako.zako.zakoui.screen.moreSettings.component.ColorCircle +import zako.zako.zako.zakoui.screen.moreSettings.component.LanguageSelectionDialog +import zako.zako.zako.zakoui.screen.moreSettings.component.MoreSettingsDialogs +import zako.zako.zako.zakoui.screen.moreSettings.component.SettingItem +import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsCard +import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsDivider +import zako.zako.zako.zakoui.screen.moreSettings.component.SwitchSettingItem +import zako.zako.zako.zakoui.screen.moreSettings.component.UidScannerSection +import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState +import kotlin.math.roundToInt + +@SuppressLint("LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt") +@OptIn(ExperimentalMaterial3Api::class) +@Destination +@Composable +fun MoreSettingsScreen( + navigator: DestinationsNavigator +) { + // 顶部滚动行为 + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } + val systemIsDark = isSystemInDarkTheme() + + // 创建设置状态管理器 + val settingsState = remember { MoreSettingsState(context, prefs, systemIsDark) } + val settingsHandlers = remember { MoreSettingsHandlers(context, prefs, settingsState) } + + // 图片选择器 + val pickImageLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.GetContent() + ) { uri: Uri? -> + uri?.let { + settingsState.selectedImageUri = it + settingsState.showImageEditor = true + } + } + + // 初始化设置 + LaunchedEffect(Unit) { + settingsHandlers.initializeSettings() + } + + // 显示图片编辑对话框 + if (settingsState.showImageEditor && settingsState.selectedImageUri != null) { + ImageEditorDialog( + imageUri = settingsState.selectedImageUri!!, + onDismiss = { + settingsState.showImageEditor = false + settingsState.selectedImageUri = null + }, + onConfirm = { transformedUri -> + settingsHandlers.handleCustomBackground(transformedUri) + settingsState.showImageEditor = false + settingsState.selectedImageUri = null + } + ) + } + + // 各种设置对话框 + MoreSettingsDialogs( + state = settingsState, + handlers = settingsHandlers + ) + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(R.string.more_settings), + style = MaterialTheme.typography.titleLarge + ) + }, + navigationIcon = { + IconButton(onClick = { navigator.popBackStack() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha), + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha) + ), + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior + ) + }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp) + .padding(top = 8.dp) + ) { + // 外观设置 + AppearanceSettings( + state = settingsState, + handlers = settingsHandlers, + pickImageLauncher = pickImageLauncher, + coroutineScope = coroutineScope + ) + + // 自定义设置 + CustomizationSettings( + state = settingsState, + handlers = settingsHandlers + ) + + // 高级设置 + KsuIsValid { + AdvancedSettings( + state = settingsState, + handlers = settingsHandlers + ) + } + } + } +} + +@Composable +private fun AppearanceSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers, + pickImageLauncher: ActivityResultLauncher, + coroutineScope: CoroutineScope +) { + SettingsCard(title = stringResource(R.string.appearance_settings)) { + // 语言设置 + LanguageSetting(state = state) + + // 主题模式 + SettingItem( + icon = Icons.Default.DarkMode, + title = stringResource(R.string.theme_mode), + subtitle = state.themeOptions[state.themeMode], + onClick = { state.showThemeModeDialog = true } + ) + + // 动态颜色开关 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + SwitchSettingItem( + icon = Icons.Filled.ColorLens, + title = stringResource(R.string.dynamic_color_title), + summary = stringResource(R.string.dynamic_color_summary), + checked = state.useDynamicColor, + onChange = handlers::handleDynamicColorChange + ) + } + + // 主题色选择 + AnimatedVisibility( + visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !state.useDynamicColor, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + ThemeColorSelection(state = state) + } + + SettingsDivider() + + // DPI 设置 + DpiSettings(state = state, handlers = handlers) + + SettingsDivider() + + // 自定义背景设置 + CustomBackgroundSettings( + state = state, + handlers = handlers, + pickImageLauncher = pickImageLauncher, + coroutineScope = coroutineScope + ) + } +} + +@Composable +private fun CustomizationSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + SettingsCard(title = stringResource(R.string.custom_settings)) { + // 图标切换 + SwitchSettingItem( + icon = Icons.Default.Android, + title = stringResource(R.string.icon_switch_title), + summary = stringResource(R.string.icon_switch_summary), + checked = state.useAltIcon, + onChange = handlers::handleIconChange + ) + + // 显示更多模块信息 + SwitchSettingItem( + icon = Icons.Filled.Info, + title = stringResource(R.string.show_more_module_info), + summary = stringResource(R.string.show_more_module_info_summary), + checked = state.showMoreModuleInfo, + onChange = handlers::handleShowMoreModuleInfoChange + ) + + // 简洁模式开关 + SwitchSettingItem( + icon = Icons.Filled.Brush, + title = stringResource(R.string.simple_mode), + summary = stringResource(R.string.simple_mode_summary), + checked = state.isSimpleMode, + onChange = handlers::handleSimpleModeChange + ) + + SwitchSettingItem( + icon = Icons.Filled.Brush, + title = stringResource(R.string.kernel_simple_kernel), + summary = stringResource(R.string.kernel_simple_kernel_summary), + checked = state.isKernelSimpleMode, + onChange = handlers::handleKernelSimpleModeChange + ) + + // 各种隐藏选项 + HideOptionsSettings(state = state, handlers = handlers) + } +} + +@Composable +private fun HideOptionsSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + // 隐藏内核版本号 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_kernel_kernelsu_version), + summary = stringResource(R.string.hide_kernel_kernelsu_version_summary), + checked = state.isHideVersion, + onChange = handlers::handleHideVersionChange + ) + + // 隐藏模块数量等信息 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_other_info), + summary = stringResource(R.string.hide_other_info_summary), + checked = state.isHideOtherInfo, + onChange = handlers::handleHideOtherInfoChange + ) + + // SuSFS 状态信息 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_susfs_status), + summary = stringResource(R.string.hide_susfs_status_summary), + checked = state.isHideSusfsStatus, + onChange = handlers::handleHideSusfsStatusChange + ) + + // Zygisk 实现状态信息 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_zygisk_implement), + summary = stringResource(R.string.hide_zygisk_implement_summary), + checked = state.isHideZygiskImplement, + onChange = handlers::handleHideZygiskImplementChange + ) + + if (Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) { + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.show_kpm_info), + summary = stringResource(R.string.show_kpm_info_summary), + checked = state.isShowKpmInfo, + onChange = handlers::handleShowKpmInfoChange + ) + } + + // 隐藏链接信息 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_link_card), + summary = stringResource(R.string.hide_link_card_summary), + checked = state.isHideLinkCard, + onChange = handlers::handleHideLinkCardChange + ) + + // 隐藏标签行 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_tag_card), + summary = stringResource(R.string.hide_tag_card_summary), + checked = state.isHideTagRow, + onChange = handlers::handleHideTagRowChange + ) +} + +@Composable +private fun AdvancedSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val snackBarHost = remember { SnackbarHostState() } + val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } + + SettingsCard(title = stringResource(R.string.advanced_settings)) { + // SELinux 开关 + SwitchSettingItem( + icon = Icons.Filled.Security, + title = stringResource(R.string.selinux), + summary = if (state.selinuxEnabled) + stringResource(R.string.selinux_enabled) else + stringResource(R.string.selinux_disabled), + checked = state.selinuxEnabled, + onChange = handlers::handleSelinuxChange + ) + + var forceSignatureVerification by rememberSaveable { + mutableStateOf(prefs.getBoolean("force_signature_verification", false)) + } + + // 强制签名验证开关 + SwitchItem( + icon = Icons.Filled.Security, + title = stringResource(R.string.module_signature_verification), + summary = stringResource(R.string.module_signature_verification_summary), + checked = forceSignatureVerification, + onCheckedChange = { enabled -> + prefs.edit { putBoolean("force_signature_verification", enabled) } + forceSignatureVerification = enabled + } + ) + + // UID 扫描开关 + if (Natives.version >= Natives.MINIMAL_SUPPORTED_UID_SCANNER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) { + UidScannerSection(prefs, snackBarHost, scope, context) + } + + // 动态管理器设置 + if (Natives.version >= Natives.MINIMAL_SUPPORTED_DYNAMIC_MANAGER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) { + SettingItem( + icon = Icons.Filled.Security, + title = stringResource(R.string.dynamic_manager_title), + subtitle = if (state.isDynamicSignEnabled) { + stringResource(R.string.dynamic_manager_enabled_summary, state.dynamicSignSize) + } else { + stringResource(R.string.dynamic_manager_disabled) + }, + onClick = { state.showDynamicSignDialog = true } + ) + } + } +} + +@Composable +private fun ThemeColorSelection(state: MoreSettingsState) { + SettingItem( + icon = Icons.Default.Palette, + title = stringResource(R.string.theme_color), + subtitle = when (ThemeConfig.currentTheme) { + is ThemeColors.Green -> stringResource(R.string.color_green) + is ThemeColors.Purple -> stringResource(R.string.color_purple) + is ThemeColors.Orange -> stringResource(R.string.color_orange) + is ThemeColors.Pink -> stringResource(R.string.color_pink) + is ThemeColors.Gray -> stringResource(R.string.color_gray) + is ThemeColors.Yellow -> stringResource(R.string.color_yellow) + else -> stringResource(R.string.color_default) + }, + onClick = { state.showThemeColorDialog = true }, + trailingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 8.dp) + ) { + val theme = ThemeConfig.currentTheme + val isDark = isSystemInDarkTheme() + + ColorCircle( + color = if (isDark) theme.primaryDark else theme.primaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + ColorCircle( + color = if (isDark) theme.secondaryDark else theme.secondaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + ColorCircle( + color = if (isDark) theme.tertiaryDark else theme.tertiaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + } + } + ) +} + +@Composable +private fun DpiSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + SettingItem( + icon = Icons.Default.FormatSize, + title = stringResource(R.string.app_dpi_title), + subtitle = stringResource(R.string.app_dpi_summary), + onClick = {}, + trailingContent = { + Text( + text = handlers.getDpiFriendlyName(state.tempDpi), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) + } + ) + + // DPI 滑动条和控制 + DpiSliderControls(state = state, handlers = handlers) +} + +@Composable +private fun DpiSliderControls( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + val sliderValue by animateFloatAsState( + targetValue = state.tempDpi.toFloat(), + label = "DPI Slider Animation" + ) + + Slider( + value = sliderValue, + onValueChange = { newValue -> + state.tempDpi = newValue.toInt() + state.isDpiCustom = !state.dpiPresets.containsValue(state.tempDpi) + }, + valueRange = 160f..600f, + steps = 11, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) + + // DPI 预设按钮行 + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + ) { + state.dpiPresets.forEach { (name, dpi) -> + val isSelected = state.tempDpi == dpi + val buttonColor = if (isSelected) + MaterialTheme.colorScheme.primaryContainer + else + MaterialTheme.colorScheme.surfaceVariant + + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 2.dp) + .clip(RoundedCornerShape(8.dp)) + .background(buttonColor) + .clickable { + state.tempDpi = dpi + state.isDpiCustom = false + } + .padding(vertical = 8.dp, horizontal = 4.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = name, + style = MaterialTheme.typography.labelMedium, + color = if (isSelected) + MaterialTheme.colorScheme.onPrimaryContainer + else + MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + + Text( + text = if (state.isDpiCustom) + "${stringResource(R.string.dpi_size_custom)}: ${state.tempDpi}" + else + "${handlers.getDpiFriendlyName(state.tempDpi)}: ${state.tempDpi}", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = 8.dp) + ) + + Button( + onClick = { state.showDpiConfirmDialog = true }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + enabled = state.tempDpi != state.currentDpi + ) { + Icon( + Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.dpi_apply_settings)) + } + } +} + +@Composable +private fun CustomBackgroundSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers, + pickImageLauncher: ActivityResultLauncher, + coroutineScope: CoroutineScope +) { + // 自定义背景开关 + SwitchSettingItem( + icon = Icons.Filled.Wallpaper, + title = stringResource(id = R.string.settings_custom_background), + summary = stringResource(id = R.string.settings_custom_background_summary), + checked = state.isCustomBackgroundEnabled, + onChange = { isChecked -> + if (isChecked) { + pickImageLauncher.launch("image/*") + } else { + handlers.handleRemoveCustomBackground() + } + } + ) + + // 透明度和亮度调节 + AnimatedVisibility( + visible = ThemeConfig.customBackgroundUri != null, + enter = fadeIn() + slideInVertically(), + exit = fadeOut() + slideOutVertically() + ) { + BackgroundAdjustmentControls( + state = state, + handlers = handlers, + coroutineScope = coroutineScope + ) + } +} + +@Composable +private fun BackgroundAdjustmentControls( + state: MoreSettingsState, + handlers: MoreSettingsHandlers, + coroutineScope: CoroutineScope +) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + // 透明度滑动条 + AlphaSlider(state = state, handlers = handlers, coroutineScope = coroutineScope) + + // 亮度调节滑动条 + DimSlider(state = state, handlers = handlers, coroutineScope = coroutineScope) + } +} + +@Composable +private fun AlphaSlider( + state: MoreSettingsState, + handlers: MoreSettingsHandlers, + coroutineScope: CoroutineScope +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 4.dp) + ) { + Icon( + Icons.Filled.Opacity, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.settings_card_alpha), + style = MaterialTheme.typography.titleSmall + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "${(state.cardAlpha * 100).roundToInt()}%", + style = MaterialTheme.typography.labelMedium, + ) + } + + val alphaSliderValue by animateFloatAsState( + targetValue = state.cardAlpha, + label = "Alpha Slider Animation" + ) + + Slider( + value = alphaSliderValue, + onValueChange = { newValue -> + handlers.handleCardAlphaChange(newValue) + }, + onValueChangeFinished = { + coroutineScope.launch(Dispatchers.IO) { + saveCardConfig(handlers.context) + } + }, + valueRange = 0f..1f, + steps = 20, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) +} + +@Composable +private fun DimSlider( + state: MoreSettingsState, + handlers: MoreSettingsHandlers, + coroutineScope: CoroutineScope +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 16.dp, bottom = 4.dp) + ) { + Icon( + Icons.Filled.LightMode, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.settings_card_dim), + style = MaterialTheme.typography.titleSmall + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "${(state.cardDim * 100).roundToInt()}%", + style = MaterialTheme.typography.labelMedium, + ) + } + + val dimSliderValue by animateFloatAsState( + targetValue = state.cardDim, + label = "Dim Slider Animation" + ) + + Slider( + value = dimSliderValue, + onValueChange = { newValue -> + handlers.handleCardDimChange(newValue) + }, + onValueChangeFinished = { + coroutineScope.launch(Dispatchers.IO) { + saveCardConfig(handlers.context) + } + }, + valueRange = 0f..1f, + steps = 20, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) +} + +fun saveCardConfig(context: Context) { + CardConfig.save(context) +} + +@Composable +private fun LanguageSetting(state: MoreSettingsState) { + val context = LocalContext.current + val language = stringResource(id = R.string.settings_language) + + // Compute display name based on current app locale + val currentLanguageDisplay = remember(state.currentAppLocale) { + val locale = state.currentAppLocale + if (locale != null) { + locale.getDisplayName(locale) + } else { + context.getString(R.string.language_system_default) + } + } + + SettingItem( + icon = Icons.Filled.Translate, + title = language, + subtitle = currentLanguageDisplay, + onClick = { state.showLanguageDialog = true } + ) + + // Language Selection Dialog + if (state.showLanguageDialog) { + LanguageSelectionDialog( + onLanguageSelected = { newLocale -> + // Update local state immediately + state.currentAppLocale = LocaleHelper.getCurrentAppLocale(context) + // Apply locale change immediately for Android < 13 + LocaleHelper.restartActivity(context) + }, + onDismiss = { state.showLanguageDialog = false } + ) + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt new file mode 100644 index 0000000..b5b9921 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt @@ -0,0 +1,459 @@ +package zako.zako.zako.zakoui.screen.moreSettings + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.Configuration +import android.net.Uri +import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CleaningServices +import androidx.compose.material.icons.filled.Groups +import androidx.compose.material.icons.filled.Scanner +import androidx.compose.material3.SnackbarHostState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.edit +import com.sukisu.ultra.Natives +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.ConfirmResult +import com.sukisu.ultra.ui.component.rememberConfirmDialog +import com.sukisu.ultra.ui.screen.SettingItem +import com.sukisu.ultra.ui.screen.SwitchItem +import com.sukisu.ultra.ui.theme.* +import com.sukisu.ultra.ui.util.* +import com.topjohnwu.superuser.Shell +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState +import zako.zako.zako.zakoui.screen.moreSettings.util.toggleLauncherIcon + +/** + * 更多设置处理器 + */ +class MoreSettingsHandlers( + val context: Context, + private val prefs: SharedPreferences, + private val state: MoreSettingsState +) { + + /** + * 初始化设置 + */ + fun initializeSettings() { + // 加载设置 + CardConfig.load(context) + state.cardAlpha = CardConfig.cardAlpha + state.cardDim = CardConfig.cardDim + state.isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null + + // 设置主题模式 + state.themeMode = when (ThemeConfig.forceDarkMode) { + true -> 2 + false -> 1 + null -> 0 + } + + // 确保卡片样式跟随主题模式 + when (state.themeMode) { + 2 -> { // 深色 + CardConfig.isUserDarkModeEnabled = true + CardConfig.isUserLightModeEnabled = false + } + 1 -> { // 浅色 + CardConfig.isUserDarkModeEnabled = false + CardConfig.isUserLightModeEnabled = true + } + 0 -> { // 跟随系统 + CardConfig.isUserDarkModeEnabled = false + CardConfig.isUserLightModeEnabled = false + } + } + + // 如果启用了系统跟随且系统是深色模式,应用深色模式默认值 + if (state.themeMode == 0 && state.systemIsDark) { + CardConfig.setThemeDefaults(true) + } + + state.currentDpi = prefs.getInt("app_dpi", state.systemDpi) + state.tempDpi = state.currentDpi + + CardConfig.save(context) + + // 初始化 SELinux 状态 + state.selinuxEnabled = Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing" + + // 初始化动态管理器配置 + state.dynamicSignConfig = Natives.getDynamicManager() + state.dynamicSignConfig?.let { config -> + if (config.isValid()) { + state.isDynamicSignEnabled = true + state.dynamicSignSize = config.size.toString() + state.dynamicSignHash = config.hash + } + } + } + + /** + * 处理主题模式变更 + */ + fun handleThemeModeChange(index: Int) { + state.themeMode = index + val newThemeMode = when (index) { + 0 -> null // 跟随系统 + 1 -> false // 浅色 + 2 -> true // 深色 + else -> null + } + context.saveThemeMode(newThemeMode) + ThemeConfig.updateTheme(darkMode = newThemeMode) + + when (index) { + 2 -> { // 深色 + ThemeConfig.updateTheme(darkMode = true) + CardConfig.updateThemePreference(darkMode = true, lightMode = false) + CardConfig.setThemeDefaults(true) + CardConfig.save(context) + } + 1 -> { // 浅色 + ThemeConfig.updateTheme(darkMode = false) + CardConfig.updateThemePreference(darkMode = false, lightMode = true) + CardConfig.setThemeDefaults(false) + CardConfig.save(context) + } + 0 -> { // 跟随系统 + ThemeConfig.updateTheme(darkMode = null) + CardConfig.updateThemePreference(darkMode = null, lightMode = null) + val isNightModeActive = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + CardConfig.setThemeDefaults(isNightModeActive) + CardConfig.save(context) + } + } + } + + /** + * 处理主题色变更 + */ + fun handleThemeColorChange(theme: ThemeColors) { + context.saveThemeColors(when (theme) { + ThemeColors.Green -> "green" + ThemeColors.Purple -> "purple" + ThemeColors.Orange -> "orange" + ThemeColors.Pink -> "pink" + ThemeColors.Gray -> "gray" + ThemeColors.Yellow -> "yellow" + else -> "default" + }) + ThemeConfig.updateTheme(theme = theme) + } + + /** + * 处理动态颜色变更 + */ + fun handleDynamicColorChange(enabled: Boolean) { + state.useDynamicColor = enabled + context.saveDynamicColorState(enabled) + ThemeConfig.updateTheme(dynamicColor = enabled) + } + + /** + * 获取DPI大小友好名称 + */ + @Composable + fun getDpiFriendlyName(dpi: Int): String { + return when (dpi) { + 240 -> stringResource(R.string.dpi_size_small) + 320 -> stringResource(R.string.dpi_size_medium) + 420 -> stringResource(R.string.dpi_size_large) + 560 -> stringResource(R.string.dpi_size_extra_large) + else -> stringResource(R.string.dpi_size_custom) + } + } + + /** + * 应用 DPI 设置 + */ + fun handleDpiApply() { + if (state.tempDpi != state.currentDpi) { + prefs.edit { + putInt("app_dpi", state.tempDpi) + } + + state.currentDpi = state.tempDpi + Toast.makeText( + context, + context.getString(R.string.dpi_applied_success, state.tempDpi), + Toast.LENGTH_SHORT + ).show() + + val restartIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + restartIntent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(restartIntent) + + state.showDpiConfirmDialog = false + } + } + + /** + * 处理自定义背景 + */ + fun handleCustomBackground(transformedUri: Uri) { + context.saveAndApplyCustomBackground(transformedUri) + state.isCustomBackgroundEnabled = true + CardConfig.cardElevation = 0.dp + CardConfig.isCustomBackgroundEnabled = true + saveCardConfig(context) + + Toast.makeText( + context, + context.getString(R.string.background_set_success), + Toast.LENGTH_SHORT + ).show() + } + + /** + * 处理移除自定义背景 + */ + fun handleRemoveCustomBackground() { + context.saveCustomBackground(null) + state.isCustomBackgroundEnabled = false + CardConfig.cardAlpha = 1f + CardConfig.cardDim = 0f + CardConfig.isCustomAlphaSet = false + CardConfig.isCustomDimSet = false + CardConfig.isCustomBackgroundEnabled = false + saveCardConfig(context) + ThemeConfig.preventBackgroundRefresh = false + + context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { + putBoolean("prevent_background_refresh", false) + } + + Toast.makeText( + context, + context.getString(R.string.background_removed), + Toast.LENGTH_SHORT + ).show() + } + + /** + * 处理卡片透明度变更 + */ + fun handleCardAlphaChange(newValue: Float) { + state.cardAlpha = newValue + CardConfig.cardAlpha = newValue + CardConfig.isCustomAlphaSet = true + prefs.edit { + putBoolean("is_custom_alpha_set", true) + putFloat("card_alpha", newValue) + } + } + + /** + * 处理卡片亮度变更 + */ + fun handleCardDimChange(newValue: Float) { + state.cardDim = newValue + CardConfig.cardDim = newValue + CardConfig.isCustomDimSet = true + prefs.edit { + putBoolean("is_custom_dim_set", true) + putFloat("card_dim", newValue) + } + } + + /** + * 处理图标变更 + */ + fun handleIconChange(newValue: Boolean) { + prefs.edit { putBoolean("use_alt_icon", newValue) } + state.useAltIcon = newValue + toggleLauncherIcon(context, newValue) + Toast.makeText(context, context.getString(R.string.icon_switched), Toast.LENGTH_SHORT).show() + } + + /** + * 处理简洁模式变更 + */ + fun handleSimpleModeChange(newValue: Boolean) { + prefs.edit { putBoolean("is_simple_mode", newValue) } + state.isSimpleMode = newValue + } + + /** + * 处理内核简洁模式变更 + */ + fun handleKernelSimpleModeChange(newValue: Boolean) { + prefs.edit { putBoolean("is_kernel_simple_mode", newValue) } + state.isKernelSimpleMode = newValue + } + + /** + * 处理隐藏版本变更 + */ + fun handleHideVersionChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_version", newValue) } + state.isHideVersion = newValue + } + + /** + * 处理隐藏其他信息变更 + */ + fun handleHideOtherInfoChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_other_info", newValue) } + state.isHideOtherInfo = newValue + } + + /** + * 处理显示KPM信息变更 + */ + fun handleShowKpmInfoChange(newValue: Boolean) { + prefs.edit { putBoolean("show_kpm_info", newValue) } + state.isShowKpmInfo = newValue + } + + /** + * 处理隐藏SuSFS状态变更 + */ + fun handleHideSusfsStatusChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_susfs_status", newValue) } + state.isHideSusfsStatus = newValue + } + + /** + * 处理隐藏Zygisk实现变更 + */ + fun handleHideZygiskImplementChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_zygisk_Implement", newValue) } + state.isHideZygiskImplement = newValue + } + + /** + * 处理隐藏链接卡片变更 + */ + fun handleHideLinkCardChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_link_card", newValue) } + state.isHideLinkCard = newValue + } + + /** + * 处理隐藏标签行变更 + */ + fun handleHideTagRowChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_tag_row", newValue) } + state.isHideTagRow = newValue + } + + /** + * 处理显示更多模块信息变更 + */ + fun handleShowMoreModuleInfoChange(newValue: Boolean) { + prefs.edit { putBoolean("show_more_module_info", newValue) } + state.showMoreModuleInfo = newValue + } + + /** + * 处理SELinux变更 + */ + fun handleSelinuxChange(enabled: Boolean) { + val command = if (enabled) "setenforce 1" else "setenforce 0" + Shell.getShell().newJob().add(command).exec().let { result -> + if (result.isSuccess) { + state.selinuxEnabled = enabled + val message = if (enabled) + context.getString(R.string.selinux_enabled_toast) + else + context.getString(R.string.selinux_disabled_toast) + + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } else { + Toast.makeText( + context, + context.getString(R.string.selinux_change_failed), + Toast.LENGTH_SHORT + ).show() + } + } + } + + /** + * 处理动态管理器配置 + */ + fun handleDynamicManagerConfig(enabled: Boolean, size: String, hash: String) { + if (enabled) { + val parsedSize = parseDynamicSignSize(size) + if (parsedSize != null && parsedSize > 0 && hash.length == 64) { + val success = Natives.setDynamicManager(parsedSize, hash) + if (success) { + state.dynamicSignConfig = Natives.DynamicManagerConfig(parsedSize, hash) + state.isDynamicSignEnabled = true + state.dynamicSignSize = size + state.dynamicSignHash = hash + Toast.makeText( + context, + context.getString(R.string.dynamic_manager_set_success), + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + context.getString(R.string.dynamic_manager_set_failed), + Toast.LENGTH_SHORT + ).show() + } + } else { + Toast.makeText( + context, + context.getString(R.string.invalid_sign_config), + Toast.LENGTH_SHORT + ).show() + } + } else { + val success = Natives.clearDynamicManager() + if (success) { + state.dynamicSignConfig = null + state.isDynamicSignEnabled = false + state.dynamicSignSize = "" + state.dynamicSignHash = "" + Toast.makeText( + context, + context.getString(R.string.dynamic_manager_disabled_success), + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + context.getString(R.string.dynamic_manager_clear_failed), + Toast.LENGTH_SHORT + ).show() + } + } + } + + /** + * 解析动态签名大小 + */ + private fun parseDynamicSignSize(input: String): Int? { + return try { + when { + input.startsWith("0x", true) -> input.substring(2).toInt(16) + else -> input.toInt() + } + } catch (_: NumberFormatException) { + null + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt new file mode 100644 index 0000000..3c182c1 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt @@ -0,0 +1,201 @@ +package zako.zako.zako.zakoui.screen.moreSettings.component + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.NavigateNext +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.sukisu.ultra.ui.theme.* + +private val SETTINGS_GROUP_SPACING = 16.dp + +@Composable +fun SettingsCard( + title: String, + icon: ImageVector? = null, + content: @Composable () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = SETTINGS_GROUP_SPACING), + colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), + elevation = getCardElevation(), + shape = MaterialTheme.shapes.medium + ) { + Column(modifier = Modifier.padding(vertical = 8.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .padding(horizontal = 16.dp) + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + } + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + ) + } + content() + } + } +} + +@Composable +fun SettingItem( + icon: ImageVector, + title: String, + subtitle: String? = null, + onClick: () -> Unit, + iconTint: Color = MaterialTheme.colorScheme.primary, + trailingContent: @Composable (() -> Unit)? = { + Icon( + Icons.AutoMirrored.Filled.NavigateNext, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 5.dp), + verticalAlignment = Alignment.Top + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = iconTint, + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp) + ) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + maxLines = Int.MAX_VALUE, + overflow = TextOverflow.Visible + ) + if (subtitle != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = Int.MAX_VALUE, + overflow = TextOverflow.Visible + ) + } + } + + trailingContent?.invoke() + } +} + +@Composable +fun SwitchSettingItem( + icon: ImageVector, + title: String, + summary: String? = null, + checked: Boolean, + onChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onChange(!checked) } + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.Top + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp) + ) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + lineHeight = 20.sp, + ) + if (summary != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = summary, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 16.sp, + ) + } + } + + Switch( + checked = checked, + onCheckedChange = onChange + ) + } +} + +@Composable +fun SettingsDivider() { + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp) + ) +} + +@Composable +fun ColorCircle( + color: Color, + isSelected: Boolean, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .size(20.dp) + .clip(CircleShape) + .background(color) + .then( + if (isSelected) { + Modifier.border( + width = 2.dp, + color = MaterialTheme.colorScheme.primary, + shape = CircleShape + ) + } else { + Modifier + } + ) + ) +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt new file mode 100644 index 0000000..cebfd8b --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt @@ -0,0 +1,620 @@ +package zako.zako.zako.zakoui.screen.moreSettings.component + +import android.content.Context +import android.content.SharedPreferences +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CleaningServices +import androidx.compose.material.icons.filled.Groups +import androidx.compose.material.icons.filled.Scanner +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.core.content.edit +import com.maxkeppeker.sheets.core.models.base.Header +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState +import com.maxkeppeler.sheets.list.ListDialog +import com.maxkeppeler.sheets.list.models.ListOption +import com.maxkeppeler.sheets.list.models.ListSelection +import com.sukisu.ultra.Natives +import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.ConfirmResult +import com.sukisu.ultra.ui.component.rememberConfirmDialog +import com.sukisu.ultra.ui.screen.SwitchItem +import com.sukisu.ultra.ui.theme.* +import com.sukisu.ultra.ui.util.cleanRuntimeEnvironment +import com.sukisu.ultra.ui.util.getUidMultiUserScan +import com.sukisu.ultra.ui.util.readUidScannerFile +import com.sukisu.ultra.ui.util.setUidAutoScan +import com.sukisu.ultra.ui.util.setUidMultiUserScan +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import zako.zako.zako.zakoui.screen.moreSettings.MoreSettingsHandlers +import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState + +@Composable +fun MoreSettingsDialogs( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + // 主题模式选择对话框 + if (state.showThemeModeDialog) { + SingleChoiceDialog( + title = stringResource(R.string.theme_mode), + options = state.themeOptions, + selectedIndex = state.themeMode, + onOptionSelected = { index -> + handlers.handleThemeModeChange(index) + }, + onDismiss = { state.showThemeModeDialog = false } + ) + } + + // DPI 设置确认对话框 + if (state.showDpiConfirmDialog) { + ConfirmDialog( + title = stringResource(R.string.dpi_confirm_title), + message = stringResource(R.string.dpi_confirm_message, state.currentDpi, state.tempDpi), + summaryText = stringResource(R.string.dpi_confirm_summary), + confirmText = stringResource(R.string.confirm), + dismissText = stringResource(R.string.cancel), + onConfirm = { handlers.handleDpiApply() }, + onDismiss = { + state.showDpiConfirmDialog = false + state.tempDpi = state.currentDpi + } + ) + } + + // 主题色选择对话框 + if (state.showThemeColorDialog) { + ThemeColorDialog( + onColorSelected = { theme -> + handlers.handleThemeColorChange(theme) + state.showThemeColorDialog = false + }, + onDismiss = { state.showThemeColorDialog = false } + ) + } + + // 动态管理器配置对话框 + if (state.showDynamicSignDialog) { + DynamicManagerDialog( + state = state, + onConfirm = { enabled, size, hash -> + handlers.handleDynamicManagerConfig(enabled, size, hash) + state.showDynamicSignDialog = false + }, + onDismiss = { state.showDynamicSignDialog = false } + ) + } +} + +@Composable +fun SingleChoiceDialog( + title: String, + options: List, + selectedIndex: Int, + onOptionSelected: (Int) -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + options.forEachIndexed { index, option -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onOptionSelected(index) + onDismiss() + } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedIndex == index, + onClick = null + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(option) + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + } + ) +} + +@Composable +fun ConfirmDialog( + title: String, + message: String, + summaryText: String? = null, + confirmText: String = stringResource(R.string.confirm), + dismissText: String = stringResource(R.string.cancel), + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + Column { + Text(message) + if (summaryText != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + summaryText, + style = MaterialTheme.typography.bodySmall + ) + } + } + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(confirmText) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(dismissText) + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LanguageSelectionDialog( + onLanguageSelected: (String) -> Unit, + onDismiss: () -> Unit +) { + val context = LocalContext.current + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + + // Check if should use system language settings + if (LocaleHelper.useSystemLanguageSettings) { + // Android 13+ - Jump to system settings + LocaleHelper.launchSystemLanguageSettings(context) + onDismiss() + } else { + // Android < 13 - Show app language selector + // Dynamically detect supported locales from resources + val supportedLocales = remember { + val locales = mutableListOf() + + // Add system default first + locales.add(java.util.Locale.ROOT) // This will represent "System Default" + + // Dynamically detect available locales by checking resource directories + val resourceDirs = listOf( + "ar", "bg", "de", "fa", "fr", "hu", "in", "it", + "ja", "ko", "pl", "pt-rBR", "ru", "th", "tr", + "uk", "vi", "zh-rCN", "zh-rTW" + ) + + resourceDirs.forEach { dir -> + try { + val locale = when { + dir.contains("-r") -> { + val parts = dir.split("-r") + java.util.Locale.Builder() + .setLanguage(parts[0]) + .setRegion(parts[1]) + .build() + } + else -> java.util.Locale.Builder() + .setLanguage(dir) + .build() + } + + // Test if this locale has translated resources + val config = android.content.res.Configuration() + config.setLocale(locale) + val localizedContext = context.createConfigurationContext(config) + + // Try to get a translated string to verify the locale is supported + val testString = localizedContext.getString(R.string.settings_language) + val defaultString = context.getString(R.string.settings_language) + + // If the string is different or it's English, it's supported + if (testString != defaultString || locale.language == "en") { + locales.add(locale) + } + } catch (_: Exception) { + // Skip unsupported locales + } + } + + // Sort by display name + val sortedLocales = locales.drop(1).sortedBy { it.getDisplayName(it) } + mutableListOf().apply { + add(locales.first()) // System default first + addAll(sortedLocales) + } + } + + val allOptions = supportedLocales.map { locale -> + val tag = if (locale == java.util.Locale.ROOT) { + "system" + } else if (locale.country.isEmpty()) { + locale.language + } else { + "${locale.language}_${locale.country}" + } + + val displayName = if (locale == java.util.Locale.ROOT) { + context.getString(R.string.language_system_default) + } else { + locale.getDisplayName(locale) + } + + tag to displayName + } + + val currentLocale = prefs.getString("app_locale", "system") ?: "system" + val options = allOptions.map { (tag, displayName) -> + ListOption( + titleText = displayName, + selected = currentLocale == tag + ) + } + + var selectedIndex by remember { + mutableIntStateOf(allOptions.indexOfFirst { (tag, _) -> currentLocale == tag }) + } + + ListDialog( + state = rememberUseCaseState( + visible = true, + onFinishedRequest = { + if (selectedIndex >= 0 && selectedIndex < allOptions.size) { + val newLocale = allOptions[selectedIndex].first + prefs.edit { putString("app_locale", newLocale) } + onLanguageSelected(newLocale) + } + onDismiss() + }, + onCloseRequest = { + onDismiss() + } + ), + header = Header.Default( + title = stringResource(R.string.settings_language), + ), + selection = ListSelection.Single( + showRadioButtons = true, + options = options + ) { index, _ -> + selectedIndex = index + } + ) + } +} +@Composable +fun ThemeColorDialog( + onColorSelected: (ThemeColors) -> Unit, + onDismiss: () -> Unit +) { + val themeColorOptions = listOf( + stringResource(R.string.color_default) to ThemeColors.Default, + stringResource(R.string.color_green) to ThemeColors.Green, + stringResource(R.string.color_purple) to ThemeColors.Purple, + stringResource(R.string.color_orange) to ThemeColors.Orange, + stringResource(R.string.color_pink) to ThemeColors.Pink, + stringResource(R.string.color_gray) to ThemeColors.Gray, + stringResource(R.string.color_yellow) to ThemeColors.Yellow + ) + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.choose_theme_color)) }, + text = { + Column { + themeColorOptions.forEach { (name, theme) -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onColorSelected(theme) } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val isDark = isSystemInDarkTheme() + Box( + modifier = Modifier.padding(end = 12.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + ColorCircle( + color = if (isDark) theme.primaryDark else theme.primaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + ColorCircle( + color = if (isDark) theme.secondaryDark else theme.secondaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + ColorCircle( + color = if (isDark) theme.tertiaryDark else theme.tertiaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + } + } + Text(name) + Spacer(modifier = Modifier.weight(1f)) + // 当前选中的主题显示选中标记 + if (ThemeConfig.currentTheme::class == theme::class) { + Icon( + Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + } + }, + confirmButton = { + Button( + onClick = onDismiss + ) { + Text(stringResource(R.string.cancel)) + } + } + ) +} + +@Composable +fun DynamicManagerDialog( + state: MoreSettingsState, + onConfirm: (Boolean, String, String) -> Unit, + onDismiss: () -> Unit +) { + var localEnabled by remember { mutableStateOf(state.isDynamicSignEnabled) } + var localSize by remember { mutableStateOf(state.dynamicSignSize) } + var localHash by remember { mutableStateOf(state.dynamicSignHash) } + + fun parseDynamicSignSize(input: String): Int? { + return try { + when { + input.startsWith("0x", true) -> input.substring(2).toInt(16) + else -> input.toInt() + } + } catch (_: NumberFormatException) { + null + } + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.dynamic_manager_title)) }, + text = { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + // 启用开关 + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { localEnabled = !localEnabled } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Switch( + checked = localEnabled, + onCheckedChange = { localEnabled = it } + ) + Spacer(modifier = Modifier.width(12.dp)) + Text(stringResource(R.string.enable_dynamic_manager)) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 签名大小输入 + OutlinedTextField( + value = localSize, + onValueChange = { input -> + val isValid = when { + input.isEmpty() -> true + input.matches(Regex("^\\d+$")) -> true + input.matches(Regex("^0[xX][0-9a-fA-F]*$")) -> true + else -> false + } + if (isValid) { + localSize = input + } + }, + label = { Text(stringResource(R.string.signature_size)) }, + enabled = localEnabled, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text + ) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // 签名哈希输入 + OutlinedTextField( + value = localHash, + onValueChange = { hash -> + if (hash.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) { + localHash = hash + } + }, + label = { Text(stringResource(R.string.signature_hash)) }, + enabled = localEnabled, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + supportingText = { + Text(stringResource(R.string.hash_must_be_64_chars)) + }, + isError = localEnabled && localHash.isNotEmpty() && localHash.length != 64 + ) + } + }, + confirmButton = { + Button( + onClick = { onConfirm(localEnabled, localSize, localHash) }, + enabled = if (localEnabled) { + parseDynamicSignSize(localSize)?.let { it > 0 } == true && + localHash.length == 64 + } else true + ) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + } + ) +} + +@Composable +fun UidScannerSection( + prefs: SharedPreferences, + snackBarHost: SnackbarHostState, + scope: CoroutineScope, + context: Context +) { + if (Natives.version < Natives.MINIMAL_SUPPORTED_UID_SCANNER) return + + val realAuto = Natives.isUidScannerEnabled() + val realMulti = getUidMultiUserScan() + + var autoOn by remember { mutableStateOf(realAuto) } + var multiOn by remember { mutableStateOf(realMulti) } + + LaunchedEffect(Unit) { + autoOn = realAuto + multiOn = realMulti + prefs.edit { + putBoolean("uid_auto_scan", autoOn) + putBoolean("uid_multi_user_scan", multiOn) + } + } + + SwitchItem( + icon = Icons.Filled.Scanner, + title = stringResource(R.string.uid_auto_scan_title), + summary = stringResource(R.string.uid_auto_scan_summary), + checked = autoOn, + onCheckedChange = { target -> + autoOn = target + if (!target) multiOn = false + + scope.launch(Dispatchers.IO) { + setUidAutoScan(target) + val actual = Natives.isUidScannerEnabled() || readUidScannerFile() + withContext(Dispatchers.Main) { + autoOn = actual + if (!actual) multiOn = false + prefs.edit { + putBoolean("uid_auto_scan", actual) + putBoolean("uid_multi_user_scan", multiOn) + } + if (actual != target) { + snackBarHost.showSnackbar( + context.getString(R.string.uid_scanner_setting_failed) + ) + } + } + } + } + ) + + AnimatedVisibility( + visible = autoOn, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + SwitchItem( + icon = Icons.Filled.Groups, + title = stringResource(R.string.uid_multi_user_scan_title), + summary = stringResource(R.string.uid_multi_user_scan_summary), + checked = multiOn, + onCheckedChange = { target -> + scope.launch(Dispatchers.IO) { + val ok = setUidMultiUserScan(target) + withContext(Dispatchers.Main) { + if (ok) { + multiOn = target + prefs.edit { putBoolean("uid_multi_user_scan", target) } + } else { + snackBarHost.showSnackbar( + context.getString(R.string.uid_scanner_setting_failed) + ) + } + } + } + } + ) + } + + AnimatedVisibility( + visible = autoOn, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + val confirmDialog = rememberConfirmDialog() + com.sukisu.ultra.ui.screen.SettingItem( + icon = Icons.Filled.CleaningServices, + title = stringResource(R.string.clean_runtime_environment), + summary = stringResource(R.string.clean_runtime_environment_summary), + onClick = { + scope.launch { + if (confirmDialog.awaitConfirm( + title = context.getString(R.string.clean_runtime_environment), + content = context.getString(R.string.clean_runtime_environment_confirm) + ) == ConfirmResult.Confirmed + ) { + if (cleanRuntimeEnvironment()) { + autoOn = false + multiOn = false + prefs.edit { + putBoolean("uid_auto_scan", false) + putBoolean("uid_multi_user_scan", false) + } + Natives.setUidScannerEnabled(false) + snackBarHost.showSnackbar( + context.getString(R.string.clean_runtime_environment_success) + ) + } else { + snackBarHost.showSnackbar( + context.getString(R.string.clean_runtime_environment_failed) + ) + } + } + } + } + ) + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt new file mode 100644 index 0000000..26e9593 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt @@ -0,0 +1,101 @@ +package zako.zako.zako.zakoui.screen.moreSettings.state + +import android.content.Context +import android.content.SharedPreferences +import android.net.Uri +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper +import com.sukisu.ultra.Natives +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.theme.CardConfig +import com.sukisu.ultra.ui.theme.ThemeConfig + +/** + * 更多设置状态管理 + */ +@Stable +class MoreSettingsState( + val context: Context, + val prefs: SharedPreferences, + val systemIsDark: Boolean +) { + // 主题模式选择 + var themeMode by mutableIntStateOf( + when (ThemeConfig.forceDarkMode) { + true -> 2 // 深色 + false -> 1 // 浅色 + null -> 0 // 跟随系统 + } + ) + + // 动态颜色开关状态 + var useDynamicColor by mutableStateOf(ThemeConfig.useDynamicColor) + + // 语言设置 + var showLanguageDialog by mutableStateOf(false) + var currentAppLocale by mutableStateOf(LocaleHelper.getCurrentAppLocale(context)) + + // 对话框显示状态 + var showThemeModeDialog by mutableStateOf(false) + var showThemeColorDialog by mutableStateOf(false) + var showDpiConfirmDialog by mutableStateOf(false) + var showImageEditor by mutableStateOf(false) + + // 动态管理器配置状态 + var dynamicSignConfig by mutableStateOf(null) + var isDynamicSignEnabled by mutableStateOf(false) + var dynamicSignSize by mutableStateOf("") + var dynamicSignHash by mutableStateOf("") + var showDynamicSignDialog by mutableStateOf(false) + + + // 各种设置开关状态 + var isSimpleMode by mutableStateOf(prefs.getBoolean("is_simple_mode", false)) + var isHideVersion by mutableStateOf(prefs.getBoolean("is_hide_version", false)) + var isHideOtherInfo by mutableStateOf(prefs.getBoolean("is_hide_other_info", false)) + var isShowKpmInfo by mutableStateOf(prefs.getBoolean("show_kpm_info", false)) + var isHideZygiskImplement by mutableStateOf(prefs.getBoolean("is_hide_zygisk_Implement", false)) + var isHideSusfsStatus by mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false)) + var isHideLinkCard by mutableStateOf(prefs.getBoolean("is_hide_link_card", false)) + var isHideTagRow by mutableStateOf(prefs.getBoolean("is_hide_tag_row", false)) + var isKernelSimpleMode by mutableStateOf(prefs.getBoolean("is_kernel_simple_mode", false)) + var showMoreModuleInfo by mutableStateOf(prefs.getBoolean("show_more_module_info", false)) + var useAltIcon by mutableStateOf(prefs.getBoolean("use_alt_icon", false)) + + // SELinux状态 + var selinuxEnabled by mutableStateOf(false) + + // 卡片配置状态 + var cardAlpha by mutableFloatStateOf(CardConfig.cardAlpha) + var cardDim by mutableFloatStateOf(CardConfig.cardDim) + var isCustomBackgroundEnabled by mutableStateOf(ThemeConfig.customBackgroundUri != null) + + // 图片选择状态 + var selectedImageUri by mutableStateOf(null) + + // DPI 设置 + val systemDpi = context.resources.displayMetrics.densityDpi + var currentDpi by mutableIntStateOf(prefs.getInt("app_dpi", systemDpi)) + var tempDpi by mutableIntStateOf(currentDpi) + var isDpiCustom by mutableStateOf(true) + + // 主题模式选项 + val themeOptions = listOf( + context.getString(R.string.theme_follow_system), + context.getString(R.string.theme_light), + context.getString(R.string.theme_dark) + ) + + // 预设 DPI 选项 + val dpiPresets = mapOf( + context.getString(R.string.dpi_size_small) to 240, + context.getString(R.string.dpi_size_medium) to 320, + context.getString(R.string.dpi_size_large) to 420, + context.getString(R.string.dpi_size_extra_large) to 560 + ) +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt new file mode 100644 index 0000000..f383ec9 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt @@ -0,0 +1,154 @@ +package zako.zako.zako.zakoui.screen.moreSettings.util + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.net.Uri +import android.os.Build +import android.provider.Settings +import java.util.* + +object LocaleHelper { + + /** + * Check if should use system language settings (Android 13+) + */ + val useSystemLanguageSettings: Boolean + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + + /** + * Launch system app locale settings (Android 13+) + */ + fun launchSystemLanguageSettings(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + try { + val intent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + context.startActivity(intent) + } catch (_: Exception) { + // Fallback to app language settings if system settings not available + } + } + } + + /** + * Apply saved language setting to context (for Android < 13) + */ + fun applyLanguage(context: Context): Context { + // On Android 13+, language is handled by system + if (useSystemLanguageSettings) { + return context + } + + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val localeTag = prefs.getString("app_locale", "system") ?: "system" + + return if (localeTag == "system") { + context + } else { + val locale = parseLocaleTag(localeTag) + setLocale(context, locale) + } + } + + /** + * Set locale for context (Android < 13) + */ + @SuppressLint("ObsoleteSdkInt") + private fun setLocale(context: Context, locale: Locale): Context { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + updateResources(context, locale) + } else { + updateResourcesLegacy(context, locale) + } + } + + @SuppressLint("UseRequiresApi", "ObsoleteSdkInt") + @TargetApi(Build.VERSION_CODES.N) + private fun updateResources(context: Context, locale: Locale): Context { + val configuration = Configuration() + configuration.setLocale(locale) + configuration.setLayoutDirection(locale) + return context.createConfigurationContext(configuration) + } + + @Suppress("DEPRECATION") + @SuppressWarnings("deprecation") + private fun updateResourcesLegacy(context: Context, locale: Locale): Context { + Locale.setDefault(locale) + val resources = context.resources + val configuration = resources.configuration + configuration.locale = locale + configuration.setLayoutDirection(locale) + resources.updateConfiguration(configuration, resources.displayMetrics) + return context + } + + /** + * Parse locale tag to Locale object + */ + private fun parseLocaleTag(tag: String): Locale { + return try { + if (tag.contains("_")) { + val parts = tag.split("_") + Locale.Builder() + .setLanguage(parts[0]) + .setRegion(parts.getOrNull(1) ?: "") + .build() + } else { + Locale.Builder() + .setLanguage(tag) + .build() + } + } catch (_: Exception) { + Locale.getDefault() + } + } + + /** + * Restart activity to apply language change (Android < 13) + */ + fun restartActivity(context: Context) { + if (context is Activity && !useSystemLanguageSettings) { + context.recreate() + } + } + + /** + * Get current app locale + */ + @SuppressLint("ObsoleteSdkInt") + fun getCurrentAppLocale(context: Context): Locale? { + return if (useSystemLanguageSettings) { + // Android 13+ - get from system app locale settings + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + try { + val localeManager = context.getSystemService(Context.LOCALE_SERVICE) as? android.app.LocaleManager + val locales = localeManager?.applicationLocales + if (locales != null && !locales.isEmpty) { + locales.get(0) + } else { + null // System default + } + } catch (_: Exception) { + null // System default + } + } else { + null // System default + } + } else { + // Android < 13 - get from SharedPreferences + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val localeTag = prefs.getString("app_locale", "system") ?: "system" + if (localeTag == "system") { + null // System default + } else { + parseLocaleTag(localeTag) + } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt new file mode 100644 index 0000000..8824505 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt @@ -0,0 +1,27 @@ +package zako.zako.zako.zakoui.screen.moreSettings.util + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import com.sukisu.ultra.ui.MainActivity + +/** + * 刷新启动器图标 + */ +fun toggleLauncherIcon(context: Context, useAlt: Boolean) { + val pm = context.packageManager + val main = ComponentName(context, MainActivity::class.java.name) + val alias = ComponentName(context, "${MainActivity::class.java.name}Alias") + + pm.setComponentEnabledSetting( + if (useAlt) alias else main, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ) + + pm.setComponentEnabledSetting( + if (useAlt) main else alias, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) +} \ No newline at end of file diff --git a/manager/app/src/main/jniLibs/.gitignore b/manager/app/src/main/jniLibs/.gitignore index 05ffdc0..939b930 100644 --- a/manager/app/src/main/jniLibs/.gitignore +++ b/manager/app/src/main/jniLibs/.gitignore @@ -1,5 +1,8 @@ -libzakozako.so -libzakozakozako.so -libkpmmgr.so -libzako.so -libandroidx.graphics.path.so \ No newline at end of file +libksud.so +libkernelsu.so +libsusfsd.so +libuid_scanner.so +libzakosign.so +libandroidx.graphics.path.so +libmmrl-file-manager.so +libmmrl-kernelsu.so diff --git a/manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so b/manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so new file mode 100644 index 0000000..dbb0b6b Binary files /dev/null and b/manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so differ diff --git a/manager/app/src/main/jniLibs/arm64-v8a/libzakoboot.so b/manager/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so similarity index 100% rename from manager/app/src/main/jniLibs/arm64-v8a/libzakoboot.so rename to manager/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so diff --git a/manager/app/src/main/jniLibs/arm64-v8a/libzakosign.so b/manager/app/src/main/jniLibs/arm64-v8a/libzakosign.so deleted file mode 100644 index 6669436..0000000 Binary files a/manager/app/src/main/jniLibs/arm64-v8a/libzakosign.so and /dev/null differ diff --git a/manager/app/src/main/jniLibs/armeabi-v7a/libzakoboot.so b/manager/app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so similarity index 100% rename from manager/app/src/main/jniLibs/armeabi-v7a/libzakoboot.so rename to manager/app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so diff --git a/manager/app/src/main/jniLibs/armeabi-v7a/libzakosign.so b/manager/app/src/main/jniLibs/armeabi-v7a/libzakosign.so deleted file mode 100644 index 72954b6..0000000 Binary files a/manager/app/src/main/jniLibs/armeabi-v7a/libzakosign.so and /dev/null differ diff --git a/manager/app/src/main/jniLibs/x86_64/libzakoboot.so b/manager/app/src/main/jniLibs/x86_64/libmagiskboot.so similarity index 100% rename from manager/app/src/main/jniLibs/x86_64/libzakoboot.so rename to manager/app/src/main/jniLibs/x86_64/libmagiskboot.so diff --git a/manager/app/src/main/res/values-ar/strings.xml b/manager/app/src/main/res/values-ar/strings.xml index 9f3ce80..542a256 100644 --- a/manager/app/src/main/res/values-ar/strings.xml +++ b/manager/app/src/main/res/values-ar/strings.xml @@ -63,7 +63,6 @@ إصدار KernelSU الحالي %s منخفض جدًا بحيث لا يعمل المدير بشكل صحيح. الرجاء الترقية إلى الإصدار %s أو أعلى! الغاء تحميل الإضافات بشكل افتراضي القيمة الافتراضية العامة لـ\"إلغاء تحميل الإضافات\" في ملفات تعريف التطبيقات. إذا تم تمكينه، إزالة جميع تعديلات الإضافات على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف. - تعطيل روابط kprobe سيسمح تمكين هذا الخيار لـKernelSU باستعادة أي ملفات معدلة بواسطة الإضافات لهذا التطبيق. المجال القواعد @@ -127,7 +126,6 @@ LKM المحددة: %s حفظ السجلات السجلات محفوظة - وضع SuS SU تأكيد وحدة التثبيت %1$s؟ وحدة غير معروفة @@ -235,7 +233,6 @@ نوع الملف غير صحيح! الرجاء تحديد ملف .kpm. إلغاء التثبيت سيتم إلغاء تثبيت KPM التالية: %s - تعطيل روابط kprobe التي أنشأتها KernelSU، باستخدام الروابط الواردة بدلاً من ذلك، والتي تشبه طريقة الربط غير GKI غير GKI. استخدم إصبعين لتكبير الصورة، وأصبع واحد لسحبها لضبط الموضع إعادة @@ -278,8 +275,6 @@ إعدادات متقدمة تخصيص شريط الأدوات عد مرة أخرى - تم تمكين SuSFS - تم تعطيل SuSFS تم تعيين الخلفية بنجاح إزالة خلفيات مخصصة Alternate icon @@ -309,9 +304,8 @@ يحتاج التطبيق إلى إعادة تشغيل لتطبيق الإعدادات الجديدة لإدارة شؤون الإعلام، ولا يؤثر على شريط حالة النظام أو التطبيقات الأخرى تم تعيين DPI إلى %1$d، فعلي بعد إعادة تشغيل التطبيق - لغة التطبيق - اتبع النظام - تم تغيير اللغة، إعادة التشغيل لتطبيق التغييرات + لغة التطبيق + اتبع النظام تعديل ظلام البطاقة رمز الخطأ diff --git a/manager/app/src/main/res/values-az/strings.xml b/manager/app/src/main/res/values-az/strings.xml index 66a4548..d90028d 100644 --- a/manager/app/src/main/res/values-az/strings.xml +++ b/manager/app/src/main/res/values-az/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Defolt olaraq modulları umount et Tətbiq Profillərində \"Umount modulları\" üçün qlobal standart dəyər. Aktivləşdirilərsə, o, Profil dəsti olmayan proqramlar üçün sistemdəki bütün modul dəyişikliklərini siləcək. - Disable kprobe hooks Bu seçimi aktivləşdirmək KernelSU-ya bu proqram üçün modullar tərəfindən hər hansı dəyişdirilmiş faylları bərpa etməyə imkan verəcək. Domen Qaydalar @@ -125,7 +124,6 @@ Selected LKM: %s Girişləri Saxla Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-bs/strings.xml b/manager/app/src/main/res/values-bs/strings.xml index 18808f7..da5e829 100644 --- a/manager/app/src/main/res/values-bs/strings.xml +++ b/manager/app/src/main/res/values-bs/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Umount module po zadanom Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil. - Disable kprobe hooks Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju. Domena Pravila @@ -125,7 +124,6 @@ Selected LKM: %s Sačuvaj Dnevnike Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-da/strings.xml b/manager/app/src/main/res/values-da/strings.xml index 2c2d8b0..3c3221a 100644 --- a/manager/app/src/main/res/values-da/strings.xml +++ b/manager/app/src/main/res/values-da/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Afmontere moduler som standard Den globale standard værdi for \"Afmonter moduler\" i App Profiler. Hvis aktiveret vil den fjerne alle modulers modifikationer til system applikationerne der ikke har en sat Profil. - Disable kprobe hooks Aktivering af denne indstilling vil tillade KernelSU at gendanne hvilken som helst modificeret filer af modulet for denne applikation. Domæne Regler @@ -125,7 +124,6 @@ Selected LKM: %s Gem Logfiler Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-de/strings.xml b/manager/app/src/main/res/values-de/strings.xml index b27aea4..b9d848a 100644 --- a/manager/app/src/main/res/values-de/strings.xml +++ b/manager/app/src/main/res/values-de/strings.xml @@ -63,7 +63,6 @@ Die aktuelle KernelSU-Version %s ist zu alt für diese Manager-Version. Bitte auf Version %s oder höher aktualisieren! Module standardmäßig aushängen Globaler Standardwert für \"Module aushängen\" im App-Profil. Falls er aktiviert ist, werden alle Moduländerungen im System für alle Apps entfernt, für die kein Profil festgelegt ist. - Kprobe-Hooks deaktivieren Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen. Domäne Regeln @@ -127,7 +126,6 @@ Wähle LKM: %s Protokolle Speichern Protokolle gespeichert - SuS SU-Modus: das Installationsmodul %1$s bestätigen ? unbekannter Modul @@ -235,7 +233,6 @@ Falscher Dateityp! Bitte wählen Sie eine .kpm Datei. Deinstallieren Folgende KPM wird deinstalliert: %s - Deaktiviere kprobe Hooks die von KernelSU erstellt wurden und stattdessen inline Hooks verwenden, was der Nicht-GKI-Kernel-Hooking Methode ähnlich ist. Verwende zwei Finger um das Bild zu vergrößern und einen Finger um die Position anzupassen Rückzahlung @@ -278,8 +275,6 @@ Erweiterte Einstellungen Passt die Symbolleiste an. Comeback - SuSFS aktiviert - SuSFS deaktiviert Hintergrund erfolgreich gesetzt Eigene Hintergründe entfernt Alternatives Symbol @@ -309,9 +304,8 @@ Die Anwendung muss neu gestartet werden, um die neuen DPI-Einstellungen zu übernehmen, hat keine Auswirkungen auf die System-Statusleiste oder andere Anwendungen DPI wurde auf %1$dgesetzt, wirksam nach dem Neustart der Anwendung - App Sprache - Folge Systemeinstellung - Sprache geändert, Neustart um Änderungen zu übernehmen + App Sprache + Folge Systemeinstellung Kartenfinsternis Anpassung fehlercode diff --git a/manager/app/src/main/res/values-es/strings.xml b/manager/app/src/main/res/values-es/strings.xml index 967e6fc..451ee77 100644 --- a/manager/app/src/main/res/values-es/strings.xml +++ b/manager/app/src/main/res/values-es/strings.xml @@ -63,7 +63,6 @@ La versión %s actual de KernelSU es demasiado baja para que el gestor funcione correctamente. Por favor, ¡actualice a la versión %s o superior! Desmontar módulos por defecto El valor global predeterminado para \"Umount modules\" en App Profile. Si está activado, eliminará todas las modificaciones de módulos del sistema para las apps que no tengan un perfil establecido. - Desactivar kprobe hooks Activar esta opción permitirá a KernelSU restaurar cualquier archivo modificado por los módulos para esta aplicación. Dominio Reglas @@ -125,7 +124,6 @@ LKM seleccionado: %s Guardar registros Registro guardado - Modo SuS SU: ¿confirmar la instalación del módulo %1$s? módulo desconocido @@ -233,7 +231,6 @@ ¡Tipo de archivo incorrecto! Por favor seleccione el archivo .kpm. Desinstalar El siguiente KPM será desinstalado: %s - Deshabilita los ganchos kprobe creados por KernelSU, usando ganchos en línea en su lugar, que es similar al método de enganche del núcleo no GKI. Usa dos dedos para acercar la imagen, y un dedo para arrastrarla para ajustar la posición Reaprovisionamiento @@ -276,8 +273,6 @@ Configuraciones avanzadas Personalizar la barra de herramientas. Retorno - SuSFS activado - SuSFS desactivado Fondo establecido correctamente Eliminar fondo personalizado Icono alternativo @@ -307,9 +302,8 @@ La aplicación necesita reiniciarse para aplicar la nueva configuración DPI, no afecta a la barra de estado del sistema u otras aplicaciones DPI ha sido establecido a %1$d, efectivo después de reiniciar la aplicación - Idioma de la aplicación - Seguir sistema - Idioma cambiado, reiniciando para aplicar cambios + Idioma de la aplicación + Seguir sistema Ajuste de oscuridad de tarjeta código de error diff --git a/manager/app/src/main/res/values-et/strings.xml b/manager/app/src/main/res/values-et/strings.xml index f3bc289..7c8640a 100644 --- a/manager/app/src/main/res/values-et/strings.xml +++ b/manager/app/src/main/res/values-et/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Haagi moodulid vaikimisi lahti Globaalne vaikeväärtus \"Lahtihaagitud moodulitele\" rakenduseprofiilis. Lubamisel eemaldab see kõik moodulite süsteemimuudatused rakendustele, millel ei ole profiili määratud. - Disable kprobe hooks Selle valiku lubamine lubab KernelSU-l taastada selle rakenduse moodulite poolt mistahes muudetud faile. Domeen Reeglid @@ -125,7 +124,6 @@ Valitud LKM: %s Salvesta Logid Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-fa/strings.xml b/manager/app/src/main/res/values-fa/strings.xml index 760f4b9..e142416 100644 --- a/manager/app/src/main/res/values-fa/strings.xml +++ b/manager/app/src/main/res/values-fa/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Umount modules by default The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set. - Disable kprobe hooks Enabling this option will allow KernelSU to restore any modified files by the modules for this app. Domain Rules @@ -125,7 +124,6 @@ Selected LKM: %s ذخیره گزارش‌ها Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-fil/strings.xml b/manager/app/src/main/res/values-fil/strings.xml index a762616..afcc0b2 100644 --- a/manager/app/src/main/res/values-fil/strings.xml +++ b/manager/app/src/main/res/values-fil/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Umount modules by default Ang pangkalahatang default na halaga para sa \"Umount modules\" sa Mga Profile ng App. Kung pinagana, aalisin nito ang lahat ng mga pagbabago sa modyul sa system para sa mga aplikasyon na walang hanay ng Profile. - Disable kprobe hooks Ang pagpapagana sa opsyong ito ay magbibigay-daan sa KernelSU na ibalik ang anumang binagong file ng mga modyul para sa aplikasyon na ito. Domain Mga Tuntunin @@ -125,7 +124,6 @@ Selected LKM: %s I-save ang mga Log Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index fb87a32..6d46515 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -63,7 +63,6 @@ La version actuelle de KernelSU (%s) est trop ancienne pour que le gestionnaire fonctionne correctement. Veuillez passer à la version %s ou à une version supérieure ! Démonter les modules par défaut Valeur globale par défaut pour l\'option \"Démonter les modules\" dans les profils d\'application. Lorsque l\'option est activée, les modifications apportées au système par les modules sont supprimées pour les applications qui n\'ont pas de profil défini. - Désactiver les crochets kprobe L\'activation de cette option permettra à KernelSU de restaurer tous les fichiers modifiés par les modules pour cette application. Domaine Règles @@ -127,7 +126,6 @@ LKM sélectionné : %s Enregistrer les journaux Journaux enregistrés - Mode Sus confirmer l\'installation du module %1$s? module inconnu @@ -235,7 +233,6 @@ Type de fichier incorrect ! Veuillez sélectionner un fichier .kpm. Désinstaller Le KPM suivant sera désinstallé : %s - Désactivez les crochets kprobe créés par KernelSU, en utilisant des crochets en ligne à la place, ce qui est similaire à la méthode de crochet du noyau non-GKI. Utilisez deux doigts pour zoomer l\'image, et un doigt pour le faire glisser pour ajuster la position Remise à disposition @@ -278,8 +275,6 @@ Paramètres avancés Choisir les boutons à afficher Reviens - SuSFS activé - SuSFS désactivé Fond d\'écran défini avec succès Fond d\'écran personnalisé supprimé Icône alternative @@ -309,9 +304,8 @@ L\'application doit être redémarrée pour appliquer les nouveaux paramètres de DPI, n\'affecte pas la barre d\'état du système ou d\'autres applications Le DPI a été réglé sur %1$d, effectif après le redémarrage de l\'application - Langue de l\'application - Suivre le paramètre système - Langue modifiée, redémarrage pour appliquer les modifications + Langue de l\'application + Suivre le paramètre système Ajustement de l\'obscurité de la carte code d\'erreur diff --git a/manager/app/src/main/res/values-hi/strings.xml b/manager/app/src/main/res/values-hi/strings.xml index 8803d03..cde029c 100644 --- a/manager/app/src/main/res/values-hi/strings.xml +++ b/manager/app/src/main/res/values-hi/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! डिफ़ॉल्ट रूप से मॉड्यूल अनमाउन्ट करें ऐप प्रोफाइल में \"अनमाउंट मॉड्यूल\" के लिए ग्लोबल डिफ़ॉल्ट वैल्यू। यदि चालू किया गया है, तो यह एप्लीकेशंस के लिऐ सिस्टम के सभी मॉड्यूल मोडिफिकेशन को हटा देगा जिनकी प्रोफ़ाइल सेट नहीं है। - Disable kprobe hooks इस विकल्प को चालू करने से KernelSU को इस एप्लिकेशन के लिए मॉड्यूल द्वारा किसी भी मोडिफाइड फ़ाइल को रिस्टोर करें। डोमेन नियम @@ -125,7 +124,6 @@ Selected LKM: %s लॉग सहेजें Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-hr/strings.xml b/manager/app/src/main/res/values-hr/strings.xml index 779dda8..ab68841 100644 --- a/manager/app/src/main/res/values-hr/strings.xml +++ b/manager/app/src/main/res/values-hr/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Umount module po zadanom Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil. - Disable kprobe hooks Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju. Domena Pravila @@ -125,7 +124,6 @@ Selected LKM: %s Spremi Zapise Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index ba073b0..ba3813a 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Modulok leválasztása alapértelmezetten A \"Modulok leválasztása\" globális alapértelmezett értéke az App Profile-ban. Ha engedélyezve van, eltávolít minden modulmódosítást a rendszerből azon alkalmazások esetében, amelyeknek nincs profilja beállítva. - Disable kprobe hooks Ha engedélyezi ezt az opciót, a KernelSU visszaállíthatja az alkalmazás moduljai által módosított fájlokat. Tartomány Szabályok @@ -125,7 +124,6 @@ Kiválasztott LKM: %s Naplók mentése Mentett naplók - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-idn/strings.xml b/manager/app/src/main/res/values-idn/strings.xml new file mode 100644 index 0000000..3dbe03e --- /dev/null +++ b/manager/app/src/main/res/values-idn/strings.xml @@ -0,0 +1,537 @@ + + + Beranda + Tidak Terpasang + Klik untuk Memasang + Berfungsi + Versi: %s + Tidak Didukung + Driver KernelSU tidak terdeteksi di kernel Anda. Mungkin Anda menggunakan kernel yang salah. + Versi Kernel + Versi SuSFS + Versi Manajer + Status SELinux + Dinonaktifkan + Ditegakkan + Permisi + Tidak Diketahui + Superuser + Gagal mengaktifkan modul: %s + Gagal menonaktifkan modul: %s + Tidak ada modul terpasang + Modul + Urutkan (Aksi Terlebih Dahulu) + Urutkan (Aktif Terlebih Dahulu) + Copot Pemasangan + Pasang + Pasang + Muat Ulang + Pengaturan + Muat Ulang Lunak + Muat Ulang ke Recovery + Muat Ulang ke Bootloader + Muat Ulang ke Mode Download + Muat Ulang ke Mode EDL + Tentang + Apakah Anda yakin ingin mencopot pemasangan modul %s? + %s telah dicopot + Gagal mencopot pemasangan: %s + Versi + Penulis + Segarkan + Tampilkan Aplikasi Sistem + Sembunyikan Aplikasi Sistem + Kirim Log + Mode Aman + Muat ulang untuk menerapkan + Modul tidak tersedia karena konflik dengan Magisk! + Pelajari tentang KernelSU + https://kernelsu.org/guide/what-is-kernelsu.html + Pelajari cara memasang KernelSU dan menggunakan modul + Dukung Kami + KernelSU bersifat gratis dan open source, sekarang dan selamanya. Namun, Anda dapat menunjukkan dukungan Anda dengan melakukan donasi. + Gabung ke saluran %2$s kami]]> + Profil Aplikasi + Bawaan + Templat + Khusus + Nama Profil + Grup + Kemampuan + Konteks SELinux + Lepas Kait Modul + Gagal memperbarui profil aplikasi untuk %s + Versi KernelSU saat ini %s terlalu rendah untuk menjalankan manajer dengan benar. Harap perbarui ke versi %s atau yang lebih tinggi! + Lepas kait modul secara bawaan + Nilai bawaan global untuk \"Lepas Kait Modul\" dalam profil aplikasi. Jika diaktifkan, ini akan menghapus semua perubahan sistem yang dibuat oleh modul untuk aplikasi tanpa profil yang ditetapkan. + Mengaktifkan opsi ini akan memungkinkan KernelSU untuk memulihkan file yang diubah oleh modul untuk aplikasi ini. + Domain + Aturan + Perbarui + Mengunduh modul: %s + Memulai pengunduhan: %s + Versi baru %s tersedia, klik untuk memperbarui. + Jalankan + Paksa Hentikan + Jalankan Ulang + Gagal memperbarui aturan SELinux untuk %s + Catatan Perubahan + Templat Profil Aplikasi + Kelola templat profil aplikasi lokal dan daring + Buat Templat + Edit Templat + ID + ID Templat Tidak Valid + Nama + Deskripsi + Simpan + Hapus + Lihat Templat + Hanya Baca + ID Templat sudah ada! + Impor/Ekspor + Impor dari Papan Klip + Ekspor ke Papan Klip + Tidak ditemukan templat lokal untuk diekspor! + Berhasil diimpor + Sinkronkan Templat Daring + Gagal menyimpan templat + Papan klip kosong! + Gagal memuat catatan perubahan: %s + Periksa Pembaruan + Secara otomatis memeriksa pembaruan saat membuka aplikasi + Gagal memberikan hak akses root! + Aksi + Tutup + Aktifkan Debug WebView + Dapat digunakan untuk mendebug WebUI. Harap aktifkan hanya jika diperlukan. + Pemasangan Langsung (Disarankan) + Pilih Gambar untuk Dipatch + Pasang ke Slot Tidak Aktif (Setelah OTA) + Perangkat Anda akan **DIPAKSA** untuk boot ke slot tidak aktif saat ini setelah reboot! +Gunakan opsi ini hanya setelah OTA selesai. +Lanjutkan? + Lanjut + Disarankan gambar partisi %1$s + Pilih KMI + Copot Pemasangan + Copot Pemasangan Sementara + Copot Pemasangan Permanen + Pulihkan Gambar Bawaan + Copot pemasangan KernelSU secara sementara, kembalikan ke keadaan awal setelah reboot berikutnya. + Copot pemasangan KernelSU secara lengkap dan permanen (Root dan semua modul). + Pulihkan gambar bawaan pabrik (jika cadangan tersedia), biasanya digunakan sebelum OTA; jika ingin mencopot KernelSU, gunakan \"Copot Pemasangan Permanen\". + Mem-flash + Flash Berhasil + Flash Gagal + LKM Terpilih: %s + Simpan Log + Log Disimpan + + Konfirmasi pemasangan modul %1$s? + modul tidak dikenal + + Konfirmasi Pemulihan Modul + Operasi ini akan menimpa semua modul yang ada. Lanjutkan? + Konfirmasi + Batal + + Pencadangan Berhasil (tar.gz) + Gagal membuat cadangan: %1$s + cadangan modul + pulihkan modul + + Modul berhasil dipulihkan, perlu reboot + Gagal memulihkan: %1$s + Muat Ulang Sekarang + Kesalahan Tidak Diketahui + + Gagal mengeksekusi perintah: %1$s + + Pencadangan daftar izin berhasil + Gagal membuat cadangan daftar izin: %1$s + Konfirmasi Pemulihan Daftar Izin + Operasi ini akan menimpa daftar izin saat ini. Lanjutkan? + Daftar izin berhasil dipulihkan + Gagal memulihkan daftar izin: %1$s + Cadangkan Daftar Izin + Pulihkan Daftar Izin + Latar Belakang Aplikasi Khusus + Pilih gambar sebagai latar belakang + Transparansi Panel Navigasi + Versi Android + Model Perangkat + Pemberian hak superuser untuk %s tidak diizinkan + Nonaktifkan Kompatibilitas su + Sementara mencegah aplikasi mana pun mendapatkan hak root melalui perintah su (proses root yang ada tidak akan terpengaruh). + Apakah Anda yakin ingin memasang %1$d modul berikut? +%2$s + Pengaturan Lainnya + SELinux + Diaktifkan + Dinonaktifkan + Mode Sederhana + Menyembunyikan kartu yang tidak perlu saat diaktifkan + Sembunyikan Versi Kernel + Menyembunyikan versi kernel + Sembunyikan Informasi Lainnya + Menyembunyikan titik merah yang menunjukkan jumlah superuser, modul, dan modul KPM di halaman navigasi bawah + Sembunyikan Status SuSFS + Menyembunyikan informasi status SuSFS di halaman beranda + Sembunyikan Kartu Tautan + Menyembunyikan informasi di kartu tautan di halaman beranda + Sembunyikan Baris Tag Modul + Menyembunyikan label nama folder dan ukuran di kartu modul + Tema + Ikuti Sistem + Terang + Gelap + Hook Manual + Warna Dinamis + Warna dinamis menggunakan tema sistem + Pilih Warna Tema + Biru + Hijau + Ungu + Oranye + Merah Muda + Abu-abu + Kuning + Pasang Anykernel3 + Flash file kernel AnyKernel3 + Diperlukan hak akses root + Pembersihan Selesai + Muat ulang sekarang? + Ya + Tidak + Gagal memuat ulang + KPM + Saat ini tidak ada modul kernel yang terpasang + Versi + Penulis + Copot Pemasangan + Berhasil dicopot + Gagal mencopot + Berhasil memuat modul kpm + Gagal memuat modul kpm + Parameter + Jalankan + Versi KPM + Tutup + Fitur modul kernel berikut dikembangkan oleh KernelPatch dan dimodifikasi untuk menyertakan fitur modul kernel SukiSU Ultra + SukiSU Ultra menantikan + Berhasil + Gagal + Ke depannya, SukiSU Ultra akan menjadi cabang KSU yang relatif independen, tetapi kami tetap berterima kasih kepada KernelSU resmi, MKSU, dan lainnya atas kontribusi mereka! + Tidak Didukung + Didukung + Kernel Belum Di-patch + Kernel Belum Diaktifkan + Pengaturan Khusus + Pemasangan KPM + Muat + Tanamkan + Silakan pilih: %1$s mode pemasangan modul +Muat: Secara sementara memuat modul +Tanamkan: Secara permanen memasang ke sistem + Gagal memeriksa keberadaan file modul + Warna Tema + Jenis file tidak valid! Harap pilih file .kpm. + Copot Pemasangan + Akan mencopot KPM berikut: %s + Gunakan dua jari untuk memperbesar gambar dan satu jari untuk menyeret, untuk menyesuaikan posisi + Provisi Ulang + + Flash Selesai + + Menyiapkan… + Membersihkan file… + Menyalin file… + Mengekstrak alat flash… + Menambal skrip flash… + Mem-flash kernel… + Flash Selesai + + Pilih Slot untuk Flash + Silakan pilih slot target untuk flashing boot + Slot A + Slot B + Slot Terpilih: %1$s + Mendapatkan slot asli + Mengatur slot target + Mengembalikan slot bawaan + Slot sistem bawaan saat ini: %1$s + + Gagal menyalin + Kesalahan Tidak Diketahui + Flash Gagal + + Pemulihan/Pemasangan LKM + Flash AnyKernel3 + Versi Kernel: %1$s + Alat patch yang digunakan: %1$s + Konfigurasi + Pengaturan Aplikasi + Alat + + Aplikasi tidak ditemukan + SELinux diaktifkan + SELinux dinonaktifkan + Gagal mengubah status SELinux + Pengaturan Lanjutan + Sesuaikan Bilah Alat + Kembali + Latar belakang berhasil diatur + Latar belakang khusus dihapus + Ikon Alternatif + Ubah ikon peluncur menjadi ikon KernelSU. + Ikon diubah + + Sembunyikan Fungsi KPM + Menyembunyikan informasi dan fungsi KPM di layar utama dan panel bawah + + Pilih Mesin WebUI untuk Digunakan + Pilih Otomatis + Paksa Gunakan WebUI X + Paksa Gunakan KSU WebUI + Sisipkan Eruda ke WebUI X + Sisipkan konsol debug ke WebUI X untuk memudahkan debugging. Memerlukan debugging web diaktifkan. + + DPI yang Diterapkan + Sesuaikan kepadatan layar hanya untuk aplikasi saat ini + Kecil + Sedang + Besar + Sangat Besar + Khusus + Menerapkan Pengaturan DPI + Konfirmasi Perubahan DPI + Apakah Anda yakin ingin mengubah DPI aplikasi dari %1$d menjadi %2$d? + Aplikasi perlu dijalankan ulang agar pengaturan DPI baru diterapkan; ini tidak akan mempengaruhi bilah status sistem atau aplikasi lainnya + DPI diatur ke %1$d, akan diterapkan setelah aplikasi dijalankan ulang + + Bahasa Aplikasi + Ikuti Sistem + Pengaturan Pencahayaan Kartu + + kode kesalahan + Silakan periksa log + Memasang modul %1$d/%2$d + Gagal memasang %d modul baru + Gagal mengunduh modul + Mem-flash Kernel + + Semua + Root + Khusus + Bawaan + + Nama Naik + Nama Turun + Waktu Pemasangan (Baru) + Waktu Pemasangan (Lama) + Ukuran Turun + Ukuran Naik + Frekuensi Penggunaan + + Tidak ada aplikasi dalam kategori ini + + Tolak Hak Akses + Berikan Hak Akses + Lepas Kaitan Modul + Nonaktifkan Lepas Kaitan Modul + Perluas Menu + Ciutkan Menu + Ke Atas + Ke Bawah + Terpilih + Pilih + + Opsi Menu + Urutkan Berdasarkan + Pilih Jenis Aplikasi + + Konfigurasi SuSFS + Deskripsi Konfigurasi + Fitur ini memungkinkan Anda untuk mengonfigurasi spoofing nilai uname dan waktu build SuSFS. Masukkan nilai yang diinginkan dan klik \"Terapkan\" agar berlaku. + Nilai Uname + Silakan masukkan nilai uname khusus + Spoof Waktu Build + Silakan masukkan nilai spoof waktu build + Nilai Saat Ini: %s + Waktu Build Saat Ini: %s + Atur Ulang ke Bawaan + Terapkan + + Konfirmasi Atur Ulang + + Gagal menemukan file ksu_susfs + Gagal mengeksekusi perintah SuSFS + Kesalahan eksekusi perintah SuSFS: %s + Nilai uname dan waktu build SuSFS berhasil diatur: %s, %s + + Konfigurasi SuSFS + + Mulai Otomatis + Secara otomatis menerapkan semua konfigurasi non-bawaan saat reboot + Perlu menambahkan konfigurasi untuk mengaktifkan + Gagal mengaktifkan mulai otomatis + Gagal menonaktifkan mulai otomatis + Kesalahan konfigurasi mulai otomatis: %s + Tidak ada konfigurasi yang tersedia untuk mulai otomatis + + Pengaturan Dasar + Jalur SUS + Kaitan SUS + Coba Lepas Kait + Pengaturan Jalur + Status Fitur Diaktifkan + + Tambah Jalur SUS + Tambah Kaitan SUS + Tambah Coba Lepas Kait + Jalur SUS berhasil ditambahkan + Kesalahan: Jalur tidak ditemukan + Jalur + Jalur Kaitan + misalnya: /system/addon.d + Tidak ada jalur SUS yang dikonfigurasi + Tidak ada kaitan SUS yang dikonfigurasi + Tidak ada coba lepas kait yang dikonfigurasi + + Mode Lepas Kait + Lepas Kait Normal (0) + Lepas Kait Terpisah (1) + Normal + Terpisah + Mode: %1$s (%2$s) + Jalur coba lepas kait berhasil ditambahkan: %s + Berhasil menyimpan jalur coba lepas kait: %s + + + Atur Ulang Jalur SUS + Ini akan menghapus semua konfigurasi jalur SUS. Apakah Anda yakin ingin melanjutkan? + Atur Ulang Kaitan SUS + Ini akan menghapus semua konfigurasi kaitan SUS. Apakah Anda yakin ingin melanjutkan? + Atur Ulang Coba Lepas Kait + Ini akan menghapus semua konfigurasi coba lepas kait. Apakah Anda yakin ingin melanjutkan? + Atur Ulang Pengaturan Jalur + + Jalur Data Android + Jalur Kartu SD + Atur Jalur Data Android + Atur Jalur Kartu SD + + Menampilkan status saat ini dari fitur SuSFS yang diaktifkan + Informasi status fitur tidak ditemukan + Diaktifkan + Dinonaktifkan + + Dukungan Jalur SUS + Dukungan Kaitan SUS + Dukungan Coba Lepas Kait + Dukungan Spoof Uname + Spoof Cmdline/Bootconfig + Dukungan Open Redirect + Dukungan Logging + Kaitan Bawaan Otomatis + Kaitan Bind Otomatis + Coba Lepas Kaitan Bind Otomatis + Sembunyikan Simbol KSU SUSFS + Dukungan SUS Kstat + Fitur Toggle Mode SUS SU + + Fitur SuSFS yang Dapat Dikonfigurasi + Aktifkan Log SuSFS + Aktifkan atau nonaktifkan logging untuk SuSFS + Pengaturan Logging SuSFS + Mengaktifkan Logging SuSFS + Menonaktifkan Logging SuSFS + Perbarui JSON + URL Perbarui JSON disalin ke papan klip + + Tampilkan Informasi Modul Lebih Banyak + Tampilkan informasi modul tambahan seperti URL perbarui JSON + Lokasi Eksekusi + Lokasi Eksekusi Saat Ini: %s + Layanan + Post-FS-Data + Jalankan setelah layanan sistem dimulai + Jalankan setelah sistem file dikaitkan tetapi sebelum sistem sepenuhnya dinyalakan. Dapat menyebabkan bootloop + Informasi Slot + Lihat informasi slot boot saat ini dan salin nilainya + Slot Aktif Saat Ini: %s + Uname: %s + Waktu Build: %s + Saat Ini + Gunakan Uname + Gunakan Waktu Build + Gagal mendapatkan informasi slot + + Modul mulai otomatis SuSFS diaktifkan, jalur modul: %s + Modul mulai otomatis SuSFS dinonaktifkan + + Konfigurasi Kstat + Konfigurasi Kstat statis ditambahkan: %1$s + Konfigurasi Kstat dihapus: %1$s + Jalur Kstat ditambahkan: %1$s + Jalur Kstat dihapus: %1$s + Kstat diperbarui: %1$s + Klon Lengkap Kstat diperbarui: %1$s + Tambahkan Konfigurasi Kstat Statis + Jalur File/Direktori + Petunjuk: Anda dapat menggunakan \"default\" untuk menggunakan nilai asli + Tambah Jalur Kstat + Tambah + Atur Ulang Konfigurasi Kstat + Apakah Anda yakin ingin membersihkan semua konfigurasi Kstat? Tindakan ini tidak dapat dibatalkan. + Deskripsi Konfigurasi Kstat + • add_sus_kstat_statically: Informasi file/direktori statis + • add_sus_kstat: Tambahkan jalur sebelum bind mount, menjaga informasi asli + • update_sus_kstat: Perbarui ino target, membiarkan ukuran dan blok tidak berubah + • update_sus_kstat_full_clone: Perbarui hanya ino, membiarkan nilai asli lainnya + Konfigurasi Kstat Statis + Manajemen Jalur Kstat + Belum ada konfigurasi Kstat, klik tombol di atas untuk menambahkan + + Kontrol Penyembunyian Kaitan SUS + Kontrol perilaku penyembunyian kaitan SUS untuk proses + Sembunyikan Kaitan SUS untuk Semua Proses + Jika diaktifkan, kaitan SUS akan disembunyikan dari semua proses, termasuk proses KSU + Jika dinonaktifkan, kaitan SUS akan disembunyikan hanya dari proses non-KSU; proses KSU akan dapat melihat kaitan + Mengaktifkan penyembunyian kaitan SUS untuk semua proses + Menonaktifkan penyembunyian kaitan SUS untuk semua proses + Disarankan untuk mengatur ke nonaktif setelah layar terbuka atau pada tahap service.sh atau boot-completed.sh, karena ini seharusnya memperbaiki masalah dengan beberapa aplikasi root yang bergantung pada kaitan yang dibuat oleh proses KSU + Pengaturan Saat Ini: %s + Sembunyikan untuk Semua Proses + Sembunyikan Hanya untuk Proses Non-KSU + Mode Versi Kernel Sederhana + Aktifkan atau nonaktifkan tampilan versi kernel SukiSU sederhana + Jalur Data Android diatur ke: %s + Jalur Kartu SD diatur ke: %s + Pengaturan jalur mungkin tidak sepenuhnya berhasil, tetapi jalur SUS akan tetap ditambahkan + + Cadangan + Buat cadangan semua konfigurasi SuSFS. File cadangan akan menyertakan semua pengaturan, jalur, dan konfigurasi. + Buat Cadangan + Berhasil membuat cadangan: %s + Gagal membuat cadangan: %s + File cadangan tidak ditemukan + Format file cadangan tidak valid + Versi cadangan tidak cocok, tetapi akan dicoba untuk dipulihkan + Pulihkan + Pulihkan konfigurasi SuSFS dari file cadangan. Ini akan menimpa semua pengaturan saat ini. + Pilih File Cadangan + Konfigurasi berhasil dipulihkan dari cadangan yang dibuat %s pada perangkat: %s + Gagal memulihkan: %s + Konfirmasi Pemulihan + Ini akan menimpa semua konfigurasi SuSFS saat ini. Apakah Anda yakin ingin melanjutkan? + Pulihkan + Tanggal Cadangan: %s + Perangkat: %s + Versi: %s + Status Terkunci + Timpa properti status bootloader dalam layanan late_start + Bersihkan Sisa-sisa + Bersihkan file dan direktori sisa dari berbagai modul dan alat (dapat menyebabkan penghapusan yang tidak disengaja, kehilangan data, dan gagal boot, gunakan dengan hati-hati) + diff --git a/manager/app/src/main/res/values-in/strings.xml b/manager/app/src/main/res/values-in/strings.xml index 87b07b0..64027ac 100644 --- a/manager/app/src/main/res/values-in/strings.xml +++ b/manager/app/src/main/res/values-in/strings.xml @@ -63,7 +63,6 @@ Versi KernelSU saat ini %s terlalu rendah untuk menjalankan manager dengan baik. Harap tingkatkan ke versi %s atau yang lebih tinggi! Melepas Modul secara bawaan Menggunakan \"Umount Modul\" secara universal pada Profil Aplikasi. Jika diaktifkan, akan menghapus semua modifikasi sistem untuk aplikasi yang tidak memiliki set profil. - Nonaktifkan kprobe hooks Aktifkan opsi ini agar KernelSU dapat memulihkan kembali berkas termodifikasi oleh modul pada aplikasi ini. Domain Aturan @@ -112,8 +111,9 @@ \nHANYA gunakan setelah proses OTA selesai. \nLanjutkan? Selanjutnya + Gunakan berkas LKM lokal + Hanya berkas .ko yang didukung %1$s image partisi terekomendasi - (tidak stabil) Pilih KMI Hapus Hapus sementara @@ -128,7 +128,6 @@ LKM dipilih: %s Simpan Log Log disimpan - Mode SuS SU: konfirmasi pemasangan modul %1$s? module tidak dikenal @@ -166,6 +165,13 @@ Memberikan hak superuser kepada %s tidak diizinkan Nonaktifkan kompatibilitas SU Nonaktifkan sementara kemampuan aplikasi untuk mendapatkan hak akses root melalui perintah ⁠su (proses root yang sedang berjalan tidak akan terpengaruh) + Nonaktifkan pelepasan (unmount) kernel + Nonaktifkan perilaku unmount pada level kernel yang digunakan oleh KernelSU. + Aktifkan keamanan yang ditingkatkan + Aktifkan kebijakan keamanan yang lebih ketat. + Bawaan + Aktifkan sementara + Aktifkan secara permanen Apakah Anda yakin ingin menginstal %1$d modul berikut?\n\n%2$s Setelan lainnya Selinux @@ -240,7 +246,6 @@ Format file tidak sesuai. Silakan pilih file dengan format .kpm. Menghapus instalan KPM berikut akan diuninstall: %s - Nonaktifkan kprobe hooks yang dibuat oleh KernelSU, gunakan inline hooks sebagai gantinya (metode ini mirip dengan hooking kernel non-GKI). Gunakan dua jari untuk memperbesar gambar, dan satu jari untuk menggeser mengatur posisi Reprovisi @@ -283,8 +288,6 @@ Pengaturan Lanjutan Kustomisasi toolbar Kembali - SuSFS dinyalakan - SuSFS dimatikan Set latar belakang berhasil Latar belakang khusus yang dihapus Ubah ikon @@ -314,9 +317,8 @@ Aplikasi membutuhkan restar untuk menerapkan opsi DPI ini, perubahan ini tidak mengganggu DPI sistem DPI telah di rubah ke %1$d, efektif setelah aplikasi di restar - Bahasa Aplikasi - Mengikuti sistem - Bahasa dirubah, mulai ulang aplikasi untuk menerapkan + Bahasa Aplikasi + Mengikuti sistem Penyesuaian Kegelapan Kartu Kode error @@ -413,8 +415,6 @@ Jalur coba umount berhasil ditambahkan: %s Jalur coba umount berhasil disimpan: %s - Konfirmasi Jalankan Coba Umount - Ini akan segera mengeksekusi semua operasi umount yang dikonfigurasi. Apakah Anda yakin ingin melanjutkan? Setel Ulang Jalur SUS Ini akan menghapus semua konfigurasi jalur SUS. Apakah Anda yakin ingin melanjutkan? @@ -445,7 +445,6 @@ Pemasangan Bind Otomatis Coba Umount Bind Mount Otomatis Sembunyikan Simbol KSU SUSFS - Dukungan Pemasangan Ajaib Dukungan SUS Kstat Fungsi pengalihan mode SUS SU @@ -513,7 +512,6 @@ Pengaturan saat ini: %s Sembunyikan untuk semua proses Sembunyikan hanya untuk proses non-KSU - Jalankan Mode Ringkas Versi Kernel Aktifkan atau nonaktifkan mode bersih yang ditampilkan oleh versi kernel SukiSU Jalur Data Android telah diatur ke: %s @@ -564,6 +562,8 @@ Lainnya Aplikasi Tambahkan Jalur Aplikasi + Versi pustaka SuSFS tidak cocok, kernel: %1$s vs manajer: %2$s. Disarankan untuk memperbarui kernel atau manajer + Peringatan Cari Aplikasi %1$d aplikasi dipilih %1$d aplikasi sudah ditambahkan @@ -604,9 +604,16 @@ Jalur Loop Tambahkan Jalur Loop - Jalur Loop SUS Konfigurasi Jalur Loop Jalur loop ditandai ulang sebagai SUS_PATH pada setiap startup aplikasi pengguna non-root atau layanan terisolasi. Ini membantu mengatasi masalah di mana jalur yang ditambahkan mungkin memiliki status inode direset atau inode dibuat ulang di kernel. + Palsukan log AVC + Palsukan log AVC telah diaktifkan + Palsukan log AVC telah dinonaktifkan + Dinonaktifkan: Nonaktifkan pemalsuan sus tcontext dari \'su\' yang ditampilkan di avc log di kernel\n +Diaktifkan: Aktifkan pemalsuan sus tcontext dari \'su\' dengan \'kernel\' yang ditampilkan di avc log in kernel + Catatan Penting:\n +- Secara default pada kernel nilai ini disetel ke \'0\'\n +- Mengaktifkan ini terkadang membuat pengembang lebih sulit mengidentifikasi penyebab saat melakukan debugging terkait izin atau masalah SELinux, sehingga disarankan agar pengguna menonaktifkannya saat sedang melakukan debugging Tervalidasi Tanda tangan modul tervalidasi @@ -615,4 +622,76 @@ Penerbit tidak dikenal Modul yang tidak ditandatangani mungkin tidak lengkap. Untuk melindungi perangkat Anda, pemasangan modul ini diblokir. Modul yang tidak ditandatangani mungkin tidak lengkap. Apakah Anda ingin mengizinkan modul berikut dari penerbit tidak dikenal untuk dipasang di perangkat ini? + Jenis hook + + Patch KPM + Untuk menambahkan fitur KPM tambahan + Patch KPM + Terapkan patch KPM ke image kernel sebelum melakukan flashing + Batalkan Patch KPM + Batalkan patch KPM yang telah diterapkan sebelumnya + Patch KPM aktif + Pembatalan patch KPM diaktifkan + Mode Patch KPM + Mode Pembatalan Patch KPM + + Sedang menyiapkan Alat KPM + Menerapkan patch KPM + Membatalkan patch KPM + Menemukan berkas Image: %s + KPM berhasil diterapkan + Patch KPM berhasil dibatalkan + File berhasil direpack + + Gagal mengekstrak berkas zip + Berkas Image tidak ditemukan + Patch KPM gagal + Pembatalan patch KPM gagal + Operasi patch KPM gagal: %s + + Ikuti kernel + Gunakan kernel apa adanya tanpa perubahan dari KPM + + Daftar aplikasi pemindaian pada mode pengguna + Mengaktifkan opsi ini akan menggunakan pemindaian mode pengguna untuk daftar aplikasi, sehingga meningkatkan kestabilan. (Jika Anda mengalami masalah seperti hang saat kernel memindai daftar aplikasi, Anda dapat mencoba mengaktifkan opsi ini.) + Pemindaian Aplikasi Multi-Pengguna + Ketika diaktifkan, fitur ini akan memindai aplikasi untuk semua pengguna, termasuk profil kerja + Gagal mengatur, silakan periksa perizinan + Bersihkan Lingkungan Runtime + Bersihkan berkas runtime dan hentikan layanan pemindai + Apakah Anda yakin ingin membersihkan lingkungan runtime? Tindakan ini akan menghentikan layanan pemindai dan menghapus berkas yang terkait. + Lingkungan runtime berhasil dibersihkan + Gagal membersihkan lingkungan runtime + + Konfirmasi Instalasi + Konfirmasi Instalasi (Berkas %d) + Instal + Modul + Kernel + Tidak diketahui + Kernel tidak diketahui + Berkas tidak diketahui + Versi + Pembuat + Deskripsi + Perangkat yang didukung + + Peta SUS + Jalur Pustaka + /data/adb/modules/my_module/zygisk/arm64-v8a.so + Tambahkan Peta SUS + Sunting Peta SUS + Peta SUS berhasil ditambahkan: %1$s + Peta SUS telah dihapus: %1$s + Peta SUS telah diperbarui: %1$s -> %2$s + Tidak ada peta SUS yang dikonfigurasi + Atur ulang Peta SUS + Tindakan ini akan menghapus semua peta SUS yang telah dikonfigurasi. Tindakan ini tidak dapat dibatalkan. + Penyembunyian Peta Memori + Sembunyikan berkas nyata yang di-mmapped dari berbagai peta di /proc/self/ + + Cari + Bersihkan Log + Apakah Anda yakin ingin mengosongkan berkas log yang dipilih? Tindakan ini tidak dapat dibatalkan. + diff --git a/manager/app/src/main/res/values-it/strings.xml b/manager/app/src/main/res/values-it/strings.xml index d9a9d6f..a33f66c 100644 --- a/manager/app/src/main/res/values-it/strings.xml +++ b/manager/app/src/main/res/values-it/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Scollega moduli da default Il valore predefinito per \"Scollega moduli\" in App Profile. Se attivato, rimuoverà tutte le modifiche al sistema da parte dei moduli per le applicazioni che non hanno un profilo impostato. - Disable kprobe hooks Attivando questa opzione permetterai a KernelSU di ripristinare ogni file modificato dai moduli per questa app. Dominio Regole @@ -127,7 +126,6 @@ LKM selezionato: %s Salva Registri Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -235,7 +233,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -278,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -309,9 +304,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index 6a9c511..fdbfa06 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -63,7 +63,6 @@ 現在の KernelSU のバージョン「%s」は低すぎるため、マネージャーは正常に動作しません。バージョン「%s」以上に更新してください! デフォルトでモジュールのマウントを解除する アプリプロファイルの「モジュールのアンマウント」の共通となるデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。 - kprobe フックを無効化 このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。 ドメイン ルール @@ -113,7 +112,6 @@ \n続行しますか? 次へ %1$s のパーティションイメージを推奨します。 - (不安定) KMI を選択してください アンインストール 一時的にアンインストールする @@ -128,7 +126,6 @@ 選択された LKM: %s ログを保存 保存されたログ - SuS SU モード: %1$s モジュールをインストールしますか? 不明なモジュール @@ -240,7 +237,6 @@ ファイルの種類が間違っています!.kpm ファイルを選択してください。 アンインストール 次の KPM がアンインストールされます: %s - KernelSU によって作成された kprobe フックを無効化して、代替となるインラインフックを使用します。これは、非 GKI カーネルのフック方式に似た物になります。 2 本の指で画像を拡大、1 本の指でドラッグで位置を調整します。 再プロビジョニング @@ -283,8 +279,6 @@ 高度な設定 ツールバーをカスタマイズ 戻る - SuSFS 有効 - SuSFS 無効 背景の設定が成功しました カスタム背景を削除しました 代替アイコン @@ -314,9 +308,8 @@ 変更した DPI 設定を適用するにはアプリを再起動する必要がありますが、システムステータスバーや他のアプリには影響しません DPI は %1$d に変更されました。アプリの再起動後に適用されます。 - アプリの言語 - システムに従う - 言語の変更を適用するために再起動しています + アプリの言語 + システムに従う カードの暗さを調整 エラーコード @@ -413,8 +406,6 @@ 追加されたパスのアンマウントに成功しました: %s アンマウントのパスの保存に成功しました: %s - 実行を確認してアンマウントを試す - 設定されたすべてのアンマウントの試行操作が直ちに実行されます。続行してもよろしいですか? SUS パスをリセット すべての SUS パスの構成が消去されます。続行してもよろしいですか? @@ -445,7 +436,6 @@ 自動でバインドマウント 自動でバインドマウントのアンマウントを試す KSU SUSFS シンボルを非表示 - Magic Mount の対応 SUS Kstat の対応 SUS SU モード切り替え機能 @@ -513,7 +503,6 @@ 現在の設定: %s すべてのプロセスを非表示 非 KSU プロセスのみ非表示 - 実行 簡潔モードなカーネル バージョン SukiSU のカーネル バージョンによって表示されるクリーンモードを有効または無効します。 Android データパスが設定されました: %s @@ -604,7 +593,6 @@ ループパス ループパスを追加 - SUS ループパス ループパスの構成 ループパスは、非 root ユーザーアプリまたは独立したサービスの起動ごとに SUS_PATH として再設定されます。これにより、追加されたパスの inode ステータスがリセットされたり、カーネル内で inode が再生成される問題に対処できます。 AVC ログの偽装 diff --git a/manager/app/src/main/res/values-kn/strings.xml b/manager/app/src/main/res/values-kn/strings.xml index 6bbee86..e60127c 100644 --- a/manager/app/src/main/res/values-kn/strings.xml +++ b/manager/app/src/main/res/values-kn/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! ಡೀಫಾಲ್ಟ್ ಆಗಿ Umount ಮಾಡ್ಯೂಲ್ ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್‌ಗಳಲ್ಲಿ \"Umount ಮಾಡ್ಯೂಲ್\" ಗಾಗಿ ಜಾಗತಿಕ ಡೀಫಾಲ್ಟ್ ಮೌಲ್ಯ. ಸಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಪ್ರೊಫೈಲ್ ಸೆಟ್ ಅನ್ನು ಹೊಂದಿರದ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಿಗಾಗಿ ಸಿಸ್ಟಮ್‌ಗೆ ಎಲ್ಲಾ ಮಾಡ್ಯೂಲ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಇದು ತೆಗೆದುಹಾಕುತ್ತದೆ. - Disable kprobe hooks ಈ ಆಯ್ಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸುವುದರಿಂದ ಈ ಅಪ್ಲಿಕೇಶನ್‌ಗಾಗಿ ಮಾಡ್ಯೂಲ್‌ಗಳ ಮೂಲಕ ಯಾವುದೇ ಮಾರ್ಪಡಿಸಿದ ಫೈಲ್‌ಗಳನ್ನು ಮರುಸ್ಥಾಪಿಸಲು KernelSU ಗೆ ಅನುಮತಿಸುತ್ತದೆ. ಡೊಮೇನ್ ನಿಯಮಗಳು @@ -125,7 +124,6 @@ Selected LKM: %s ಲಾಗ್ಗಳನ್ನು ಉಳಿಸಿ Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-ko/strings.xml b/manager/app/src/main/res/values-ko/strings.xml index 38e0ee1..8e6ee7d 100644 --- a/manager/app/src/main/res/values-ko/strings.xml +++ b/manager/app/src/main/res/values-ko/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! 기본값으로 모듈 사용 해제 앱 프로필 메뉴의 \"모듈 마운트 해제\" 설정에 대한 전역 기본값을 설정합니다. 활성화 시, 개별 프로필이 설정되지 않은 앱은 시스템에 대한 모듈의 모든 수정사항이 적용되지 않습니다. - Disable kprobe hooks 이 옵션이 활성화되면, KernelSU는 이 앱에 대한 모듈의 모든 수정사항을 복구합니다. 도메인 규칙 @@ -125,7 +124,6 @@ 선택된 LKM: %s 로그 저장 로그 저장됨 - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-lt/strings.xml b/manager/app/src/main/res/values-lt/strings.xml index 6987bed..4206c33 100644 --- a/manager/app/src/main/res/values-lt/strings.xml +++ b/manager/app/src/main/res/values-lt/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Atjungti modulius pagal numatytuosius parametrus Visuotinė numatytoji „Modulių atjungimo“ reikšmė programų profiliuose. Jei įjungta, ji pašalins visus sistemos modulio pakeitimus programoms, kurios neturi profilio. - Disable kprobe hooks Įjungus šią parinktį, KernelSU galės atkurti visus modulių modifikuotus failus šiai programai. Domenas Taisyklės @@ -125,7 +124,6 @@ Selected LKM: %s Saglabāt Žurnālus Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-lv/strings.xml b/manager/app/src/main/res/values-lv/strings.xml index 3e70c16..92b8287 100644 --- a/manager/app/src/main/res/values-lv/strings.xml +++ b/manager/app/src/main/res/values-lv/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Pēc noklusējuma atvienot moduļus Globālā noklusējuma vērtība vienumam “Atvienot moduļus” lietotņu profilos. Ja tas ir iespējots, lietojumprogrammām, kurām nav iestatīts profils, tiks noņemtas visas sistēmas moduļu modifikācijas. - Disable kprobe hooks Iespējojot šo opciju, KernelSU varēs atjaunot visus moduļos šīs lietojumprogrammas modificētos failus. Domēns Noteikumi @@ -127,7 +126,6 @@ Izvēlētais lkm: %s Išsaugoti Žurnalus Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -235,7 +233,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -278,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -309,9 +304,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-mr/strings.xml b/manager/app/src/main/res/values-mr/strings.xml index 97473dc..d070c17 100644 --- a/manager/app/src/main/res/values-mr/strings.xml +++ b/manager/app/src/main/res/values-mr/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! डीफॉल्टनुसार मॉड्यूल्स उमाउंट करा अॅप प्रोफाइलमधील \"उमाउंट मॉड्यूल्स\" साठी जागतिक डीफॉल्ट मूल्य. सक्षम असल्यास, ते प्रोफाइल सेट नसलेल्या ॲप्लिकेशनचे सिस्टममधील सर्व मॉड्यूल बदल काढून टाकेल. - Disable kprobe hooks हा पर्याय सक्षम केल्याने KernelSU ला या ऍप्लिकेशनसाठी मॉड्यूल्सद्वारे कोणत्याही सुधारित फाइल्स पुनर्संचयित करण्यास अनुमती मिळेल. डोमेन नियम @@ -125,7 +124,6 @@ Selected LKM: %s लॉग जतन करा Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-ms/strings.xml b/manager/app/src/main/res/values-ms/strings.xml index 314575c..245e8b5 100644 --- a/manager/app/src/main/res/values-ms/strings.xml +++ b/manager/app/src/main/res/values-ms/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Umount modules by default The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set. - Disable kprobe hooks Enabling this option will allow KernelSU to restore any modified files by the modules for this app. Domain Rules @@ -125,7 +124,6 @@ Selected LKM: %s Simpan Log Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-nl/strings.xml b/manager/app/src/main/res/values-nl/strings.xml index 14dd895..3e516f1 100644 --- a/manager/app/src/main/res/values-nl/strings.xml +++ b/manager/app/src/main/res/values-nl/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Ontkoppel standaard de modules De globale standaardwaarde voor \"Umount modules\" in App Profile. Als dit is ingeschakeld, worden alle modulewijzigingen in het systeem verwijderd voor apps waarvoor geen profiel is ingesteld. - Disable kprobe hooks Met deze optie ingeschakeld zal KernelSU toelaten om alle gewijzigde bestanden door de modules voor deze app te herstellen. Domein Regels @@ -127,7 +126,6 @@ Geselecteerde LKM: %s Logboeken Opslaan Logs opgeslagen - SuS SU mode: confirm install module %1$s? unknown module @@ -235,7 +233,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -278,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -309,9 +304,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-pl/strings.xml b/manager/app/src/main/res/values-pl/strings.xml index 9346f91..3a2203a 100644 --- a/manager/app/src/main/res/values-pl/strings.xml +++ b/manager/app/src/main/res/values-pl/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Domyślnie odmontuj moduły Globalna wartość domyślna opcji \"Odmontuj moduły\" w profilu aplikacji. Jeśli jest włączona, wycofuje wszystkie modyfikacje dokonane przez moduły dla aplikacji, które nie mają ustawionego profilu. - Disable kprobe hooks Włączenie tej opcji umożliwi KernelSU przywrócenie wszelkich zmodyfikowanych plików przez moduły dla tej aplikacji. Domena Reguły @@ -127,7 +126,6 @@ Wybrano LKM: %s Zapisz dzienniki Dzienniki zapisane - SuS SU mode: confirm install module %1$s? unknown module @@ -235,7 +233,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -278,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -309,9 +304,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-pt/strings.xml b/manager/app/src/main/res/values-pt/strings.xml index 9a8406d..cda81c7 100644 --- a/manager/app/src/main/res/values-pt/strings.xml +++ b/manager/app/src/main/res/values-pt/strings.xml @@ -63,7 +63,6 @@ A versão atual do KernelSU %s é muito baixa para o gerenciador funcionar corretamente. Atualize para a versão %s ou superior! Módulos desativados por padrão O valor padrão global para \"Módulos Umount\" em Perfis de Aplicativos. Se ativado, removerá todas as modificações de módulo do sistema para aplicativos que não possuem um Perfil definido. - Desabilitar ganchos de \"Kprobe\" Ativar esta opção permitirá que o KernelSU restaure quaisquer arquivos modificados pelos módulos para este aplicativo. Domínio Regras @@ -125,7 +124,6 @@ LKM selecionado: %s Salvar Registros Registros salvos - Modo SU SuSU: ¿confirmar la instalación del módulo %1$s? módulo desconocido @@ -233,7 +231,6 @@ Tipo de arquivo incorreto! Selecione o arquivo .kpm. Desinstalar O seguinte KPM será desinstalado: %s - Desative os hooks kprobe criados pelo KernelSU, usando ganchos embutidos em vez disso, o que é semelhante ao método de gancho do kernel GKI. Use dois dedos para ampliar a imagem e um dedo para arrastá-la para ajustar a posição Restituição @@ -276,8 +273,6 @@ Configurações Avançadas Personaliza a barra de ferramentas. Retorno - SuSFS habilitado - SuSFS desativado Fundo definido com sucesso Remover Ícone alternativo @@ -307,9 +302,8 @@ O aplicativo precisa ser reiniciado para aplicar as novas configurações DPI, não afeta a barra de status do sistema ou outras aplicações DPI foi definido para %1$d, efetivo após reiniciar o aplicativo - Língua do aplicativo - Padrão do sistema - Idioma alterado, reiniciando para aplicar as alterações + Língua do aplicativo + Padrão do sistema Ajuste da escuridão do cartão Código de erro diff --git a/manager/app/src/main/res/values-ro/strings.xml b/manager/app/src/main/res/values-ro/strings.xml index 8e6affd..10d9095 100644 --- a/manager/app/src/main/res/values-ro/strings.xml +++ b/manager/app/src/main/res/values-ro/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! U-montează modulele în mod implicit Valoarea implicită globală pentru „Module u-montate” în Profilurile aplicațiilor. Dacă este activat, va elimina toate modificările modulelor aduse sistemului pentru aplicațiile care nu au un profil setat. - Disable kprobe hooks Activarea acestei opțiuni va permite KernelSU să restaureze orice fișiere modificate de către modulele pentru această aplicație. Domeniu Reguli @@ -127,7 +126,6 @@ Lkm selectat: %s Salvează Jurnale Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -235,7 +233,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -278,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -309,9 +304,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index b62f290..1c6aede 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -63,7 +63,6 @@ Текущая версия KernelSU %s слишком низкая для правильной работы менеджера. Пожалуйста, обновите до версии %s или выше! Размонтировать модули по умолчанию Глобальное значение по умолчанию для \"Размонтировать модули\" в профиле приложения. При включении будут удалены все модификации модулей в системе для приложений, у которых не задан профиль - Отключить kprobe хуки Включение этой опции позволит KernelSU восстанавливать любые измененные модулями файлы для данного приложения. Домен Правила @@ -112,8 +111,10 @@ \n Используйте эту опцию только после завершения OTA. \n Продолжить? Далее + Выбрать раздел + Использовать локальный файл LKM + Поддерживаются только файлы .ko Образ раздела %1$s рекомендуется - (нестабильный) Выбрать KMI Удалить Удалить на время @@ -128,7 +129,6 @@ Выбран LKM: %s Сохранить логи Логи сохранены - SuS SU режим: подтвердите установку модуля %1$s? неизвестный модуль @@ -166,6 +166,13 @@ Предоставление рут-доступа %s запрещено Отключить su совместимость Временно отключить все приложения от получения рут-доступа через su команду (запущенные процессы с рут-доступом не будут затронуты). + Отключить размонтирование ядра + Временное отключение умного уровня ядра KernelSU. + Включить повышенную безопасность + Включить более строгие политики безопасности. + По умолчанию + Временно включены + Постоянно включены Уверены, что хотите установить следующие %1$d модули? \n\n%2$s Доп. настройки SELinux @@ -240,7 +247,6 @@ Неверный тип файла! Пожалуйста, выберите .kpm файл. Удалить Следующие KPM будут удалены: %s - Отключите хуки kprobe, созданные KernelSU, используя встроенные хуки, которые похожи на метод расширения ядра вне GKI. Используйте два пальца для увеличения изображения, и один палец для изменения положения Реализация @@ -283,8 +289,6 @@ Расширенные Внешний вид Возвращение - SuSFS включен - SuSFS выключен Фон успешно установлен Пользовательский фон удалён Альт. иконка @@ -314,9 +318,8 @@ Приложение нужно перезапустить, чтобы применить новые настройки DPI. Это не влияет на системную строку состояния или другие приложения DPI был установлен в %1$d, действующий после перезапуска приложения - Язык приложения - Как в системе - Язык изменён, перезапуск для применения изменений + Язык приложения + Как в системе Затемнение карточек код ошибки @@ -413,8 +416,6 @@ Попытка размонтировать путь успешно добавлена: %s Попытка размонтировать путь успешно сохранена: %s - Подтверждение запуска попробовать размонтировать - Это немедленно выполнит все настроенные операции размонтирования. Вы уверены, что хотите продолжить? Сбросить SUS пути Это очистит все конфигурации пути SUS. Вы уверены, что хотите продолжить? @@ -445,7 +446,6 @@ Автоматическое бинд монтирование Автоматически попробовать размонтировать привязать монтировать Скрытие KSU SUSFS Symbols - Поддержка Magic Mount Поддержка SUS Kstat Функция переключения режима SUS SU @@ -513,7 +513,6 @@ Текущие настройки: %s Скрыть для всех процессов Скрыть только для процессов, не связанных с KSU - Запустить Скрыть доп. информацию о ядре Включить или отключить чистый режим, отображаемой версии ядра SukiSU Путь к данным Android был установлен на: %s @@ -564,6 +563,8 @@ Другое Приложение Добавить путь приложения + Несоответствие версий библиотеки SuSFS! Ядро: %1$s, менеджер: %2$s. Рекомендуется обновить ядро или менеджер + Внимание Поиск приложений Выбрано %1$d приложений %1$d приложений уже добавлено @@ -604,7 +605,6 @@ Циклические пути Добавить циклический путь - Циклический путь SUS Конфигурация пути цикла Пути цикла повторно отмечены как SUS_PATH в каждом пользовательском приложении, не являющемся root, или изолированном запуске службы. Это помогает решить проблемы, в которых добавленные пути могут иметь сброс статуса inode или повторно созданные inode в ядре. Спуф AVC лога @@ -623,4 +623,121 @@ Неподписанные модули могут быть неполными. Для защиты устройства установка этого модуля была заблокирована. Неподписанные модули могут быть неполными. Вы хотите разрешить установку на этом устройстве следующего модуля от неизвестного издателя? Типы хуков + + Патч KPM + Добавление дополнительных функций KPM + Патч KPM + Применить патч KPM к образу ядра перед прошивкой + Патч для отмены KPM + Отменить ранее применённый патч KPM + Патч KPM включен + Патч для отмены KPM включен + Режим патча KPM + Режим отмены патча KPM + + Подготовка инструментов KPM + Применение патча KPM + Отмена патча KPM + Найден файл образа: %s + Патч KPM успешно применен + Патч KPM успешно отменён + Файл успешно переупакован + + Не удалось извлечь zip-файл + Файл образа не найден + Ошибка патча KPM + Ошибка патча отмены KPM + Ошибка патча KPM: %s + + Следовать за ядром + Использовать ядро без каких-либо изменений KPM + + Пользовательский режим сканирования списка приложений + Включение этой опции позволит использовать сканирование пользовательского режима для списка приложений, улучшая стабильность. (Если вы столкнетесь с такими проблемами, как замораживание во время сканирования ядра списка приложений, вы можете включить эту опцию.) + Поиск многопользовательских приложений + Когда включено, сканирует приложения для всех пользователей, включая рабочие профили + Не удалось установить, проверьте права доступа + Очистить среду Runtime + Очистить среду Runtime и остановить службу сканирования + Вы уверены, что хотите очистить среду Runtime? Это остановит службу сканирования и удалит связанные с ней файлы. + Runtime окружение успешно очищено + Не удалось очистить среду runtime + + Подтвердите установку + Подтвердите установку (%d файлов) + Установить + Модули + Ядро + Неизвестно + Неизвестное ядро + Неизвестный файл + Версия + Автор + Описание + Поддерживаемые устройства + + SUS Maps + Путь к библиотеке + /data/adb/modules/my_module/zygisk/arm64-v8a.so + Добавить SUS Map + Изменить SUS Map + SUS map успешно добавлен: %1$s + SUS map удален: %1$s + SUS map обновлен: %1$s -> %2$s + SUS maps не настроены + Сбросить SUS Maps + Это действие удалит все настроенные SUS maps. Это действие нельзя отменить. + Скрытие физической памяти + Скрыть настоящий файл mmapped с разных карт в /proc/self/ + Скрыть реальные пути к папкам памяти из /proc/self/[maps|smaps_rollup|map_files|mem|pagemap]. Обратите внимание: эта функция не поддерживает скрытие анонимных карт и может скрыть встроенные хуки или PLT хуки вызванные инъекцией самой библиотеки. + Важное уведомление: для приложений с хорошо реализованными механизмами обнаружений эта функция может работать не эффективно . + Сначала найдите PID и UID целевого приложения, используя ps -enf, затем проверьте соответствующие пути в /proc/<pid>/maps и сравните номера устройства с номерами в /proc/1/mountinfo. Функция скрытия будет работать должным образом только если номера устройств совпадают. + + Просмотр логов + Назад + Поиск + Очистить логи + Вы уверены, что хотите удалить выбранный файл журнала? Это действие нельзя отменить. + Логи успешно очищены + Фильтрация по типу + Все типы + Показаны записи %1$d из %2$d + Логи не найдены + Совпадающих журналов не найдено + Обновить + Необработанный лог + Поиск по UID, команде или деталям… + Очистить поиск + Просмотр логов использования + Просмотр журналов доступа к KernelSU + Исключить подтипы + Текущее приложение + Страница: %1$d/%2$d | Всего журналов: %3$d + Слишком много журналов, отображаются только последние записи %1$d + Загрузить больше логов + Все логи отображаются + + Подтвердите удаление SukiSU Manager? + Удаление не повлияет на функциональность вашего root-доступа. Он продолжит работать от этого менеджера. + Текущий менеджер несовместим с этим ядром! Пожалуйста, обновите ядро до версии %2$d или выше (сейчас %1$d) + + Управление путями размонтирования + Управления путями размонтирования ядра + Для применения изменений необходима перезагрузка. Система применит новый конфиг при следующем запуске. + Добавить путь размонтирования + Смонтировать путь + Флаги размонтирования + 0=Обычное размонтирование, 8=MNT_DETACH, -1=Автоматически + Флаги + Подтвердите удаление + Вы уверены, что хотите удалить путь размонтирования%s? + Путь добавлен, изменения вступят в силу после перезагрузки + Путь удален, изменения вступят в силу после перезагрузки + Неудача + Подтвердите действие + Вы уверены, что хотите удалить все кастомные пути? (Стандартные пути не будут удалены) + Кастомные пути удалены + Удалить кастомные пути + Применить конфигурацию + Конфигурация применена к ядру diff --git a/manager/app/src/main/res/values-sl/strings.xml b/manager/app/src/main/res/values-sl/strings.xml index 85d14e8..27bf371 100644 --- a/manager/app/src/main/res/values-sl/strings.xml +++ b/manager/app/src/main/res/values-sl/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Po privzetem izvrzi module Globalno privzeta vrednost za \"Izvrzi module\" v aplikacijskih profilih. Če je omogočena, bo to odstranilo vse sistemske modifikacije modulov za aplikacije, ki nimajo nastavljenega profila. - Disable kprobe hooks Omogočanje te opcije bo dovolilo KernelSU, da obnovi vse zaradi modulov spremenjene datoteke za to aplikacijo. Domena Pravila @@ -125,7 +124,6 @@ Selected LKM: %s Shrani Dnevnike Logs saved - SuS SU mode: confirm install module %1$s? unknown module @@ -233,7 +231,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -276,8 +273,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -307,9 +302,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-th/strings.xml b/manager/app/src/main/res/values-th/strings.xml index dafa582..ed07c50 100644 --- a/manager/app/src/main/res/values-th/strings.xml +++ b/manager/app/src/main/res/values-th/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Umount โมดูลตามค่าเริ่มต้น หากเปิดใช้งานค่าเริ่มต้นโดยทั่วไปสำหรับ \"Umount โมดูล\" ในโปรไฟล์แอป จะเป็นการลบการแก้ไขโมดูลทั้งหมดในระบบสำหรับแอปพลิเคชันที่ไม่มีการตั้งค่าโปรไฟล์ - Disable kprobe hooks การเปิดใช้งานตัวเลือกนี้จะทำให้ KernelSU สามารถกู้คืนไฟล์ที่แก้ไขโดยโมดูลสำหรับแอปนี้ได้ โดเมน กฎ @@ -127,7 +126,6 @@ เลือก LKM: %s บันทึกบันทึก บันทึก Log แล้ว - SuS SU mode: confirm install module %1$s? unknown module @@ -235,7 +233,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -278,8 +275,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -309,9 +304,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index 4f36cc1..3ee1112 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -63,7 +63,6 @@ Mevcut KernelSU sürümü %s, yöneticinin düzgün çalışması için çok düşük. Lütfen sürüm %s veya daha yüksek bir sürüme yükseltin! Modülleri varsayılan olarak bağlamayı kaldır Uygulama Profilindeki \"Modülleri bağlamayı kaldır\" için küresel varsayılan değer. Etkinleştirilirse, profil ayarlanmamış uygulamalar için sistemdeki tüm modül değişikliklerini kaldırır. - Kprobe kancalarını devre dışı bırak Bu seçeneği etkinleştirmek, KernelSU\'nun bu uygulama için modüller tarafından değiştirilen dosyaları geri yüklemesine izin verecektir. Etki alanı Kurallar @@ -110,8 +109,9 @@ Etkin olmayan yuvaya yükle (OTA sonrası) Cihazınız, yeniden başlatma sonrasında **ZORUNLU** olarak mevcut etkin olmayan yuvaya önyükleme yapacaktır!\nSadece OTA tamamlandıktan sonra bu seçeneği kullanın.\nDevam etmek istiyor musunuz? İleri + Yerel LKM dosyası kullan + Yalnızca .ko dosyaları desteklenir %1$s bölüm görüntüsü önerilir - (kararsız) KMI seçin Kaldır Geçici olarak kaldır @@ -126,7 +126,6 @@ Seçilen LKM: %s Günlükleri kaydet Günlükler kaydedildi - SuS SU modu: %1$s modülünü yüklemek istediğinizden emin misiniz? Bilinmeyen modül @@ -164,6 +163,13 @@ %s için süper kullanıcı yetkisi verilemiyor Su uyumluluğunu devre dışı bırak Geçici olarak herhangi bir uygulamanın su komutu aracılığıyla root ayrıcalıkları elde etmesini devre dışı bırakır (mevcut root işlemleri etkilenmez). + Çekirdek ayırmasını devre dışı bırak + KernelSU tarafından kontrol edilen çekirdek seviyesindeki ayırma davranışını devre dışı bırakın. + Gelişmiş güvenliği etkinleştir + Daha katı güvenlik politikalarını etkinleştirin. + Varsayılan + Geçici olarak etkinleştir + Kalıcı olarak etkinleştir Aşağıdaki %1$d modülü yüklemek istediğinizden emin misiniz? \n\n%2$s Daha fazla ayar SELinux @@ -238,7 +244,6 @@ Yanlış dosya türü! Lütfen .kpm dosyasını seçin. Kaldır Aşağıdaki KPM kaldırılacak: %s - KernelSU tarafından oluşturulan kprobe kancalarını devre dışı bırakın, bunun yerine inline kancalar kullanın, bu da Non-GKI çekirdek kanca yöntemine benzer. Görüntüyü yaklaştırmak için iki parmağınızı kullanın ve bir parmağınızla sürükleyerek konumu ayarlayın Yeniden sağla @@ -281,8 +286,6 @@ Gelişmiş Ayarlar Araç çubuğunu özelleştir Geri - SuSFS etkin - SuSFS devre dışı Arka plan başarıyla ayarlandı Özel arka planlar kaldırıldı Alternatif simge @@ -312,9 +315,8 @@ Yeni DPI ayarlarını uygulamak için uygulamanın yeniden başlatılması gerekir, sistem durum çubuğunu veya diğer uygulamaları etkilemez DPI %1$d olarak ayarlandı, uygulama yeniden başlatıldıktan sonra geçerli olur - Uygulama Dili - Sistemi takip et - Dil değiştirildi, değişiklikleri uygulamak için yeniden başlatılıyor + Uygulama Dili + Sistemi takip et Kart Karanlığını Ayarlama hata kodu @@ -411,8 +413,6 @@ Bağlamayı kaldırmayı dene yolu başarıyla eklendi: %s Bağlamayı kaldırmayı dene yolu kaydetme başarılı: %s - Bağlamayı Kaldırmayı Dene Çalıştırmayı Onayla - Bu, yapılandırılmış tüm bağlamayı kaldırmayı dene işlemlerini hemen çalıştıracaktır. Devam etmek istiyor musunuz? SUS Yollarını Sıfırla Bu, tüm SUS yol yapılandırmalarını temizleyecektir. Devam etmek istiyor musunuz? @@ -443,7 +443,6 @@ Otomatik Bağlama Noktası Bağlama Otomatik Bağlamayı Kaldırmayı Dene Bağlama KSU SUSFS Sembollerini Gizle - Magic Mount Desteği SUS Kstat Desteği SUS SU mod değiştirme işlevi @@ -511,7 +510,6 @@ Mevcut ayar: %s Tüm işlemler için gizle Sadece KSU dışı işlemler için gizle - Çalıştır Çekirdek Sürümü Özet Modu SukiSU çekirdek sürümünün gösterdiği sade modu etkinleştirin veya devre dışı bırakın Android Veri yolu şuna ayarlandı: %s @@ -562,6 +560,8 @@ Diğer Uygulama Uygulama Yolu Ekle + SuSFS kütüphane sürümü uyuşmazlığı, çekirdek: %1$s vs yönetici: %2$s, Çekirdeği veya yöneticiyi güncellemeniz önerilir + Uyarı Uygulama Ara %1$d uygulama seçildi %1$d uygulama zaten eklendi @@ -602,7 +602,6 @@ Döngü Yolları Döngü Yolu Ekle - SUS Döngü Yolu Döngü Yolu Yapılandırması Döngü yolları, her kök olmayan (non-root) kullanıcı uygulaması veya yalıtılmış hizmet başlangıcında SUS_PATH olarak yeniden işaretlenir. Bu, eklenen yolların inode durumunun sıfırlanması veya çekirdekte yeniden oluşturulması gibi sorunları gidermeye yardımcı olur. AVC Günlük Kaydı Taklidi @@ -621,4 +620,102 @@ etkin: Çekirdekteki AVC günlük kaydında, \'su\' komutuna ait tcontext\'i \'k Bilinmeyen yayıncı İmzasız modüller eksik veya değiştirilmiş olabilir. Cihazınızı korumak için bu modülün kurulumu engellenmiştir. İmzasız modüller eksik veya değiştirilmiş olabilir. Bilinmeyen bir yayıncıdan gelen aşağıdaki modülün bu cihaza kurulmasına izin vermek istiyor musunuz? + Hook tipi + + KPM Yaması + Ek KPM özellikleri eklemek için + KPM Yaması + Yüklemeden önce çekirdek imajına KPM yamasını uygula + KPM Yamasını Geri Al + Daha önce uygulanan KPM yamasını geri al + KPM yaması etkinleştirildi + KPM yaması geri alma etkinleştirildi + KPM Yama Modu + KPM Yamasını Geri Alma Modu + + KPM araçları hazırlanıyor + KPM yaması uygulanıyor + KPM yaması geri alınıyor + İmaj dosyası bulundu: %s + KPM yaması başarıyla uygulandı + KPM yaması başarıyla geri alındı + Dosya başarıyla yeniden paketlendi + + Zip dosyası çıkarılamadı + İmaj dosyası bulunamadı + KPM yaması başarısız oldu + KPM yamasını geri alma başarısız oldu + KPM yama işlemi başarısız oldu: %s + + Çekirdeği Takip Et + Çekirdeği herhangi bir KPM değişikliği olmadan olduğu gibi kullan + + Kullanıcı modu uygulama listesi taraması + Bu seçeneği etkinleştirmek, uygulama listesi için kullanıcı modu taramasını kullanarak kararlılığı artıracaktır. (Uygulama listesinin çekirdek tarafından taranması sırasında donma gibi sorunlar yaşıyorsanız, bu seçeneği etkinleştirmeyi deneyebilirsiniz.) + Çok Kullanıcılı Uygulama Taraması + Etkinleştirildiğinde, iş profilleri de dahil olmak üzere tüm kullanıcıların uygulamalarını tarar. + Ayar başarısız oldu, lütfen izinleri kontrol edin + Çalışma Zamanı Ortamını Temizle + Çalışma zamanı dosyalarını temizleyin ve tarayıcı hizmetini durdurun + Çalışma zamanı ortamını temizlemek istediğinizden emin misiniz? Bu işlem tarayıcı hizmetini durduracak ve ilgili dosyaları kaldıracaktır. + Çalışma zamanı ortamı başarıyla temizlendi + Çalışma zamanı ortamı temizlenemedi + + Kurulumu Onayla + Kurulumu Onayla (%d dosya) + Yükle + Modül + Kernel + Bilinmiyor + Bilinmeyen Kernel + Bilinmeyen Dosya + Sürüm + Geliştirici + Açıklama + Desteklenen Cihazlar + + SUS Eşlemeleri + Kütüphane Yolu + /data/adb/modules/my_module/zygisk/arm64-v8a.so + SUS Eşlemesi Ekle + SUS Eşlemesini Düzenle + SUS eşlemesi başarıyla eklendi: %1$s + SUS eşlemesi kaldırıldı: %1$s + SUS eşlemesi güncellendi: %1$s -> %2$s + Yapılandırılmış SUS eşlemesi yok + SUS Eşlemelerini Sıfırla + Bu, yapılandırılmış tüm SUS eşlemelerini kaldıracaktır. Bu işlem geri alınamaz. + Bellek Eşlemesi Gizleme + /proc/self/ içindeki çeşitli eşlemelerden mmap\'lenmiş gerçek dosyayı gizle + Bellek eşlemelerinin gerçek dosya yollarını /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] konumundan gizleyin. Lütfen dikkat: Bu özellik, anonim bellek eşlemelerini gizlemeyi desteklemez ve enjekte edilen kütüphanenin kendisinin neden olduğu satır içi kancaları veya PLT kancalarını da gizleyemez. + Önemli Not: İyi uygulanmış enjeksiyon tespit mekanizmalarına sahip uygulamalar için bu özellik, tespiti etkili bir şekilde atlatamayabilir. + Öncelikle, ps -enf kullanarak hedef uygulamanın PID ve UID\'sini bulun, ardından /proc/<pid>/maps içindeki ilgili yolları kontrol edin ve tutarlılığı sağlamak için cihaz numaralarını /proc/1/mountinfo\'dakilerle karşılaştırın. Yalnızca cihaz numaraları eşleştiğinde harita gizleme işlevi düzgün çalışabilir. + + Günlük Görüntüleyici + Geri + Ara + Günlükleri Temizle + Seçili günlük dosyasını temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz. + Günlükler başarıyla temizlendi + Türe Göre Filtrele + Tüm Türler + %2$d girişten %1$d tanesi gösteriliyor + Günlük bulunamadı + Eşleşen günlük bulunamadı + Yenile + Ham Günlük + UID, komut veya ayrıntılara göre ara… + Aramayı temizle + Kullanım Günlüklerini Görüntüle + KernelSU süper kullanıcı erişim günlüklerini görüntüle + Alt türleri hariç tut + Mevcut Uygulama + Sayfa: %1$d/%2$d | Toplam günlük: %3$d + Çok fazla günlük var, yalnızca son %1$d giriş gösteriliyor + Daha Fazla Günlük Yükle + Tüm günlükler görüntülendi + + SukiSU Yöneticisi Kaldırılsın mı? + Kaldırma işlemine devam etmek, root erişiminizin temel işlevselliğini etkilemeyecektir. Root, bu yöneticiden bağımsız olarak çalışacak şekilde tasarlanmıştır. + Mevcut yönetici bu çekirdekle uyumsuz! Lütfen çekirdeği %2$d veya daha yüksek bir sürüme yükseltin (mevcut sürüm %1$d) diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 39216a2..e407110 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -1,6 +1,5 @@ - SukiSU Ultra Головна Не встановлено Натисніть, щоб встановити @@ -52,7 +51,7 @@ Підтримати нас KernelSU є, і завжди буде, безкоштовним та з відкритим вихідним кодом. Однак ви можете показати нам свою підтримку, зробивши пожертву. Приєднуйтесь до нашого каналу %2$s]]> - Профіль додатку + Профіль додатку За замовчуванням Шаблон Власний @@ -65,7 +64,6 @@ Поточна версія KernelSU %s занадто низька для коректної роботи менеджера. Будь ласка, оновіться до версії %s або вище! Відмонтовувати модулі за замовчуванням Глобальне значення за замовчуванням для "Відмонтувати модулі" у профілі додатку. Якщо ввімкнено, це видалить усі зміни системи, зроблені модулями, для додатків без встановленого профілю. - Вимкнути хуки kprobe Увімкнення цієї опції дозволить KernelSU відновити будь-які змінені модулями файли для цього додатку. Домен Правила @@ -113,7 +111,6 @@ Ваш пристрій буде **ПРИМУСОВО** завантажено в поточний неактивний слот після перезавантаження!\nВикористовуйте цю опцію лише після завершення OTA.\nПродовжити? Далі Рекомендується образ розділу %1$s - (нестабільно) Вибрати KMI Видалити Тимчасово видалити @@ -128,7 +125,6 @@ Обраний LKM: %s Зберегти логи Логи збережено - Режим SuS SU: Підтвердити встановлення модуля %1$s? невідомий модуль @@ -238,7 +234,6 @@ Неправильний тип файлу! Будь ласка, виберіть файл .kpm. Видалити Буде видалено наступний KPM: %s - Вимкнути хуки kprobe, створені KernelSU, використовуючи натомість інлайн-хуки, що схоже на метод хуків для ядер без GKI. Використовуйте два пальці для масштабування зображення та один палець для перетягування, щоб налаштувати положення Переналаштувати @@ -281,8 +276,6 @@ Розширені налаштування Налаштувати панель інструментів Повернутися - SuSFS увімкнено - SuSFS вимкнено Фон успішно встановлено Видалено власні фони Альтернативна іконка @@ -312,9 +305,8 @@ Додаток потрібно перезапустити, щоб застосувати нові налаштування DPI; це не вплине на системний рядок стану або інші додатки DPI встановлено на %1$d, набуде чинності після перезапуску додатку - Мова додатку - Як у системі - Мову змінено, перезапуск для застосування змін + Мова додатку + Як у системі Налаштування затемнення карток код помилки @@ -411,8 +403,6 @@ Шлях для спроби відмонтування успішно додано: %s Спроба збереження шляху відмонтування успішна: %s - Підтвердити виконання спроби відмонтування - Це негайно виконає всі налаштовані операції спроби відмонтування. Ви впевнені, що хочете продовжити? Скинути шляхи SUS Це видалить усі конфігурації шляхів SUS. Ви впевнені, що хочете продовжити? @@ -443,7 +433,6 @@ Автоматичне прив\'язане монтування Автоматична спроба відмонтування прив\'язаного монтування Приховати символи KSU SUSFS - Підтримка Magic Mount Підтримка SUS Kstat Функція перемикання режиму SUS SU @@ -511,7 +500,6 @@ Поточне налаштування: %s Приховувати для всіх процесів Приховувати лише для процесів, що не належать KSU - Запустити Спрощений режим версії ядра Увімкнути або вимкнути спрощене відображення версії ядра SukiSU Шлях до Android Data встановлено на: %s diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index c3c9788..b62325b 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -6,10 +6,10 @@ Đang hoạt động Phiên bản: %s Không được hỗ trợ - Không phát hiện được trình điều khiển SukiSU Ultra trên Kernel của bạn, Kernel sai? + Không phát hiện được Trình điều khiển SukiSU Ultra trên Kernel của bạn, Kernel sai? Phiên bản Kernel Phiên bản SuSFS - Phiên bản trình quản lý + Phiên bản Trình quản lý Trạng thái SELinux Vô hiệu hoá Enforcing @@ -41,7 +41,7 @@ Làm mới Hiển thị ứng dụng hệ thống Ẩn ứng dụng hệ thống - Gửi logs + Gửi nhật ký Chế độ an toàn Khởi động lại để có hiệu lực Các module không khả dụng do xung đột với Magisk! @@ -60,10 +60,9 @@ Bối cảnh SELinux Umount modules Cập nhật Hồ sơ ứng dụng cho %s thất bại - Phiên bản SukiSU Ultra hiện tại %s quá thấp để trình quản lý hoạt động bình thường. Vui lòng cập nhật lên phiên bản %s hoặc cao hơn! + Phiên bản SukiSU Ultra hiện tại %s quá thấp để Trình quản lý hoạt động bình thường. Vui lòng cập nhật lên phiên bản %s hoặc cao hơn! Umount modules Giá trị mặc định chung cho \"Umount modules\" trong Hồ sơ ứng dụng. Nếu được bật, mọi thay đổi hệ thống do các module gây ra sẽ bị gỡ bỏ khỏi hệ thống và các ứng dụng chưa thiết lập hồ sơ - Vô hiệu hoá kprobes hook Bật tùy chọn này sẽ cho phép SukiSU Ultra khôi phục mọi file đã được các module sửa đổi trong ứng dụng này Tên miền Quy tắc @@ -111,7 +110,6 @@ Thiết bị của bạn sẽ **BUỘC** phải khởi động vào phân vùng chưa được sử dụng!\nChỉ sử dụng tùy chọn này sau khi cập nhật OTA hoàn tất.\nTiếp tục? Kế tiếp Phân vùng %1$s được khuyến nghị - (Thử nghiệm) Chọn KMI Gỡ cài đặt Gỡ cài đặt tạm thời @@ -124,9 +122,8 @@ Flash thành công Flash thất bại Đã chọn LKM: %s - Lưu logs - Logs đã được lưu - Chế độ SuS SU: + Lưu nhật ký + Nhật ký đã được lưu Xác nhận cài đặt module %1$s? Module không xác định @@ -238,7 +235,6 @@ Loại file không đúng! Vui lòng chọn file .kpm Gỡ cài đặt KPM sau đây sẽ được gỡ cài đặt: %s - Vô hiệu hoá kprobes hook được tạo bởi SukiSU Ultra, thay vào đó sử dụng inlines hook, tương tự như phương pháp hook của Kernel non-GKI Sử dụng hai ngón tay để phóng to hình ảnh và một ngón tay kéo thả để điều chỉnh vị trí Chọn lại @@ -281,8 +277,6 @@ Cài đặt nâng cao Cài đặt giao diện Trở lại - SuSFS đã bật - SuSFS đã tắt Đã cài đặt hình nền thành công Đã xóa hình nền tùy chỉnh Thay thế icon @@ -312,13 +306,12 @@ Ứng dụng cần được khởi động lại để áp dụng cài đặt DPI mới, không ảnh hưởng đến thanh trạng thái hệ thống hoặc các ứng dụng khác DPI đã được đặt thành %1$d, có hiệu lực sau khi khởi động lại ứng dụng - Ngôn ngữ ứng dụng - Mặc định theo hệ thống - Ngôn ngữ đã thay đổi, khởi động lại để áp dụng thay đổi + Ngôn ngữ ứng dụng + Mặc định theo hệ thống Độ trong suốt của thẻ Error code - Vui lòng kiểm tra logs + Vui lòng kiểm tra nhật ký Đang cài đặt module %1$d/%2$d Cài đặt module %d thất bại Tải xuống module thất bại @@ -366,7 +359,7 @@ Reset về Default Áp dụng - Xác nhận khôi phục + Xác nhận Đặt lại Không tìm thấy file ksu_susfs Thực thi lệnh SuSFS thất bại @@ -411,14 +404,12 @@ Đường dẫn Try Umount đã thêm thành công: %s Đường dẫn Try Umount đã lưu thành công: %s - Xác nhận chạy Try Umount - Thao tác này sẽ áp dụng ngay lập tức tất cả các thiết lập Try Umount đã cấu hình. Bạn có chắc chắn muốn tiếp tục không? - Khôi phục Đường dẫn SuS + Đặt lại Đường dẫn SuS Thao tác này sẽ xóa tất cả các cấu hình Đường dẫn SuS. Bạn có chắc chắn muốn tiếp tục không? - Khôi phục SuS Mount + Đặt lại SuS Mount Thao tác này sẽ xóa tất cả các cấu hình SuS Mount. Bạn có chắc chắn muốn tiếp tục không? - Khôi phục Try Umount + Đặt lại Try Umount Thao tác này sẽ xóa tất cả các cấu hình Try Umount. Bạn có chắc chắn muốn tiếp tục không? Reset Cài đặt Đường dẫn @@ -438,21 +429,20 @@ Hỗ trợ giả mạo Uname Giả mạo Cmdline/Bootconfig Mở hỗ trợ chuyển hướng - Hỗ trợ ghi logs + Hỗ trợ ghi nhật ký Tự động Mount mặc định Tự động Bind Mount Tự động Try Umount Bind Mount Ẩn biểu tượng KSU SuSFS - Hỗ trợ Magic Mount Hỗ trợ SuS Kstat Chức năng chuyển đổi chế độ SuS SU - Nhấn để bật/tắt ghi logs - Kích hoạt logs SuSFS - Bật hoặc tắt ghi logs cho SuSFS - Cấu hình ghi logs SuSFS - Bật ghi logs SuSFS - Tắt ghi logs SuSFS + Nhấn để bật/tắt ghi nhật ký + Kích hoạt nhật ký SuSFS + Bật hoặc tắt ghi nhật ký cho SuSFS + Cấu hình ghi nhật ký SuSFS + Bật ghi nhật ký SuSFS + Tắt ghi nhật ký SuSFS JSON cập nhật JSON URL cập nhật đã được sao chép vào clipboard @@ -489,7 +479,7 @@ Gợi ý: Bạn có thể sử dụng \"default\" để thiết lập giá trị ban đầu Thêm Đường dẫn Kstat Thêm - Khôi phục Cấu hình Kstat + Đặt lại Cấu hình Kstat Bạn có chắc chắn muốn xóa tất cả cấu hình Kstat không? Không thể hoàn tác hành động này Mô tả cấu hình Kstat • add_sus_kstat_statically: Thông tin thống kê cấu hình tĩnh của các File/Folder @@ -511,7 +501,6 @@ Cài đặt hiện tại: %s Ẩn khỏi tất cả các tiến trình Chỉ ẩn đối với các tiến trình không phải KSU - Chạy Hiển thị tóm tắt \"Phiên bản Kernel\" Tóm tắt hiển thị phiên bản Kernel cho ngắn gọn Đường dẫn Android Data đã được đặt thành: %s @@ -562,6 +551,8 @@ Khác Ứng dụng Thêm Đường dẫn ứng dụng + Phiên bản thư viện SuSFS không khớp (Kernel: %1$s & Trình quản lý: %2$s). Nên cập nhật Kernel hoặc Trình quản lý + Cảnh báo Tìm kiếm ứng dụng %1$d ứng dụng đã chọn %1$d ứng dụng đã thêm @@ -591,9 +582,9 @@ Chỉnh sửa Đường dẫn Vòng lặp SuS Đường dẫn Vòng lặp SuS đã thêm thành công: %1$s Đã xoá Đường dẫn Vòng lặp SuS: %1$s - Đã cập nhật Đường dẫn Vòng lặp SuS: %1$s -> %2$s + Đã cập nhật Đường dẫn Vòng lặp SuS: %1$s → %2$s Không có Đường dẫn Vòng lặp SuS nào được cấu hình - Khôi phục Đường dẫn Vòng lặp SuS + Đặt lại Đường dẫn Vòng lặp SuS Bạn có chắc chắn muốn xóa tất cả các Đường dẫn Vòng lặp SuS không? Thao tác này không thể hoàn tác Đường dẫn Vòng lặp /data/example/path @@ -602,7 +593,6 @@ Đường dẫn Vòng lặp Thêm Đường dẫn Vòng lặp - Đường dẫn Vòng lặp SuS Cấu hình Đường dẫn Vòng lặp Đường dẫn Vòng lặp được đổi tên thành SUS_PATH mỗi khi một ứng dụng không phải root hoặc dịch vụ cô lập được khởi động. Điều này giúp giải quyết vấn đề đường dẫn đã thêm có thể trở nên không hợp lệ do trạng thái inode được đặt lại hoặc inode được tạo lại trong Kernel Giả mạo nhật ký AVC @@ -622,4 +612,100 @@ Bật: Kích hoạt tính năng giả mạo sus tcontext của \'su\' thành \'k Các module chưa được ký có thể chưa hoàn chỉnh. Để bảo vệ thiết bị của bạn, module này đã bị chặn cài đặt Các module chưa được ký có thể chưa hoàn chỉnh. Bạn có muốn cài đặt module này từ một tác giả chưa xác định không? Chế độ Hook + + Vá KPM + Thêm tính năng KPM + Vá KPM + Thực hiện Vá KPM vào kernel trước khi flash + Hoàn tác Vá KPM + Hoàn tác Vá KPM đã áp dụng trước đó + Vá KPM đã bật + Hoàn tác Vá KPM đã bật + Chế độ Vá KPM + Chế độ hoàn tác Vá KPM + + Chuẩn bị công cụ Vá KPM + Áp dụng Vá KPM + Hoàn tác Vá KPM + Đã tìm thấy file image: %s + Đã vá KPM thành công + Hoàn tác vá KPM thành công + Nén lại file thành công + + Giải nén file thất bại + Không tìm thấy file image + Vá KPM thất bại + Hoàn tác Vá KPM thất bại + Quá trình Vá KPM thất bại: %s + + Mặc định theo file Kernel + Sử dụng file kernel mặc định mà không có bất kỳ sửa đổi nào về KPM + + Chế độ quét danh sách ứng dụng người dùng + Bật tuỳ chọn này thì chế độ người dùng sẽ được sử dụng để quét danh sách ứng dụng nhằm cải thiện tính ổn định (Nếu danh sách ứng dụng quét kernel bị kẹt và xảy ra các sự cố khác, bạn có thể thử bật tùy chọn này) + Quét ứng dụng nhiều người dùng + Khi được bật, tất cả ứng dụng của người dùng sẽ được quét, bao gồm cả dữ liệu công việc, v.v + Thiết lập thất bại, vui lòng kiểm tra quyền + Dọn dẹp môi trường hoạt động + Dọn dẹp các file hoạt động và dừng quét các dịch vụ + Bạn có chắc chắn muốn dọn dẹp môi trường hoạt động không? Thao tác này sẽ dừng dịch vụ quét và xóa các file liên quan + Dọn dẹp môi trường hoạt động thành công + Dọn dẹp môi trường hoạt động thất bại + + Xác nhận cài đặt + Xác nhận cài đặt (%d files) + Cài đặt + Module + Kernel + Không xác định + Kernel không xác định + Tệp không xác định + Phiên bản + Tác giả + Mô tả + Thiết bị được hỗ trợ + + SuS Maps + Đường dẫn thư viện + /data/adb/modules/my_module/zygisk/arm64-v8a.so + Thêm SuS Map + Chỉnh sửa SuS Map + Đã thêm SuS Map thành công: %1$s + Đã xoá SuS Map: %1$s + Đã cập nhật SuS Map: %1$s → %2$s + Không có SuS Map nào được cấu hình + Đặt lại SuS Map + Thao tác này sẽ xóa tất cả các SuS Map đã cấu hình. Không thể hoàn tác thao tác này + Ẩn Bộ nhớ Map + Ẩn tệp mmapp khỏi các map khác nhau trong /proc/self/ + Ẩn đường dẫn tệp của bộ nhớ map khỏi /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Lưu ý: Tính năng này không hỗ trợ ẩn bộ nhớ map ẩn danh, cũng như không thể ẩn các hook nội tuyến hoặc hook PLT do chính thư viện được đưa vào gây ra + Quan trọng: Đối với các ứng dụng có cơ chế phát hiện tấn công mạnh mẽ, tính năng này có thể không hiệu quả trong việc bỏ qua khả năng phát hiện + Trước tiên, hãy tìm PID và UID của ứng dụng đích bằng lệnh ps -enf. Sau đó kiểm tra các đường dẫn liên quan trong /proc/<pid>/maps và so sánh chúng với số thiết bị trong /proc/1/mountinfo để đảm bảo tính nhất quán. Chỉ khi số thiết bị khớp nhau thì chức năng ẩn map mới hoạt động bình thường + + Trình xem nhật ký + Trở lại + Tìm kiếm + Xoá nhật ký + Bạn có chắc chắn muốn xóa tệp nhật ký đã chọn không? Thao tác này không thể hoàn tác + Đã xoá nhật ký thành công + Lọc theo loại + Tất cả các loại + Hiển thị %1$d trong tổng %2$d nhật ký + Không tìm thấy nhật ký nào + Không tìm thấy nhật ký nào phù hợp + Làm mới + Raw Log + Tìm kiếm theo UID, lệnh hoặc chi tiết… + Xoá tìm kiếm + Xem nhật ký sử dụng + Xem nhật ký truy cập Superuser + Loại trừ các phân loại + Ứng dụng hiện tại + Trang: %1$d/%2$d | Tổng nhật ký: %3$d + Quá nhiều nhật ký, chỉ hiển thị %1$d nhật ký mới nhất + Đang load thêm nhật ký + Tất cả nhật ký đã được hiển thị + + Bạn muốn xoá tôi đi sao 😭 + Hừm, được rồi, tôi sẽ bị bạn gỡ cài đặt. Chức năng root sẽ không ngừng hoạt động chỉ vì bạn mất một Trình quản lý. Đừng lo chỉ Gỡ cài đặt Trình quản lý thôi thì không thể mất quyền truy cập root được đâu, zako~❤️ diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 2141334..29a7e81 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -63,7 +63,6 @@ 当前 KernelSU 版本 %s 过低,管理器无法正常工作,请将内核 KernelSU 版本升级至 %s 或以上! 默认卸载模块 App Profile 中\"卸载模块\"的全局默认值,如果启用,将会为没有设置 Profile 的应用移除所有模块针对系统的修改。 - 禁用 kprobe 钩子 启用该选项后将允许 KernelSU 为本应用还原被模块修改过的文件。 规则 @@ -108,10 +107,12 @@ 直接安装(推荐) 选择一个需要修补的镜像 安装到未使用的槽位(OTA 后) - 将在重启后强制切换到另一个槽位!\n注意只能在 OTA 更新完成后的重启之前使用。\n确认? + 将在重启后强制切换到另一个槽位!注意只能在 OTA 更新完成后的重启之前使用。 下一步 + 选择分区 + 使用本地 LKM 文件 + 仅支持选择 .ko 文件 建议选择 %1$s 分区镜像 - (实验性的) 选择 KMI 卸载 临时卸载 @@ -123,10 +124,9 @@ 刷写中 刷写完成 刷写失败 - 选择的 LKM:%s + 已选择的 LKM:%s 保存日志 日志已保存 - SuS SU 模式: 确认安装模块 %1$s? 未知模块 @@ -163,7 +163,16 @@ 设备 不允许授予 %s 超级用户权限 禁用 su 兼容性 - 临时禁止任何应用程序通过 su 命令获取 Root 权限(现有的 Root 进程不受影响) + 禁止任何应用通过 su 命令获取 root 权限(已运行的 root 进程不受影响)。 + 关闭内核 umount + 关闭 KernelSU 控制的内核级 umount 行为。 + 增强安全性 + 使用更严格的安全策略。 + 内核不支持此功能。 + 此功能由模块管理。 + 默认 + 临时启用 + 始终启用 确定要安装以下 %1$d 个模块吗?\n\n%2$s 更多设置 SELinux @@ -238,7 +247,6 @@ 文件类型不正确,请选择 .kpm 文件 卸载 将卸载以下 KPM 模块:\n%s - 禁用由 KernelSU 创建的 kprobe 钩子,并使用非 kprobe 内联钩子代替,实现方式类似于不支持 kprobe 的非 GKI 内核。 使用双指缩放图片,单指拖动调整位置 重置 @@ -281,8 +289,6 @@ 高级设置 外观设置 返回 - SuSFS 已启用 - SuSFS 已禁用 背景设置成功 已移除自定义背景 备选图标 @@ -312,9 +318,8 @@ 应用需要重启以应用新的 DPI 设置,不会影响系统状态栏或其他应用 DPI 已设置为 %1$d,重启应用后生效 - 应用语言 - 跟随系统 - 语言已更改,重启应用以应用更改 + 应用语言 + 跟随系统 卡片暗度调节 错误代码 @@ -411,8 +416,6 @@ 尝试 umount 路径添加成功: %s 尝试 umount 路径保存成功: %s - 确认运行尝试卸载 - 这将立即执行所有已配置的尝试卸载操作,确定要继续吗? 重置 SuS 路径 这将清除所有 SuS 路径配置,确定要继续吗? @@ -443,7 +446,6 @@ 自动绑定挂载 自动尝试卸载绑定挂载 隐藏 KSU SuSFS 符号 - 魔法坐骑支持 SuS Kstat 支持 SuS SU 模式切换功能 @@ -511,7 +513,6 @@ 当前设置: %s 对所有进程隐藏 仅对非 KSU 进程隐藏 - 运行 内核版本简洁模式 启用或禁用 SukiSU 内核版本显示的简洁模式 Android Data 路径已设置为: %s @@ -562,6 +563,8 @@ 其他 应用 添加应用路径 + SuSFS 库版本不匹配,内核:%1$s vs 管理器:%2$s,建议更新内核或管理器 + 警告 搜索应用 %1$d 个已选应用 %1$d 个已添加应用 @@ -583,6 +586,7 @@ 未知 活跃管理器 无活跃管理器 + SukiSU Zygisk 实现 SuS 循环路径 @@ -595,12 +599,12 @@ 重置循环路径 确定要清空所有 SuS 循环路径吗?此操作无法撤销。 循环路径 + /data/example/path 注意:只有不在 /storage/ 和 /sdcard/ 内的路径才能通过循环路径添加。 错误:循环路径不能位于 /storage/ 或 /sdcard/ 目录内 循环路径 添加循环路径 - SuS 循环路径 循环路径配置 循环路径会在每次非 root 用户应用或隔离服务启动时重新标记为 SUS_PATH。这有助于解决添加的路径可能因 inode 状态重置或内核中 inode 重新创建而失效的问题 AVC 日志欺骗 @@ -620,7 +624,7 @@ 未经签名的模块可能不完整。为了对设备进行保护,已阻止安装此模块。 未经签名的模块可能不完整。你想安装来自未知发布者的模块吗? 钩子类型 - + KPM 修补 用于添加附加的 KPM 功能 KPM 修补 @@ -631,26 +635,21 @@ KPM 撤销修补已启用 KPM 修补模式 KPM 撤销修补模式 - + 准备 KPM 修补工具 正在应用 KPM 修补 正在撤销 KPM 修补 - KPM 工具准备完成 找到 Image 文件: %s KPM 修补成功 KPM 撤销修补成功 文件重新打包完成 - - 提取 kptools 工具失败 - 提取 kpimg 文件失败 - 准备 KPM 工具失败: %s + 解压压缩包失败 未找到 Image 文件 KPM 修补失败 KPM 撤销修补失败 - 重新打包压缩文件失败 KPM 修补操作失败: %s - + 跟随内核 原样使用内核,不进行任何 KPM 修改 @@ -659,11 +658,90 @@ 多用户应用扫描 开启后将扫描所有用户的应用,包括工作资料等 设置失败,请检查权限 - 设置失败: %s 清理运行环境 清理运行时文件并停止扫描服务 您确定要清理运行环境吗?这将停止扫描服务并删除相关文件 运行环境清理成功 运行环境清理失败 - 清理运行环境时出错:%s + + 确认安装 + 确认安装(%d 个文件) + 确认安装 + 模块 + 内核 + 未知类型 + 未知内核 + 未知文件 + 版本 + 作者 + 描述 + 支持设备 + + SUS映射 + 库文件路径 + /data/adb/modules/my_module/zygisk/arm64-v8a.so + 添加SUS映射 + 编辑SUS映射 + SUS映射添加成功: %1$s + SUS映射已移除: %1$s + SUS映射已更新: %1$s -> %2$s + 未配置SUS映射 + 重置SUS映射 + 这将移除所有已配置的SUS映射。此操作无法撤销。 + 内存映射隐藏 + 隐藏/proc/self/中各种映射中的mmap真实文件 + 从 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隐藏内存映射的真实文件路径。请注意:此功能不支持隐藏匿名内存映射,也无法隐藏由注入库本身产生的内联钩子或 PLT 钩子。 + 重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测。 + 首先通过 ps -enf 查找目标应用的 PID 和 UID,然后检查 /proc/<pid>/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作。 + + 日志查看器 + 返回 + 搜索 + 清空日志 + 确定要清空选中的日志文件吗?此操作无法撤销。 + 日志清空成功 + 按类型筛选 + 所有类型 + 显示 %1$d / %2$d 条记录 + 未找到日志 + 未找到匹配的日志 + 刷新 + 原始日志 + 按 UID、命令或详情搜索… + 清除搜索 + 查看使用日志 + 查看 KernelSU 超级用户访问日志 + 排除子类型 + 当前应用 + 页面: %1$d/%2$d | 总日志: %3$d + 日志过多,仅显示最新 %1$d 条 + 加载更多日志 + 已显示所有日志 + + 真要走? + 哼,卸就卸。Root 功能可不会因为失去区区一个管理器就停止运作。别担心,zakozako 只卸载管理器可干不掉 Root 呢,zako~❤️ + 当前管理器与此内核不兼容!请将内核升级至版本 %2$d 或以上(当前 %1$d) + Umount 路径管理 + 管理内核卸载路径 + 添加或删除路径后需要重启设备才能生效。系统会在下次启动时应用新的配置。 + 添加 Umount 路径 + 挂载路径 + 卸载标志 + 0=正常卸载, 8=MNT_DETACH, -1=自动 + 标志 + 默认条目 + 确认删除 + 确定要删除路径 %s 吗? + 路径已添加,重启后生效 + 路径已删除,重启后生效 + 操作失败 + 确认操作 + 确定要清除所有自定义路径吗?(默认路径将保留) + 自定义路径已清除 + 清除自定义 + 应用配置 + 配置已应用到内核 + 包含 %1$d 个应用 + 禁用超级用户日志 + 禁用 KernelSU 超级用户访问记录 diff --git a/manager/app/src/main/res/values-zh-rHK/strings.xml b/manager/app/src/main/res/values-zh-rHK/strings.xml index 9101fc7..e78faec 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -63,7 +63,6 @@ 當前 KernelSU 版本 %s 過低,管理器無法正常工作,請將核心 KernelSU 版本升級至 %s 或以上! 默認卸載模組 App Profile 中\"卸載模組\"嘅全局默認值,如果啟用,將會為冇設定 Profile 嘅應用移除所有模組針對系統嘅修改。 - 禁用 Kprobe Hook 啟用該選項後將允許 KernelSU 為本應用還原被模組修改過嘅文件。 規則 @@ -125,7 +124,6 @@ 選擇嘅 LKM:%s 存儲日誌 日誌已存儲 - SuS SU 模式: 確認安裝模組 %1$s? 未知模組 @@ -235,7 +233,6 @@ 文件類型唔正確,請選擇 .kpm 文件 卸載 將卸載以下 KPM 模組:\n%s - 禁用由 KernelSU 創建嘅 Kprobe Hook,並使用非 Kprobe 內嘅聯鈎子代替,實現方式類似於唔支援 Kprobe 嘅非 GKI 核心。 使用雙指縮放圖片,單指拖動調整位置 重置 @@ -278,8 +275,6 @@ 高級配置 外觀配置 返回 - SuSFS 已啟用 - SuSFS 已禁用 背景設定成功 已移除自定義背景 備選圖標 @@ -309,9 +304,8 @@ 應用需要重新啟動以應用新嘅 DPI 配置,唔會影響系統狀態欄或其他應用 DPI 已設定為 %1$d,重新啟動應用後生效 - 應用語言 - 跟隨系統 - 語言已更改,重新啟動應用以應用更改 + 應用語言 + 跟隨系統 卡片暗度調節 錯誤代碼 @@ -408,8 +402,6 @@ 嘗試 umount 路徑添加成功: %s 嘗試 umount 路徑存儲成功: %s - 確認運行嘗試卸載 - 這將立即執行所有已配置嘅嘗試卸載操作,確定要繼續嗎? 重置 SuS 路徑 這將清除所有 SuS 路徑配置,確定要繼續嗎? @@ -440,7 +432,6 @@ 自動綁定掛載 自動嘗試卸載綁定掛載 隱藏 KSU SuSFS 符號 - 魔法坐騎支援 SuS Kstat 支援 SuS SU 模式切換功能 @@ -508,7 +499,6 @@ 而家嘅配置: %s 對所有程序隱藏 淨係對非 KS> 程序隱藏 - 執行 核心版本簡潔模式 啟用或者禁用 SukiSU 核心版本顯示嘅簡潔模式 Android Data 路徑已配置為: %s @@ -583,7 +573,6 @@ Zygisk 實現 - SuS 循環路徑 循環路徑配置 循環路徑會喺每次非 root 用戶應用程式或者隔離服務啟動時,重新標記做 SUS_PATH。咁樣可以解決因為 inode 狀態重設或者核心重新建立 inode 而令到添加嘅路徑失效嘅問題。 AVC 日誌欺騙 @@ -602,4 +591,13 @@ 未知發布者 未經簽名嘅模組可能唔完整。為咗保護裝置,已經阻止安裝呢個模組。 未經簽名嘅模組可能唔完整。你想唔想安裝嚟自未知發布者嘅模組? + + + + + + + + + diff --git a/manager/app/src/main/res/values-zh-rTW/strings.xml b/manager/app/src/main/res/values-zh-rTW/strings.xml index 2fd6d0f..deac5de 100644 --- a/manager/app/src/main/res/values-zh-rTW/strings.xml +++ b/manager/app/src/main/res/values-zh-rTW/strings.xml @@ -6,7 +6,7 @@ 運作中 版本:%s 不支援 - 未在內核上偵測到 KernelSU 驅動程式,內核錯誤? + 內核上未偵測到 KernelSU 驅動程式,內核錯誤? 內核版本 SuSFS 版本 管理器版本 @@ -63,7 +63,6 @@ 目前 KernelSU 版本 %s 過低,管理器無法正常運作,請將內核 KernelSU 版本升級至 %s 或以上! 預設卸載模組 應用程式設定檔中\"卸載模組\"\的全域預設值,若啟用,將為未設定設定檔的應用程式移除所有模組對系統的修改。 - 禁用 kprobe 切換 啟用此選項後,將允許 KernelSU 為此應用程式還原被模組修改的檔案。 規則 @@ -111,7 +110,6 @@ 將在重新啟動後強制切換至另一槽位!\n注意:僅能在 OTA 更新完成後重新啟動前使用。\n確定繼續? 下一步 建議選擇 %1$s 分區映像檔 - (實驗性的) 選擇 KMI 解除安裝 臨時解除安裝 @@ -126,7 +124,6 @@ 已選擇的 LKM:%s 儲存日誌 日誌已儲存 - SuS SU 模式: 確定安裝模組 %1$s? 未知模組 @@ -238,7 +235,6 @@ 檔案類型不正確,請選擇 .kpm 檔案 解除安裝 將解除安裝以下 KPM 模組:\n%s - 禁用由 KernelSU 建立的 kprobe 掛鉤,並使用非 kprobe 內聯掛鉤代替,實現方式類似於不支援 kprobe 的非 GKI 內核。 使用雙指縮放圖片,單指拖曳調整位置 重置 @@ -281,8 +277,6 @@ 進階設定 外觀設定 返回 - SuSFS 已啟用 - SuSFS 已禁用 背景設定成功 已移除自訂背景 備用圖示 @@ -312,9 +306,8 @@ 應用程式需重新啟動以套用新的 DPI 設定,不會影響系統狀態列或其他應用程式 DPI 已設為 %1$d,重新啟動應用程式後生效 - 應用程式語言 - 跟隨系統 - 語言已更改,重新啟動應用程式以套用變更 + 應用程式語言 + 跟隨系統 卡片暗度調整 錯誤代碼 @@ -411,8 +404,6 @@ 嘗試 umount 路徑新增成功: %s 嘗試 umount 路徑儲存成功: %s - 確認執行嘗試卸載 - 這將立即執行所有已設定的嘗試卸載操作,確定要繼續嗎? 重設 SuS 路徑 這將清除所有 SuS 路徑設定,確定要繼續嗎? @@ -443,7 +434,6 @@ 自動綁定掛載 自動嘗試卸載綁定掛載 隱藏 KSU SuSFS 符號 - 魔法掛載支援 SuS 內核統計支援 SuS SU 模式切換功能 @@ -511,7 +501,6 @@ 目前設定: %s 對所有程序隱藏 僅對非 KSU 程序隱藏 - 執行 內核版本簡潔模式 啟用或停用 SukiSU 內核版本顯示的簡潔模式 Android Data 路徑已設定為: %s @@ -584,7 +573,6 @@ 活躍管理器 無活躍管理器 Zygisk 實現 - SukiSU SuS 循環路徑 新增 SuS 循環路徑 @@ -598,10 +586,9 @@ 循環路徑 注意:只有不在 /storage/ 和 /sdcard/ 內的路徑才能透過循環路徑新增。 錯誤:循環路徑不能位於 /storage/ 或 /sdcard/ 目錄內 - 循環路徑 新增循環路徑 - SuS 循環路徑 + 循環路徑設定 循環路徑會在每次非 root 使用者應用程式或隔離服務啟動時重新標記為 SUS_PATH。這有助於解決新增的路徑可能因 inode 狀態重設或內核中 inode 重新建立而失效的問題 AVC 日誌偽裝 @@ -621,7 +608,7 @@ 未經簽名的模組可能不完整。為了對設備進行保護,已阻止安裝此模組。 未經簽名的模組可能不完整。你想安裝來自未知發布者的模組嗎? 鉤子類型 - + KPM 修補 用於新增附加的 KPM 功能 KPM 修補 @@ -632,26 +619,63 @@ KPM 撤銷修補已啟用 KPM 修補模式 KPM 撤銷修補模式 - + 準備 KPM 修補工具 - 正在套用 KPM 修補 + 正在應用 KPM 修補 正在撤銷 KPM 修補 - KPM 工具準備完成 找到 Image 檔案: %s KPM 修補成功 KPM 撤銷修補成功 檔案重新打包完成 - 提取 kptools 工具失敗 - - 提取 kpimg 檔案失敗 - 準備 KPM 工具失敗: %s - 解壓縮檔失敗 + + 解壓壓縮檔失敗 未找到 Image 檔案 KPM 修補失敗 KPM 撤銷修補失敗 - 重新打包壓縮檔失敗 KPM 修補操作失敗: %s - + 跟隨內核 原樣使用內核,不進行任何 KPM 修改 + + 使用者態掃描應用列表 + 開啟後將使用使用者態掃描應用列表,提高穩定性 (因內核掃描應用列表出現卡死等問題可以嘗試開啟此選項) + 多使用者應用掃描 + 開啟後將掃描所有使用者的應用,包括工作資料等 + 設定失敗,請檢查許可權 + 清理執行環境 + 清理執行時檔案並停止掃描服務 + 您確定要清理執行環境嗎?這將停止掃描服務並刪除相關檔案 + 執行環境清理成功 + 執行環境清理失敗 + + 確認安裝 + 確認安裝 (%d 個檔案) + 確認安裝 + 模組 + 核心 + 未知型別 + 未知內核 + 未知檔案 + 版本 + 作者 + 描述 + 支援的裝置 + + SUS 映射 + 庫檔案路徑 + 新增 SUS 映射 + 編輯 SUS 映射 + SUS 映射新增成功: %1$s + SUS 映射已移除: %1$s + SUS 映射已更新: %1$s -> %2$s + 未設定 SUS 映射 + 重設 SUS 映射 + 這將移除所有已設定的 SUS 映射。此操作無法復原。 + 記憶體映射隱藏 + 隱藏 /proc/self/ 中各種映射裡的 mmap 真實檔案 + 從 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隱藏記憶體映射的真實檔案路徑。請注意:此功能不支援隱藏匿名記憶體映射,也無法隱藏由注入庫本身產生的內聯勾子 (Inline Hooks) 或 PLT 勾子 (PLT Hooks)。 + 重要提示:對於具備完善注入偵測機制的應用程式,此功能可能無法有效繞過偵測。 + 首先透過 ps -enf 尋找目標應用程式的 PID 和 UID,然後檢查 /proc/<pid>/maps 中的相關路徑,並與 /proc/1/mountinfo 中的裝置號碼進行比對以確保一致性。只有當裝置號碼一致時,隱藏映射才能正常運作。 + + diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index f796ada..ab839ee 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -65,7 +65,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Umount modules by default The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set - Disable kprobes hook Enabling this option will allow KernelSU to restore any modified files by the modules for this app Domain Rules @@ -112,8 +111,10 @@ Install to inactive slot (After OTA) Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? Next + Select partition + Use local LKM file + Only .ko files are supported %1$s partition image is recommended - (Unstable) Select KMI Uninstall Uninstall temporarily @@ -128,7 +129,6 @@ Selected LKM: %s Save logs Logs saved - SuS SU mode: Confirm install module %1$s? Unknown module @@ -165,7 +165,16 @@ Device model Granting superuser to %s is not allowed Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (Existing root processes will not be affected) + Disable the ability of any app to gain root privileges via the ⁠su command (existing root processes won\'t be affected). + Disable kernel umount + Disable kernel-level umount behavior controlled by KernelSU. + Enable enhanced security + Enable stricter security policies. + Kernel does not support this feature. + This feature is managed by a module. + Default + Temporarily enable + Permanently enable Sure you want to install the following %1$d modules? \n\n%2$s More settings SELinux @@ -190,6 +199,7 @@ Light Dark Manual Hook + Inline Hook Dynamic colours Dynamic colours using system themes Choose a theme colour @@ -240,7 +250,6 @@ Incorrect file type! Please select .kpm file Uninstall The following KPM will be uninstalled: %s - Disable kprobes hook created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -283,8 +292,6 @@ Advanced Settings Customize the toolbar Comeback - SuSFS enabled - SuSFS disabled Background set successfully Removed custom backgrounds Alternate icon @@ -314,9 +321,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code @@ -413,8 +419,6 @@ Try to umount path added successfully: %s Attempted umount path save succeeded: %s - Confirm Run Try Umount - This will immediately execute all configured try umount operations. Are you sure you want to continue? Reset SUS Paths This will clear all SUS path configurations. Are you sure you want to continue? @@ -445,7 +449,6 @@ Auto Bind Mount Auto Try Umount Bind Mount Hide KSU SUSFS Symbols - Magic Mount Support SUS Kstat Support SUS SU mode switching function @@ -513,7 +516,6 @@ Current setting: %s Hide for all processes Hide only for non-KSU processes - Run Kernel Version Concise Mode Enable or disable the clean mode displayed by the SukiSU kernel version Android Data path has been set to: %s @@ -564,6 +566,8 @@ Other App Add App Path + SuSFS library version mismatch, kernel: %1$s vs manager: %2$s, It is recommended to update the kernel or manager + Warning Search Apps %1$d apps selected %1$d apps already added @@ -604,7 +608,6 @@ Loop Paths Add Loop Path - SUS Loop Path Loop Path Configuration Loop paths are re-flagged as SUS_PATH on each non-root user app or isolated service startup. This helps address issues where added paths may have their inode status reset or inode re-created in the kernel AVC Log Spoofing @@ -643,20 +646,15 @@ Important Note:\n Preparing KPM tools Applying KPM patch Undoing KPM patch - KPM tools prepared Found Image file: %s KPM patch applied successfully KPM patch undone successfully File repacked successfully - Failed to extract kptools - Failed to extract kpimg file - Failed to prepare KPM tools: %s Failed to extract zip file Image file not found KPM patch failed KPM undo patch failed - Failed to repack zip file KPM patch operation failed: %s Follow Kernel @@ -667,11 +665,93 @@ Important Note:\n Multi-User Application Scanning When enabled, scans applications for all users, including work profiles Setting failed, please check permissions - Setting failed: %s Clean Runtime Environment Clean up runtime files and stop the scanner service Are you sure you want to clean the runtime environment? This will stop the scanner service and remove related files. Runtime environment cleaned successfully Failed to clean runtime environment - Error cleaning runtime environment: %s - \ No newline at end of file + + Confirm Installation + Confirm Installation (%d files) + Install + Module + Kernel + Unknown + Unknown Kernel + Unknown File + Version + Author + Description + Supported Devices + + SUS Maps + Library Path + /data/adb/modules/my_module/zygisk/arm64-v8a.so + Add SUS Map + Edit SUS Map + SUS map added successfully: %1$s + SUS map removed: %1$s + SUS map updated: %1$s -> %2$s + No SUS maps configured + Reset SUS Maps + This will remove all configured SUS maps. This action cannot be undone. + Memory Map Hiding + Hide the mmapped real file from various maps in /proc/self/ + Hide the real file paths of memory mappings from /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Please note: This feature does not support hiding anonymous memory mappings, nor can it hide inline hooks or PLT hooks caused by the injected library itself. + Important Notice: For applications with well-implemented injection detection mechanisms, this feature may not effectively bypass detection. + First, find the target application\'s PID and UID using ps -enf, then check the relevant paths in /proc/<pid>/maps and compare the device numbers with those in /proc/1/mountinfo to ensure consistency. Only when the device numbers match can the map hiding function work properly. + + Log Viewer + Back + Search + Clear Logs + Are you sure you want to clear the selected log file? This action cannot be undone. + Logs cleared successfully + Filter by Type + All Types + Showing %1$d of %2$d entries + No logs found + No matching logs found + Refresh + Raw Log + Search by UID, command, or details… + Clear search + View Usage Logs + View KernelSU superuser access logs + Exclude sub-types + Current App + Page: %1$d/%2$d | Total logs: %3$d + Too many logs, showing only the latest %1$d entries + Load More Logs + All logs displayed + + Confirm Uninstallation SukiSU Manager? + Proceeding with the uninstallation will not affect the core functionality of your root access. The root is designed to operate independently of this manager. + + The current manager is incompatible with this kernel! Please upgrade the kernel to version %2$d or higher (currently %1$d) + + Umount Path Management + Manage kernel unmount paths + A reboot is required for changes to take effect. The system will apply the new configuration on the next boot. + Add Umount Path + Mount Path + Unmount Flags + 0=Normal unmount, 8=MNT_DETACH, -1=Auto + Flags + Default Entry + Confirm Delete + Are you sure you want to delete the path %s? + Path added, will take effect after reboot + Path removed, will take effect after reboot + Operation failed + Confirm Action + Are you sure you want to clear all custom paths? (Default paths will be preserved) + Custom paths cleared + Clear Custom Paths + Apply Configuration + Configuration applied to kernel + MNT_DETACH + Contains %d apps + Disable superuser logging + Disable KernelSU superuser access logging + diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index 72ec98b..396caec 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -29,10 +29,12 @@ cmaker { val androidMinSdkVersion = 26 val androidTargetSdkVersion = 36 val androidCompileSdkVersion = 36 -val androidCompileNdkVersion = "28.0.13004108" +val androidBuildToolsVersion = "36.1.0" +val androidCompileNdkVersion by extra(libs.versions.ndk.get()) +val androidCmakeVersion by extra("3.22.0+") val androidSourceCompatibility = JavaVersion.VERSION_21 val androidTargetCompatibility = JavaVersion.VERSION_21 -val managerVersionCode by extra(1 * 10000 + getGitCommitCount() + 700) +val managerVersionCode by extra(4 * 10000 + getGitCommitCount() - 2815) val managerVersionName by extra(getGitDescribe()) fun getGitCommitCount(): Int { @@ -52,6 +54,7 @@ subprojects { extensions.configure(CommonExtension::class.java) { compileSdk = androidCompileSdkVersion ndkVersion = androidCompileNdkVersion + buildToolsVersion = androidBuildToolsVersion defaultConfig { minSdk = androidMinSdkVersion @@ -76,4 +79,4 @@ subprojects { } } } -} \ No newline at end of file +} diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index d846e97..d9e7dd2 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -1,16 +1,16 @@ [versions] accompanist-drawablepainter = "0.37.3" -agp = "8.13.0" +agp = "8.13.1" gson = "2.13.2" -kotlin = "2.2.20" -ksp = "2.2.20-2.0.2" -compose-bom = "2025.09.00" +kotlin = "2.2.21" +ksp = "2.2.21-2.0.4" +compose-bom = "2025.11.00" lifecycle = "2.9.4" -navigation = "2.9.4" +navigation = "2.9.6" activity-compose = "1.11.0" kotlinx-coroutines = "1.10.2" coil-compose = "2.7.0" -compose-destination = "2.2.0" +compose-destination = "2.3.0" sheets-compose-dialogs = "1.3.0" markdown = "4.6.2" webkit = "1.14.0" @@ -19,11 +19,13 @@ parcelablelist = "2.0.1" libsu = "6.0.0" apksign = "1.4" cmaker = "1.2" -compose-material = "1.9.1" -compose-material3 = "1.3.2" -compose-ui = "1.9.1" +compose-material = "1.9.4" +compose-material3 = "1.4.0" +compose-ui = "1.9.4" documentfile = "1.1.0" mmrl = "2bb00b3c2b" +ndk = "29.0.13599879-beta2" +foundation = "1.9.4" [plugins] agp-app = { id = "com.android.application", version.ref = "agp" } @@ -81,11 +83,12 @@ sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs" markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } -lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "29.0.13599879-beta2" } +lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version.ref = "ndk" } androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" } mmrl-webui = { group = "com.github.MMRLApp.MMRL", name = "webui", version.ref = "mmrl" } mmrl-platform = { group = "com.github.MMRLApp.MMRL", name = "platform", version.ref = "mmrl" } mmrl-ui = { group = "com.github.MMRLApp.MMRL", name = "ui", version.ref = "mmrl" } -mmrl-hidden-api = { group = "com.github.MMRLApp.MMRL", name = "hidden-api", version.ref = "mmrl" } \ No newline at end of file +mmrl-hidden-api = { group = "com.github.MMRLApp.MMRL", name = "hidden-api", version.ref = "mmrl" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } \ No newline at end of file diff --git a/manager/gradle/wrapper/gradle-wrapper.properties b/manager/gradle/wrapper/gradle-wrapper.properties index 2a84e18..bad7c24 100644 --- a/manager/gradle/wrapper/gradle-wrapper.properties +++ b/manager/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/manager/gradlew b/manager/gradlew index 23d15a9..c27469b 100755 --- a/manager/gradlew +++ b/manager/gradlew @@ -114,8 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" - # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then @@ -172,7 +170,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +209,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/manager/gradlew.bat b/manager/gradlew.bat index 5eed7ee..8747509 100644 --- a/manager/gradlew.bat +++ b/manager/gradlew.bat @@ -70,11 +70,9 @@ goto fail :execute @rem Setup the command line -set CLASSPATH= - @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/manager/randomizer b/manager/randomizer new file mode 100644 index 0000000..2cd4e4b --- /dev/null +++ b/manager/randomizer @@ -0,0 +1,31 @@ +#! /usr/bin/env bash + +# Generate 3 random lowercase words (6 letters each) +word1=$(tr -dc 'a-z' -#include -#include -#include -#include -#include - -#define KERNEL_SU_OPTION 0xDEADBEEF -#define KSU_OPTIONS 0xdeadbeef - -// KPM控制代码 -#define CMD_KPM_CONTROL 28 -#define CMD_KPM_CONTROL_MAX 7 - -// 控制代码 -// prctl(xxx, 28, "PATH", "ARGS") -// success return 0, error return -N -#define SUKISU_KPM_LOAD 28 - -// prctl(xxx, 29, "NAME") -// success return 0, error return -N -#define SUKISU_KPM_UNLOAD 29 - -// num = prctl(xxx, 30) -// error return -N -// success return +num or 0 -#define SUKISU_KPM_NUM 30 - -// prctl(xxx, 31, Buffer, BufferSize) -// success return +out, error return -N -#define SUKISU_KPM_LIST 31 - -// prctl(xxx, 32, "NAME", Buffer[256]) -// success return +out, error return -N -#define SUKISU_KPM_INFO 32 - -// prctl(xxx, 33, "NAME", "ARGS") -// success return KPM's result value -// error return -N -#define SUKISU_KPM_CONTROL 33 - -// prctl(xxx, 34, buffer, bufferSize) -// success return KPM's result value -// error return -N -#define SUKISU_KPM_VERSION 34 - -#define CONTROL_CODE(n) (n) - -void print_usage(const char *prog) { - printf("Usage: %s [args]\n", prog); - printf("Commands:\n"); - printf(" load Load a KPM module\n"); - printf(" unload Unload a KPM module\n"); - printf(" num Get number of loaded modules\n"); - printf(" list List loaded KPM modules\n"); - printf(" info Get info of a KPM module\n"); - printf(" control Send control command to a KPM module\n"); - printf(" version Print KPM Loader version\n"); -} - -int main(int argc, char *argv[]) { - if (argc < 2) { - print_usage(argv[0]); - return 1; - } - - int ret = -1; - int out = -1; // 存储返回值 - - if (strcmp(argv[1], "load") == 0 && argc >= 3) { - // 加载 KPM 模块 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_LOAD), argv[2], (argc > 3 ? argv[3] : NULL), &out); - if(out > 0) { - printf("Success"); - } - } else if (strcmp(argv[1], "unload") == 0 && argc >= 3) { - // 卸载 KPM 模块 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_UNLOAD), argv[2], NULL, &out); - } else if (strcmp(argv[1], "num") == 0) { - // 获取加载的 KPM 数量 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_NUM), NULL, NULL, &out); - printf("%d", out); - return 0; - } else if (strcmp(argv[1], "list") == 0) { - // 获取模块列表 - char buffer[1024] = {0}; - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_LIST), buffer, sizeof(buffer), &out); - if (out >= 0) { - printf("%s", buffer); - } - } else if (strcmp(argv[1], "info") == 0 && argc >= 3) { - // 获取指定模块信息 - char buffer[256] = {0}; - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_INFO), argv[2], buffer, &out); - if (out >= 0) { - printf("%s\n", buffer); - } - } else if (strcmp(argv[1], "control") == 0 && argc >= 4) { - // 控制 KPM 模块 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_CONTROL), argv[2], argv[3], &out); - } else if (strcmp(argv[1], "version") == 0) { - char buffer[1024] = {0}; - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_VERSION), buffer, sizeof(buffer), &out); - if (out >= 0) { - printf("%s", buffer); - } - } else { - print_usage(argv[0]); - return 1; - } - - if (out < 0) { - printf("Error: %s\n", strerror(-out)); - return -1; - } - - return 0; -} diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock index 67aaca6..aab55b7 100644 --- a/userspace/ksud/Cargo.lock +++ b/userspace/ksud/Cargo.lock @@ -2,20 +2,11 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "adler32" @@ -23,18 +14,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "allocator-api2" version = "0.2.21" @@ -47,12 +26,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_log-sys" version = "0.3.2" @@ -61,9 +34,9 @@ checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" [[package]] name = "android_logger" -version = "0.14.1" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" dependencies = [ "android_log-sys", "env_filter", @@ -81,9 +54,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -96,85 +69,70 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", - "once_cell", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.61.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" dependencies = [ "derive_arbitrary", ] [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" @@ -184,9 +142,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "block-buffer" @@ -199,9 +157,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -211,32 +169,34 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.22" +version = "1.2.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32db95edf998450acc7881c932f94cd9b05c87b4b2599e8bab064753da4acfd1" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", "shlex", ] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -246,9 +206,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.38" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000" +checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" dependencies = [ "clap_builder", "clap_derive", @@ -256,9 +216,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.38" +version = "4.5.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120" +checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" dependencies = [ "anstream", "anstyle", @@ -268,33 +228,33 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "const_format" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" dependencies = [ "const_format_proc_macros", ] @@ -351,9 +311,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -416,9 +376,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -426,21 +386,21 @@ dependencies = [ [[package]] name = "dary_heap" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" +checksum = "06d2e3287df1c007e74221c49ca10a95d557349e54b3a75dc2fb14712c751f04" [[package]] name = "deflate64" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", ] @@ -453,18 +413,18 @@ checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "derive_arbitrary" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] @@ -494,9 +454,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" dependencies = [ "log", ] @@ -536,12 +496,12 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.10" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -572,22 +532,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "filetime" -version = "0.2.26" +name = "find-msvc-tools" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" -dependencies = [ - "cfg-if", - "libc", - "libredox", - "windows-sys 0.60.2", -] +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", "libz-rs-sys", @@ -595,14 +549,10 @@ dependencies = [ ] [[package]] -name = "fs4" -version = "0.13.1" +name = "foldhash" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" -dependencies = [ - "rustix 1.0.7", - "windows-sys 0.59.0", -] +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" [[package]] name = "fsevent-sys" @@ -625,47 +575,36 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.21" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasip2", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" dependencies = [ - "ahash", "allocator-api2", + "equivalent", + "foldhash", ] -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - [[package]] name = "heck" version = "0.5.0" @@ -689,9 +628,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -713,44 +652,58 @@ dependencies = [ [[package]] name = "include-flate" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df49c16750695486c1f34de05da5b7438096156466e7f76c38fcdf285cf0113e" +checksum = "e01b7cb6ca682a621e7cda1c358c9724b53a7b4409be9be1dd443b7f3a26f998" dependencies = [ "include-flate-codegen", - "lazy_static", + "include-flate-compress", "libflate", + "zstd", ] [[package]] name = "include-flate-codegen" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c5b246c6261be723b85c61ecf87804e8ea4a35cb68be0ff282ed84b95ffe7d7" +checksum = "4f49bf5274aebe468d6e6eba14a977eaf1efa481dc173f361020de70c1c48050" dependencies = [ + "include-flate-compress", "libflate", + "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 2.0.110", + "zstd", +] + +[[package]] +name = "include-flate-compress" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae6a40e716bcd5931f5dbb79cd921512a4f647e2e9413fded3171fca3824dbc" +dependencies = [ + "libflate", + "zstd", ] [[package]] name = "indexmap" -version = "2.9.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown", ] [[package]] name = "inotify" -version = "0.9.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.10.0", "inotify-sys", "libc", ] @@ -766,18 +719,18 @@ dependencies = [ [[package]] name = "is_executable" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" +checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" dependencies = [ - "winapi", + "windows-sys 0.60.2", ] [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" @@ -796,10 +749,20 @@ dependencies = [ ] [[package]] -name = "js-sys" -version = "0.3.77" +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" dependencies = [ "once_cell", "wasm-bindgen", @@ -835,6 +798,42 @@ dependencies = [ "libc", ] +[[package]] +name = "ksud" +version = "0.1.0" +dependencies = [ + "android-properties", + "android_logger", + "anyhow", + "chrono", + "clap", + "const_format", + "derive-new", + "encoding_rs", + "env_logger", + "extattr", + "getopts", + "humansize", + "is_executable", + "java-properties", + "jwalk", + "libc", + "log", + "nom", + "notify", + "regex-lite", + "rust-embed", + "rustix 0.38.34", + "serde", + "serde_json", + "sha1", + "sha256", + "tempfile", + "which", + "zip 6.0.0", + "zip-extensions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -843,15 +842,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.172" +version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" [[package]] name = "libflate" -version = "2.1.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" +checksum = "e3248b8d211bd23a104a42d81b4fa8bb8ac4a3b75e7a43d85d2c9ccb6179cd74" dependencies = [ "adler32", "core2", @@ -862,12 +861,12 @@ dependencies = [ [[package]] name = "libflate_lz77" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" +checksum = "a599cb10a9cd92b1300debcef28da8f70b935ec937f44fcd1b70a7c986a11c5c" dependencies = [ "core2", - "hashbrown 0.14.5", + "hashbrown", "rle-decode-fast", ] @@ -877,17 +876,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" -[[package]] -name = "libredox" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" -dependencies = [ - "bitflags 2.8.0", - "libc", - "redox_syscall", -] - [[package]] name = "libz-rs-sys" version = "0.5.2" @@ -905,15 +893,15 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lzma-rs" @@ -925,6 +913,16 @@ dependencies = [ "crc", ] +[[package]] +name = "lzma-rust2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2", +] + [[package]] name = "lzma-sys" version = "0.1.20" @@ -938,29 +936,30 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] @@ -974,23 +973,28 @@ dependencies = [ [[package]] name = "notify" -version = "6.1.1" +version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.8.0", - "crossbeam-channel", - "filetime", + "bitflags 2.10.0", "fsevent-sys", "inotify", "kqueue", "libc", "log", "mio", + "notify-types", "walkdir", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] +[[package]] +name = "notify-types" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d" + [[package]] name = "num-conv" version = "0.1.0" @@ -1006,21 +1010,18 @@ dependencies = [ "autocfg", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1040,59 +1041,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] -name = "proc-macro2" -version = "1.0.95" +name = "proc-macro-error" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] -[[package]] -name = "procfs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" -dependencies = [ - "bitflags 2.8.0", - "chrono", - "flate2", - "hex", - "procfs-core", - "rustix 0.38.44", -] - -[[package]] -name = "procfs-core" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" -dependencies = [ - "bitflags 2.8.0", - "chrono", - "hex", -] - [[package]] name = "quote" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1100,28 +1100,19 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.5.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" -dependencies = [ - "bitflags 2.8.0", -] - [[package]] name = "regex-lite" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" +checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" [[package]] name = "rle-decode-fast" @@ -1131,9 +1122,9 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" [[package]] name = "rust-embed" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "025908b8682a26ba8d12f6f2d66b987584a4a87bc024abc5bbc12553a8cd178a" +checksum = "947d7f3fad52b283d261c4c99a084937e2fe492248cb9a68a8435a861b8798ca" dependencies = [ "include-flate", "rust-embed-impl", @@ -1143,40 +1134,34 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6065f1a4392b71819ec1ea1df1120673418bf386f50de1d6f54204d836d4349c" +checksum = "5fa2c8c9e8711e10f9c4fd2d64317ef13feaab820a4c51541f1a8c8e2e851ab2" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn", + "syn 2.0.110", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.7.2" +version = "8.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6cc0c81648b20b70c491ff8cce00c1c3b223bb8ed2b5d41f0e54c6c4c0a3594" +checksum = "60b161f275cb337fe0a44d924a5f4df0ed69c2c39519858f931ce61c779d3475" dependencies = [ "sha2", "walkdir", ] -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - [[package]] name = "rustix" version = "0.38.34" -source = "git+https://github.com/Kernel-SU/rustix.git?branch=main#4a53fbc7cb7a07cabe87125cc21dbc27db316259" +source = "git+https://github.com/Kernel-SU/rustix.git?rev=4a53fbc7cb7a07cabe87125cc21dbc27db316259#4a53fbc7cb7a07cabe87125cc21dbc27db316259" dependencies = [ - "bitflags 2.8.0", - "errno 0.3.10", + "bitflags 2.10.0", + "errno 0.3.14", "itoa", "libc", "linux-raw-sys 0.4.15", @@ -1186,35 +1171,22 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.44" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.8.0", - "errno 0.3.10", + "bitflags 2.10.0", + "errno 0.3.14", "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" -dependencies = [ - "bitflags 2.8.0", - "errno 0.3.10", - "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.59.0", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1233,34 +1205,45 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] @@ -1318,9 +1301,19 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.101" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" dependencies = [ "proc-macro2", "quote", @@ -1329,22 +1322,22 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.0.7", - "windows-sys 0.59.0", + "rustix 1.1.2", + "windows-sys 0.61.2", ] [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "num-conv", @@ -1355,38 +1348,37 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "tokio" -version = "1.45.0" +version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "backtrace", "bytes", "pin-project-lite", ] [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" [[package]] name = "unicode-xid" @@ -1423,45 +1415,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.2+wasi-0.2.4" +name = "wasip2" +version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1469,35 +1448,34 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn", - "wasm-bindgen-backend", + "syn 2.0.110", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" dependencies = [ "unicode-ident", ] [[package]] name = "which" -version = "7.0.3" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +checksum = "d3fabb953106c3c8eea8306e4393700d7657561cb43122571b172bbfb7c7ba1d" dependencies = [ - "either", "env_home", - "rustix 1.0.7", + "rustix 1.1.2", "winsafe", ] @@ -1519,11 +1497,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1534,9 +1512,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-core" -version = "0.61.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -1547,59 +1525,50 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.110", ] [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -1609,37 +1578,22 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-link", ] [[package]] @@ -1660,27 +1614,21 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -1689,15 +1637,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -1707,15 +1649,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -1725,9 +1661,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -1737,15 +1673,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -1755,15 +1685,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -1773,15 +1697,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -1791,15 +1709,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -1809,9 +1721,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winsafe" @@ -1820,13 +1732,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.8.0", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "xz2" @@ -1837,63 +1746,6 @@ dependencies = [ "lzma-sys", ] -[[package]] -name = "zakozako" -version = "0.1.0" -dependencies = [ - "android-properties", - "android_logger", - "anyhow", - "chrono", - "clap", - "const_format", - "derive-new", - "encoding_rs", - "env_logger", - "extattr", - "fs4", - "getopts", - "humansize", - "is_executable", - "java-properties", - "jwalk", - "libc", - "log", - "nom", - "notify", - "procfs", - "regex-lite", - "rust-embed", - "rustix 0.38.34", - "serde_json", - "sha1", - "sha256", - "tempfile", - "which", - "zip", - "zip-extensions", -] - -[[package]] -name = "zerocopy" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zip" version = "3.0.0" @@ -1902,23 +1754,38 @@ checksum = "12598812502ed0105f607f941c386f43d441e00148fce9dec3ca5ffb0bde9308" dependencies = [ "arbitrary", "crc32fast", - "deflate64", "flate2", "indexmap", "lzma-rs", "memchr", - "time", "xz2", "zopfli", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "arbitrary", + "crc32fast", + "deflate64", + "flate2", + "indexmap", + "lzma-rust2", + "memchr", + "time", + "zopfli", +] + [[package]] name = "zip-extensions" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f105becb0a5da773e655775dd05fee454ca1475bcc980ec9d940a02f42cee40" dependencies = [ - "zip", + "zip 3.0.0", ] [[package]] @@ -1929,12 +1796,40 @@ checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" [[package]] name = "zopfli" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" dependencies = [ "bumpalo", "crc32fast", "log", "simd-adler32", ] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index e9abb9c..ec56275 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -1,16 +1,16 @@ [package] -name = "zakozako" +name = "ksud" version = "0.1.0" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -notify = "6.1" +notify = "8.2" anyhow = "1" clap = { version = "4", features = ["derive"] } const_format = "0.2" -zip = { version = "3", features = [ +zip = { version = "6", features = [ "deflate", "deflate64", "time", @@ -38,25 +38,24 @@ rust-embed = { version = "8", features = [ "debug-embed", "compression", # must clean build after updating binaries ] } -which = "7" +which = "8" getopts = "0.2" sha256 = "1" sha1 = "0.10" tempfile = "3" chrono = "0.4" regex-lite = "0.1" -fs4 = "0.13" +serde = { version = "1.0", features = ["derive"] } [target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies] -rustix = { git = "https://github.com/Kernel-SU/rustix.git", branch = "main", features = [ +rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc7cb7a07cabe87125cc21dbc27db316259", features = [ "all-apis", ] } # some android specific dependencies which compiles under unix are also listed here for convenience of coding android-properties = { version = "0.2", features = ["bionic-deprecated"] } -procfs = "0.17" [target.'cfg(target_os = "android")'.dependencies] -android_logger = { version = "0.14", default-features = false } +android_logger = { version = "0.15", default-features = false } [profile.release] overflow-checks = false @@ -64,3 +63,4 @@ codegen-units = 1 lto = "fat" opt-level = 3 strip = true +split-debuginfo = "unpacked" diff --git a/userspace/ksud/bin/aarch64/ksuinit b/userspace/ksud/bin/aarch64/ksuinit index 8d3ba05..df67742 100755 Binary files a/userspace/ksud/bin/aarch64/ksuinit and b/userspace/ksud/bin/aarch64/ksuinit differ diff --git a/userspace/ksud/bin/aarch64/resetprop b/userspace/ksud/bin/aarch64/resetprop index 2dc7d0a..dd58ca4 100644 Binary files a/userspace/ksud/bin/aarch64/resetprop and b/userspace/ksud/bin/aarch64/resetprop differ diff --git a/userspace/ksud/bin/x86_64/ksuinit b/userspace/ksud/bin/x86_64/ksuinit index 9517baf..11dde60 100755 Binary files a/userspace/ksud/bin/x86_64/ksuinit and b/userspace/ksud/bin/x86_64/ksuinit differ diff --git a/userspace/ksud/bin/x86_64/resetprop b/userspace/ksud/bin/x86_64/resetprop index 8003061..9048971 100644 Binary files a/userspace/ksud/bin/x86_64/resetprop and b/userspace/ksud/bin/x86_64/resetprop differ diff --git a/userspace/ksud/build.rs b/userspace/ksud/build.rs index b833723..fd935d6 100644 --- a/userspace/ksud/build.rs +++ b/userspace/ksud/build.rs @@ -1,8 +1,4 @@ -use std::env; -use std::fs::File; -use std::io::Write; -use std::path::Path; -use std::process::Command; +use std::{env, fs::File, io::Write, path::Path, process::Command}; fn get_git_version() -> Result<(u32, String), std::io::Error> { let output = Command::new("git") @@ -14,8 +10,8 @@ fn get_git_version() -> Result<(u32, String), std::io::Error> { let version_code: u32 = version_code .trim() .parse() - .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse git count"))?; - let version_code = 10000 + 700 + version_code; // For historical reasons + .map_err(|_| std::io::Error::other("Failed to parse git count"))?; + let version_code = 40000 - 2815 + version_code; // For historical reasons let version_name = String::from_utf8( Command::new("git") @@ -23,7 +19,7 @@ fn get_git_version() -> Result<(u32, String), std::io::Error> { .output()? .stdout, ) - .map_err(|_| std::io::Error::other("Failed to read git describe stdout"))?; + .map_err(|_| std::io::Error::other("Failed to parse git count"))?; let version_name = version_name.trim_start_matches('v').to_string(); Ok((version_code, version_name)) } diff --git a/userspace/ksud/src/boot_patch.rs b/userspace/ksud/src/boot_patch.rs index 4e4d3e7..531b422 100644 --- a/userspace/ksud/src/boot_patch.rs +++ b/userspace/ksud/src/boot_patch.rs @@ -1,22 +1,19 @@ #[cfg(unix)] -use std::os::unix::fs::PermissionsExt; -use std::path::Path; -use std::path::PathBuf; -use std::process::Command; -use std::process::Stdio; +use std::{ + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, + process::{Command, Stdio}, +}; -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use anyhow::bail; -use anyhow::ensure; +use anyhow::{Context, Result, anyhow, bail, ensure}; use regex_lite::Regex; use which::which; -use crate::defs; -use crate::defs::BACKUP_FILENAME; -use crate::defs::{KSU_BACKUP_DIR, KSU_BACKUP_FILE_PREFIX}; -use crate::{assets, utils}; +use crate::{ + assets, + defs::{self, BACKUP_FILENAME, KSU_BACKUP_DIR, KSU_BACKUP_FILE_PREFIX}, + utils, +}; #[cfg(target_os = "android")] fn ensure_gki_kernel() -> Result<()> { @@ -27,7 +24,7 @@ fn ensure_gki_kernel() -> Result<()> { } #[cfg(target_os = "android")] -pub fn get_kernel_version() -> Result<(i32, i32, i32)> { +fn get_kernel_version() -> Result<(i32, i32, i32)> { let uname = rustix::system::uname(); let version = uname.release().to_string_lossy(); let re = Regex::new(r"(\d+)\.(\d+)\.(\d+)")?; @@ -118,11 +115,11 @@ fn parse_kmi_from_kernel(kernel: &PathBuf, workdir: &Path) -> Result { let re = Regex::new(r"(?:.* )?(\d+\.\d+)(?:\S+)?(android\d+)").context("Failed to compile regex")?; for s in printable_strings { - if let Some(caps) = re.captures(s) { - if let (Some(kernel_version), Some(android_version)) = (caps.get(1), caps.get(2)) { - let kmi = format!("{}-{}", android_version.as_str(), kernel_version.as_str()); - return Ok(kmi); - } + if let Some(caps) = re.captures(s) + && let (Some(kernel_version), Some(android_version)) = (caps.get(1), caps.get(2)) + { + let kmi = format!("{}-{}", android_version.as_str(), kernel_version.as_str()); + return Ok(kmi); } } println!("- Failed to get KMI version"); @@ -153,126 +150,40 @@ fn parse_kmi_from_boot(magiskboot: &Path, image: &PathBuf, workdir: &Path) -> Re parse_kmi_from_kernel(&image_path, workdir) } -fn do_cpio_cmd(magiskboot: &Path, workdir: &Path, cmd: &str) -> Result<()> { +fn do_cpio_cmd(magiskboot: &Path, workdir: &Path, cpio_path: &Path, cmd: &str) -> Result<()> { let status = Command::new(magiskboot) .current_dir(workdir) .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("cpio") - .arg("ramdisk.cpio") + .arg(cpio_path) .arg(cmd) .status()?; - ensure!(status.success(), "magiskboot cpio {} failed", cmd); Ok(()) } -fn do_vendor_init_boot_cpio_cmd(magiskboot: &Path, workdir: &Path, cmd: &str) -> Result<()> { - let vendor_init_boot_cpio = workdir.join("vendor_ramdisk").join("init_boot.cpio"); +fn is_magisk_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result { let status = Command::new(magiskboot) .current_dir(workdir) .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("cpio") - .arg(vendor_init_boot_cpio) - .arg(cmd) + .arg(cpio_path) + .arg("test") .status()?; - - ensure!(status.success(), "magiskboot cpio {} failed", cmd); - Ok(()) + // 0: stock, 1: magisk + Ok(status.code() == Some(1)) } -fn do_vendor_ramdisk_cpio_cmd(magiskboot: &Path, workdir: &Path, cmd: &str) -> Result<()> { - let vendor_ramdisk_cpio = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); +fn is_kernelsu_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result { let status = Command::new(magiskboot) .current_dir(workdir) .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("cpio") - .arg(vendor_ramdisk_cpio) - .arg(cmd) - .status()?; - - ensure!(status.success(), "magiskboot cpio {} failed", cmd); - Ok(()) -} - -fn is_magisk_patched(magiskboot: &Path, workdir: &Path) -> Result { - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args(["cpio", "ramdisk.cpio", "test"]) - .status()?; - - // 0: stock, 1: magisk - Ok(status.code() == Some(1)) -} - -fn is_magisk_patched_vendor_init_boot(magiskboot: &Path, workdir: &Path) -> Result { - let vendor_init_boot_cpio = workdir.join("vendor_ramdisk").join("init_boot.cpio"); - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args(["cpio", vendor_init_boot_cpio.to_str().unwrap(), "test"]) - .status()?; - - // 0: stock, 1: magisk - Ok(status.code() == Some(1)) -} - -fn is_magisk_patched_vendor_ramdisk(magiskboot: &Path, workdir: &Path) -> Result { - let vendor_ramdisk_cpio = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args(["cpio", vendor_ramdisk_cpio.to_str().unwrap(), "test"]) - .status()?; - - // 0: stock, 1: magisk - Ok(status.code() == Some(1)) -} - -fn is_kernelsu_patched(magiskboot: &Path, workdir: &Path) -> Result { - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args(["cpio", "ramdisk.cpio", "exists kernelsu.ko"]) - .status()?; - - Ok(status.success()) -} - -fn is_kernelsu_patched_vendor_init_boot(magiskboot: &Path, workdir: &Path) -> Result { - let vendor_ramdisk_cpio = workdir.join("vendor_ramdisk").join("init_boot.cpio"); - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args([ - "cpio", - vendor_ramdisk_cpio.to_str().unwrap(), - "exists kernelsu.ko", - ]) - .status()?; - - Ok(status.success()) -} - -fn is_kernelsu_patched_vendor_ramdisk(magiskboot: &Path, workdir: &Path) -> Result { - let vendor_ramdisk_cpio = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .args([ - "cpio", - vendor_ramdisk_cpio.to_str().unwrap(), - "exists kernelsu.ko", - ]) + .arg(cpio_path) + .arg("exists kernelsu.ko") .status()?; Ok(status.success()) @@ -308,10 +219,7 @@ pub fn restore( let kmi = get_current_kmi().unwrap_or_else(|_| String::from("")); - let skip_init = kmi.starts_with("android12-"); - - let (bootimage, bootdevice) = - find_boot_image(&image, skip_init, false, false, workdir, &magiskboot)?; + let (bootimage, bootdevice) = find_boot_image(&image, &kmi, false, false, workdir, &None)?; println!("- Unpacking boot image"); let status = Command::new(&magiskboot) @@ -323,33 +231,36 @@ pub fn restore( .status()?; ensure!(status.success(), "magiskboot unpack failed"); - let no_ramdisk = !workdir.join("ramdisk.cpio").exists(); - // let no_ramdisk = !workdir.join("ramdisk.cpio").exists(); - let no_vendor_init_boot = !workdir - .join("vendor_ramdisk") - .join("init_boot.cpio") - .exists(); - let no_vendor_ramdisk = !workdir.join("vendor_ramdisk").join("ramdisk.cpio").exists(); - let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir)?; - let is_kernelsu_patched_vendor_init_boot = - is_kernelsu_patched_vendor_init_boot(&magiskboot, workdir)?; - let is_kernelsu_patched_vendor_ramdisk = - is_kernelsu_patched_vendor_ramdisk(&magiskboot, workdir)?; - ensure!( - is_kernelsu_patched - || is_kernelsu_patched_vendor_init_boot - || is_kernelsu_patched_vendor_ramdisk, - "boot image is not patched by KernelSU" - ); + let mut ramdisk = workdir.join("ramdisk.cpio"); + if !ramdisk.exists() { + ramdisk = workdir.join("vendor_ramdisk").join("init_boot.cpio") + } + if !ramdisk.exists() { + ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); + } + if !ramdisk.exists() { + bail!("No compatible ramdisk found.") + } + let ramdisk = ramdisk.as_path(); + let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?; + ensure!(is_kernelsu_patched, "boot image is not patched by KernelSU"); let mut new_boot = None; let mut from_backup = false; #[cfg(target_os = "android")] - if do_cpio_cmd(&magiskboot, workdir, &format!("exists {BACKUP_FILENAME}")).is_ok() { + if do_cpio_cmd( + &magiskboot, + workdir, + ramdisk, + &format!("exists {BACKUP_FILENAME}"), + ) + .is_ok() + { do_cpio_cmd( &magiskboot, workdir, + ramdisk, &format!("extract {BACKUP_FILENAME} {BACKUP_FILENAME}"), )?; let sha = std::fs::read(workdir.join(BACKUP_FILENAME))?; @@ -372,81 +283,13 @@ pub fn restore( } if new_boot.is_none() { - if !no_ramdisk { - println!("- Restoring /ramdisk"); - println!("- Removing /ramdisk/kernelsu.ko"); - // remove kernelsu.ko - do_cpio_cmd(&magiskboot, workdir, "rm kernelsu.ko")?; + // remove kernelsu.ko + do_cpio_cmd(&magiskboot, workdir, ramdisk, "rm kernelsu.ko")?; - // if init.real exists, restore it - println!("- Checking if init.real exists"); - let status = do_cpio_cmd(&magiskboot, workdir, "exists init.real").is_ok(); - if status { - println!("- /ramdisk/init.real exists"); - println!("- Restoring /ramdisk/init.real to init"); - do_cpio_cmd(&magiskboot, workdir, "mv init.real init")?; - } else { - println!("- /ramdisk/init.real not found"); - println!("- Removing ramdisk.cpio"); - let ramdisk = workdir.join("ramdisk.cpio"); - std::fs::remove_file(ramdisk)?; - } - } else if !no_vendor_init_boot { - println!("- Restoring /vendor_ramdisk/init_boot"); - println!("- Removing /vendor_ramdisk/init_boot/kernelsu.ko"); - // vendor init_boot restore - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "rm kernelsu.ko")?; - - println!("- Checking if init.real exists"); - let status = - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "exists init.real").is_ok(); - if status { - println!("- /vendor_ramdisk/init_boot/init.real exists"); - println!("- Restoring /vendor_ramdisk/init_boot/init.real to init"); - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "mv init.real init")?; - } else { - println!("- /vendor_ramdisk/init_boot/init.real not found"); - println!("- Removing vendor_ramdisk/init_boot.cpio"); - let vendor_init_boot = workdir.join("vendor_ramdisk").join("init_boot.cpio"); - std::fs::remove_file(vendor_init_boot)?; - } - } else if !no_vendor_ramdisk { - println!("- Restoring /vendor_ramdisk/ramdisk"); - println!("- Removing /vendor_ramdisk/ramdisk/kernelsu.ko"); - // vendor ramdisk restore - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "rm kernelsu.ko")?; - - let status = - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "exists init.real").is_ok(); - if status { - println!("- /vendor_ramdisk/ramdisk/init.real exists"); - println!("- Restoring /vendor_ramdisk/ramdisk/init.real to init"); - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "mv init.real init")?; - } else { - println!("- /vendor_ramdisk/ramdisk/init.real not found"); - println!("- Removing vendor_ramdisk/ramdisk.cpio"); - let vendor_ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); - std::fs::remove_file(vendor_ramdisk)?; - } - } else { - println!("- Restoring /ramdisk"); - println!("- Removing /ramdisk/kernelsu.ko"); - // remove kernelsu.ko - do_cpio_cmd(&magiskboot, workdir, "rm kernelsu.ko")?; - - // if init.real exists, restore it - println!("- Checking if init.real exists"); - let status = do_cpio_cmd(&magiskboot, workdir, "exists init.real").is_ok(); - if status { - println!("- /ramdisk/init.real exists"); - println!("- Restoring /ramdisk/init.real to init"); - do_cpio_cmd(&magiskboot, workdir, "mv init.real init")?; - } else { - println!("- /ramdisk/init.real not found"); - println!("- Removing ramdisk.cpio"); - let ramdisk = workdir.join("ramdisk.cpio"); - std::fs::remove_file(ramdisk)?; - } + // if init.real exists, restore it + let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists init.real").is_ok(); + if status { + do_cpio_cmd(&magiskboot, workdir, ramdisk, "mv init.real init")?; } println!("- Repacking boot image"); @@ -455,7 +298,7 @@ pub fn restore( .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("repack") - .arg(bootimage.display().to_string()) + .arg(&bootimage) .status()?; ensure!(status.success(), "magiskboot repack failed"); new_boot = Some(workdir.join("new-boot.img")); @@ -501,8 +344,11 @@ pub fn patch( out: Option, magiskboot: Option, kmi: Option, + partition: Option, ) -> Result<()> { - let result = do_patch(image, kernel, kmod, init, ota, flash, out, magiskboot, kmi); + let result = do_patch( + image, kernel, kmod, init, ota, flash, out, magiskboot, kmi, partition, + ); if let Err(ref e) = result { println!("- Install Error: {e}"); } @@ -520,6 +366,7 @@ fn do_patch( out: Option, magiskboot_path: Option, kmi: Option, + partition: Option, ) -> Result<()> { println!(include_str!("banner")); @@ -574,18 +421,10 @@ fn do_patch( } }; - let skip_init = kmi.starts_with("android12-"); + let (bootimage, bootdevice) = + find_boot_image(&image, &kmi, ota, is_replace_kernel, workdir, &partition)?; - let (bootimage, bootdevice) = find_boot_image( - &image, - skip_init, - ota, - is_replace_kernel, - workdir, - &magiskboot, - )?; - - let bootimage = bootimage.display().to_string(); + let bootimage = bootimage.as_path(); // try extract magiskboot/bootctl let _ = assets::ensure_binaries(false); @@ -620,101 +459,49 @@ fn do_patch( .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("unpack") - .arg(&bootimage) + .arg(bootimage) .status()?; ensure!(status.success(), "magiskboot unpack failed"); - let no_ramdisk = !workdir.join("ramdisk.cpio").exists(); - let no_vendor_init_boot = !workdir - .join("vendor_ramdisk") - .join("init_boot.cpio") - .exists(); - let no_vendor_ramdisk = !workdir.join("vendor_ramdisk").join("ramdisk.cpio").exists(); - if no_ramdisk && no_vendor_init_boot && no_vendor_ramdisk { - println!("- No compatible ramdisk found."); - println!("- Will create our own ramdisk!"); + let mut ramdisk = workdir.join("ramdisk.cpio"); + if !ramdisk.exists() { + ramdisk = workdir.join("vendor_ramdisk").join("init_boot.cpio") } - let is_magisk_patched = is_magisk_patched(&magiskboot, workdir)?; - let is_magisk_patched_vendor_init_boot = - is_magisk_patched_vendor_init_boot(&magiskboot, workdir)?; - let is_magisk_patched_vendor_ramdisk = is_magisk_patched_vendor_ramdisk(&magiskboot, workdir)?; - ensure!( - !is_magisk_patched - || !is_magisk_patched_vendor_init_boot - || !is_magisk_patched_vendor_ramdisk, - "Cannot work with Magisk patched image" - ); + if !ramdisk.exists() { + ramdisk = workdir.join("vendor_ramdisk").join("ramdisk.cpio"); + } + if !ramdisk.exists() { + println!("- No ramdisk, create by default"); + ramdisk = "ramdisk.cpio".into(); + } + let ramdisk = ramdisk.as_path(); + let is_magisk_patched = is_magisk_patched(&magiskboot, workdir, ramdisk)?; + ensure!(!is_magisk_patched, "Cannot work with Magisk patched image"); println!("- Adding KernelSU LKM"); - let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir)?; - let is_kernelsu_patched_vendor_init_boot = - is_kernelsu_patched_vendor_init_boot(&magiskboot, workdir)?; - let is_kernelsu_patched_vendor_ramdisk = - is_kernelsu_patched_vendor_ramdisk(&magiskboot, workdir)?; + let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?; let mut need_backup = false; - if (no_ramdisk && !is_kernelsu_patched_vendor_init_boot) - || (no_ramdisk && no_vendor_init_boot && !is_kernelsu_patched_vendor_ramdisk) - || !is_kernelsu_patched - { - if !no_ramdisk { - println!("- Checking if /ramdisk/init exists"); - let status = do_cpio_cmd(&magiskboot, workdir, "exists init"); - if status.is_ok() { - println!("- Backing up ramdisk/init"); - do_cpio_cmd(&magiskboot, workdir, "mv init init.real")?; - } - need_backup = flash; - } else if !no_vendor_init_boot { - println!("- Checking if /vendor_ramdisk/init_boot/init exists"); - let status = do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "exists init"); - if status.is_ok() { - println!("- Backing up vendor_ramdisk/init_boot/init"); - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "mv init init.real")?; - } - need_backup = flash; - } else if !no_vendor_ramdisk { - println!("- Checking if /vendor_ramdisk/ramdisk/init exists"); - let status = do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "exists init"); - if status.is_ok() { - println!("- Backing up vendor_ramdisk/ramdisk/init"); - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "mv init init.real")?; - } - need_backup = flash; - } else { - println!("- Checking if /ramdisk/init exists"); - let status = do_cpio_cmd(&magiskboot, workdir, "exists init"); - if status.is_ok() { - println!("- Backing up ramdisk/init"); - do_cpio_cmd(&magiskboot, workdir, "mv init init.real")?; - } - need_backup = flash; + if !is_kernelsu_patched { + // kernelsu.ko is not exist, backup init if necessary + let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, "exists init"); + if status.is_ok() { + do_cpio_cmd(&magiskboot, workdir, ramdisk, "mv init init.real")?; } + need_backup = flash; } - if !no_ramdisk { - println!("- Patching /ramdisk"); - do_cpio_cmd(&magiskboot, workdir, "add 0755 init init")?; - do_cpio_cmd(&magiskboot, workdir, "add 0755 kernelsu.ko kernelsu.ko")?; - } else if !no_vendor_init_boot { - println!("- Patching /vendor_ramdisk/init_boot"); - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "add 0755 init init")?; - do_vendor_init_boot_cpio_cmd(&magiskboot, workdir, "add 0755 kernelsu.ko kernelsu.ko")?; - } else if !no_vendor_ramdisk { - println!("- Patching /vendor_ramdisk/ramdisk"); - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "add 0750 init init")?; - do_vendor_ramdisk_cpio_cmd(&magiskboot, workdir, "add 0750 kernelsu.ko kernelsu.ko")?; - } else { - println!("- Creating and Patching /ramdisk"); - do_cpio_cmd(&magiskboot, workdir, "add 0755 init init")?; - do_cpio_cmd(&magiskboot, workdir, "add 0755 kernelsu.ko kernelsu.ko")?; - } + do_cpio_cmd(&magiskboot, workdir, ramdisk, "add 0755 init init")?; + do_cpio_cmd( + &magiskboot, + workdir, + ramdisk, + "add 0755 kernelsu.ko kernelsu.ko", + )?; #[cfg(target_os = "android")] - if need_backup { - if let Err(e) = do_backup(&magiskboot, workdir, &bootimage) { - println!("- Backup stock image failed: {e}"); - } + if need_backup && let Err(e) = do_backup(&magiskboot, workdir, ramdisk, bootimage) { + println!("- Backup stock image failed: {e}"); } println!("- Repacking boot image"); @@ -724,7 +511,7 @@ fn do_patch( .stdout(Stdio::null()) .stderr(Stdio::null()) .arg("repack") - .arg(&bootimage) + .arg(bootimage) .status()?; ensure!(status.success(), "magiskboot repack failed"); let new_boot = workdir.join("new-boot.img"); @@ -779,7 +566,7 @@ fn calculate_sha1(file_path: impl AsRef) -> Result { } #[cfg(target_os = "android")] -fn do_backup(magiskboot: &Path, workdir: &Path, image: &str) -> Result<()> { +fn do_backup(magiskboot: &Path, workdir: &Path, cpio_path: &Path, image: &Path) -> Result<()> { let sha1 = calculate_sha1(image)?; let filename = format!("{KSU_BACKUP_FILE_PREFIX}{sha1}"); @@ -791,6 +578,7 @@ fn do_backup(magiskboot: &Path, workdir: &Path, image: &str) -> Result<()> { do_cpio_cmd( magiskboot, workdir, + cpio_path, &format!("add 0755 {BACKUP_FILENAME} {BACKUP_FILENAME}"), )?; println!("- Stock image has been backup to"); @@ -860,143 +648,109 @@ fn find_magiskboot(magiskboot_path: Option, workdir: &Path) -> Result

, - skip_init: bool, + kmi: &str, ota: bool, is_replace_kernel: bool, workdir: &Path, - magiskboot: &Path, + partition: &Option, ) -> Result<(PathBuf, Option)> { let bootimage; let mut bootdevice = None; if let Some(ref image) = *image { - ensure!(image.exists(), "- Boot image not found"); + ensure!(image.exists(), "boot image not found"); bootimage = std::fs::canonicalize(image)?; } else { if cfg!(not(target_os = "android")) { println!("- Current OS is not android, refusing auto bootimage/bootdevice detection"); - bail!("- Please specify a boot image"); - } - let mut slot_suffix = - utils::getprop("ro.boot.slot_suffix").unwrap_or_else(|| String::from("")); - - if !slot_suffix.is_empty() && ota { - if slot_suffix == "_a" { - slot_suffix = "_b".to_string() - } else { - slot_suffix = "_a".to_string() - } - }; - - let init_boot_partition = format!("/dev/block/by-name/init_boot{slot_suffix}"); - let vendor_boot_partition = format!("/dev/block/by-name/vendor_boot{slot_suffix}"); - let boot_partition = format!("/dev/block/by-name/boot{slot_suffix}"); - - let init_boot_exist = Path::new(&init_boot_partition).exists(); - let vendor_boot_exist = Path::new(&vendor_boot_partition).exists(); - - // helper: unpack a partition and check for a ramdisk and init - fn unpack_and_check_init( - magiskboot: &Path, - workdir: &Path, - partition: &str, - ramdisk_cpio: &str, - ) -> Result { - let tmp_img = workdir.join("probe.img"); - dd(partition, &tmp_img)?; - let status = Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .arg("unpack") - .arg(&tmp_img) - .status()?; - if !status.success() { - let _ = std::fs::remove_file(&tmp_img); - return Ok(false); - } - let ramdisk_path = workdir.join(ramdisk_cpio); - let has_init = if ramdisk_path.exists() { - Command::new(magiskboot) - .current_dir(workdir) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .arg("cpio") - .arg(ramdisk_cpio) - .arg("exists init") - .status() - .map(|s| s.success()) - .unwrap_or(false) - } else { - false - }; - // Clean up - let _ = std::fs::remove_file(&tmp_img); - let _ = std::fs::remove_file(workdir.join("ramdisk.cpio")); - let _ = std::fs::remove_dir_all(workdir.join("vendor_ramdisk")); - Ok(has_init) + bail!("Please specify a boot image"); } - let mut selected_partition = &boot_partition; + let slot_suffix = get_slot_suffix(ota); + let boot_partition_name = choose_boot_partition(kmi, is_replace_kernel, partition); + let boot_partition = format!("/dev/block/by-name/{boot_partition_name}{slot_suffix}"); - if !is_replace_kernel && init_boot_exist && !skip_init { - // try init_boot/ramdisk.cpio - if unpack_and_check_init(magiskboot, workdir, &init_boot_partition, "ramdisk.cpio")? { - println!("- Using init_boot partition (ramdisk.cpio)."); - selected_partition = &init_boot_partition; - } - } - - // try vendor_boot/vendor_ramdisk/init_boot.cpio - if selected_partition == &boot_partition - && !is_replace_kernel - && vendor_boot_exist - && !skip_init - { - if unpack_and_check_init( - magiskboot, - workdir, - &vendor_boot_partition, - "vendor_ramdisk/init_boot.cpio", - )? { - println!("- Using vendor_boot partition (vendor_ramdisk/init_boot.cpio)."); - selected_partition = &vendor_boot_partition; - } - } - - // try vendor_boot/vendor_ramdisk/ramdisk.cpio - if selected_partition == &boot_partition - && !is_replace_kernel - && vendor_boot_exist - && !skip_init - { - if unpack_and_check_init( - magiskboot, - workdir, - &vendor_boot_partition, - "vendor_ramdisk/ramdisk.cpio", - )? { - println!("- Using vendor_boot partition (vendor_ramdisk/ramdisk.cpio)."); - selected_partition = &vendor_boot_partition; - } - } - - if selected_partition == &boot_partition { - println!("- Using boot partition (ramdisk.cpio)."); - } - - println!("- Bootdevice: {selected_partition}"); + println!("- Bootdevice: {boot_partition}"); let tmp_boot_path = workdir.join("boot.img"); - dd(selected_partition, &tmp_boot_path)?; + dd(&boot_partition, &tmp_boot_path)?; - ensure!(tmp_boot_path.exists(), "- Tmp boot image not found"); + ensure!(tmp_boot_path.exists(), "boot image not found"); bootimage = tmp_boot_path; - bootdevice = Some(selected_partition.to_string()); + bootdevice = Some(boot_partition); }; Ok((bootimage, bootdevice)) } +#[cfg(target_os = "android")] +pub fn choose_boot_partition( + kmi: &str, + is_replace_kernel: bool, + partition: &Option, +) -> String { + let slot_suffix = get_slot_suffix(false); + let skip_init_boot = kmi.starts_with("android12-"); + let init_boot_exist = Path::new(&format!("/dev/block/by-name/init_boot{slot_suffix}")).exists(); + + // if specific partition is specified, use it + if let Some(part) = partition { + return match part.as_str() { + "boot" | "init_boot" | "vendor_boot" => part.clone(), + _ => "boot".to_string(), + }; + } + + // if init_boot exists and not skipping it, use it + if !is_replace_kernel && init_boot_exist && !skip_init_boot { + return "init_boot".to_string(); + } + + "boot".to_string() +} + +#[cfg(not(target_os = "android"))] +pub fn choose_boot_partition( + _kmi: &str, + _is_replace_kernel: bool, + _partition: &Option, +) -> String { + "boot".to_string() +} + +#[cfg(target_os = "android")] +pub fn get_slot_suffix(ota: bool) -> String { + let mut slot_suffix = utils::getprop("ro.boot.slot_suffix").unwrap_or_else(|| String::from("")); + if !slot_suffix.is_empty() && ota { + if slot_suffix == "_a" { + slot_suffix = "_b".to_string() + } else { + slot_suffix = "_a".to_string() + } + } + slot_suffix +} + +#[cfg(not(target_os = "android"))] +pub fn get_slot_suffix(_ota: bool) -> String { + String::new() +} + +#[cfg(target_os = "android")] +pub fn list_available_partitions() -> Vec { + let slot_suffix = get_slot_suffix(false); + let candidates = vec!["boot", "init_boot", "vendor_boot"]; + candidates + .into_iter() + .filter(|name| Path::new(&format!("/dev/block/by-name/{}{}", name, slot_suffix)).exists()) + .map(|s| s.to_string()) + .collect() +} + +#[cfg(not(target_os = "android"))] +pub fn list_available_partitions() -> Vec { + Vec::new() +} + fn post_ota() -> Result<()> { use crate::defs::ADB_DIR; use assets::BOOTCTL_PATH; @@ -1034,4 +788,4 @@ rm -f /data/adb/post-fs-data.d/post_ota.sh std::fs::set_permissions(post_ota_sh, std::fs::Permissions::from_mode(0o755))?; Ok(()) -} \ No newline at end of file +} diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index bf4835f..7afbe99 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -1,13 +1,12 @@ use anyhow::{Ok, Result}; use clap::Parser; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; #[cfg(target_os = "android")] use android_logger::Config; #[cfg(target_os = "android")] use log::LevelFilter; -use crate::defs::KSUD_VERBOSE_LOG_FILE; use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils}; /// KernelSU userspace cli @@ -16,9 +15,6 @@ use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils}; struct Args { #[command(subcommand)] command: Commands, - - #[arg(short, long, default_value_t = cfg!(debug_assertions))] - verbose: bool, } #[derive(clap::Subcommand, Debug)] @@ -63,6 +59,12 @@ enum Commands { command: Profile, }, + /// Manage kernel features + Feature { + #[command(subcommand)] + command: Feature, + }, + /// Patch boot or init_boot images to apply KernelSU BootPatch { /// boot image path, if not specified, will try to find the boot image automatically @@ -100,6 +102,10 @@ enum Commands { /// KMI version, if specified, will use the specified KMI #[arg(long, default_value = None)] kmi: Option, + + /// target partition override (init_boot | boot | vendor_boot) + #[arg(long, default_value = None)] + partition: Option, }, /// Restore boot or init_boot images patched by KernelSU @@ -122,11 +128,30 @@ enum Commands { #[command(subcommand)] command: BootInfo, }, + + /// KPM module manager + #[cfg(target_arch = "aarch64")] + Kpm { + #[command(subcommand)] + command: kpm_cmd::Kpm, + }, + + /// Manage kernel umount paths + Umount { + #[command(subcommand)] + command: Umount, + }, + /// For developers Debug { #[command(subcommand)] command: Debug, }, + /// Kernel interface + Kernel { + #[command(subcommand)] + command: Kernel, + }, } #[derive(clap::Subcommand, Debug)] @@ -135,7 +160,23 @@ enum BootInfo { CurrentKmi, /// show supported kmi versions - SupportedKmi, + SupportedKmis, + + /// check if device is A/B capable + IsAbDevice, + + /// show auto-selected boot partition name + DefaultPartition, + + /// list available partitions for current or OTA toggled slot + AvailablePartitions, + + /// show slot suffix for current or OTA toggled slot + SlotSuffix { + /// toggle to another slot + #[arg(short = 'u', long, default_value = "false")] + ota: bool, + }, } #[derive(clap::Subcommand, Debug)] @@ -143,7 +184,7 @@ enum Debug { /// Set the manager app, kernel CONFIG_KSU_DEBUG should be enabled. SetManager { /// manager package name - #[arg(default_value_t = String::from("me.weishu.kernelsu"))] + #[arg(default_value_t = String::from("com.sukisu.ultra"))] apk: String, }, @@ -163,10 +204,41 @@ enum Debug { /// Get kernel version Version, - Mount, - /// For testing Test, + + /// Process mark management + Mark { + #[command(subcommand)] + command: MarkCommand, + }, +} + +#[derive(clap::Subcommand, Debug)] +enum MarkCommand { + /// Get mark status for a process (or all) + Get { + /// target pid (0 for total count) + #[arg(default_value = "0")] + pid: i32, + }, + + /// Mark a process + Mark { + /// target pid (0 for all processes) + #[arg(default_value = "0")] + pid: i32, + }, + + /// Unmark a process + Unmark { + /// target pid (0 for all processes) + #[arg(default_value = "0")] + pid: i32, + }, + + /// Refresh mark for all running processes + Refresh, } #[derive(clap::Subcommand, Debug)] @@ -198,14 +270,14 @@ enum Module { zip: String, }, - /// Uninstall module - Uninstall { + /// Undo module uninstall mark + UndoUninstall { /// module id id: String, }, - /// Restore module - Restore { + /// Uninstall module + Uninstall { /// module id id: String, }, @@ -230,6 +302,51 @@ enum Module { /// list all modules List, + + /// manage module configuration + Config { + #[command(subcommand)] + command: ModuleConfigCmd, + }, +} + +#[derive(clap::Subcommand, Debug)] +enum ModuleConfigCmd { + /// Get a config value + Get { + /// config key + key: String, + }, + + /// Set a config value + Set { + /// config key + key: String, + /// config value + value: String, + /// use temporary config (cleared on reboot) + #[arg(short, long)] + temp: bool, + }, + + /// List all config entries + List, + + /// Delete a config entry + Delete { + /// config key + key: String, + /// delete from temporary config + #[arg(short, long)] + temp: bool, + }, + + /// Clear all config entries + Clear { + /// clear temporary config + #[arg(short, long)] + temp: bool, + }, } #[derive(clap::Subcommand, Debug)] @@ -272,6 +389,134 @@ enum Profile { ListTemplates, } +#[derive(clap::Subcommand, Debug)] +enum Feature { + /// Get feature value and support status + Get { + /// Feature ID or name (su_compat, kernel_umount) + id: String, + }, + + /// Set feature value + Set { + /// Feature ID or name + id: String, + /// Feature value (0=disable, 1=enable) + value: u64, + }, + + /// List all available features + List, + + /// Check feature status (supported/unsupported/managed) + Check { + /// Feature ID or name (su_compat, kernel_umount) + id: String, + }, + + /// Load configuration from file and apply to kernel + Load, + + /// Save current kernel feature states to file + Save, +} + +#[derive(clap::Subcommand, Debug)] +enum Kernel { + /// Nuke ext4 sysfs + NukeExt4Sysfs { + /// mount point + mnt: String, + }, + /// Manage umount list + Umount { + #[command(subcommand)] + command: UmountOp, + }, + /// Notify that module is mounted + NotifyModuleMounted, +} + +#[derive(clap::Subcommand, Debug)] +enum UmountOp { + /// Add mount point to umount list + Add { + /// mount point path + mnt: String, + /// umount flags (default: 0, MNT_DETACH: 2) + #[arg(short, long, default_value = "0")] + flags: u32, + }, + /// Delete mount point from umount list + Del { + /// mount point path + mnt: String, + }, + /// Wipe all entries from umount list + Wipe, +} + +#[cfg(target_arch = "aarch64")] +mod kpm_cmd { + use clap::Subcommand; + use std::path::PathBuf; + + #[derive(Subcommand, Debug)] + pub enum Kpm { + /// Load a KPM module: load [args] + Load { path: PathBuf, args: Option }, + /// Unload a KPM module: unload + Unload { name: String }, + /// Get number of loaded modules + Num, + /// List loaded KPM modules + List, + /// Get info of a KPM module: info + Info { name: String }, + /// Send control command to a KPM module: control + Control { name: String, args: String }, + /// Print KPM Loader version + Version, + } +} + +#[derive(clap::Subcommand, Debug)] +enum Umount { + /// Add custom umount path + Add { + /// Mount path to add + path: String, + + /// Check mount type (overlay) + #[arg(long, default_value = "false")] + + /// Umount flags (0 or 8 for MNT_DETACH) + #[arg(long, default_value = "-1")] + flags: i32, + }, + + /// Remove custom umount path + Remove { + /// Mount path to remove + path: String, + }, + + /// List all umount paths + List, + + /// Clear all recorded umount paths + ClearCustom, + + /// Save configuration to file + Save, + + /// Load and apply configuration from file + Load, + + /// Apply current configuration to kernel + Apply, +} + pub fn run() -> Result<()> { #[cfg(target_os = "android")] android_logger::init_once( @@ -291,10 +536,6 @@ pub fn run() -> Result<()> { let cli = Args::parse(); - if !cli.verbose && !Path::new(KSUD_VERBOSE_LOG_FILE).exists() { - log::set_max_level(LevelFilter::Info); - } - log::info!("command: {:?}", cli.command); let result = match cli.command { @@ -308,12 +549,72 @@ pub fn run() -> Result<()> { } match command { Module::Install { zip } => module::install_module(&zip), + Module::UndoUninstall { id } => module::undo_uninstall_module(&id), Module::Uninstall { id } => module::uninstall_module(&id), - Module::Restore { id } => module::restore_uninstall_module(&id), Module::Enable { id } => module::enable_module(&id), Module::Disable { id } => module::disable_module(&id), Module::Action { id } => module::run_action(&id), Module::List => module::list_modules(), + Module::Config { command } => { + // Get module ID from environment variable + let module_id = std::env::var("KSU_MODULE").map_err(|_| { + anyhow::anyhow!("This command must be run in the context of a module") + })?; + + use crate::module_config; + match command { + ModuleConfigCmd::Get { key } => { + // Use merge_configs to respect priority (temp overrides persist) + let config = module_config::merge_configs(&module_id)?; + match config.get(&key) { + Some(value) => { + println!("{}", value); + Ok(()) + } + None => anyhow::bail!("Key '{}' not found", key), + } + } + ModuleConfigCmd::Set { key, value, temp } => { + // Validate input at CLI layer for better user experience + module_config::validate_config_key(&key)?; + module_config::validate_config_value(&value)?; + + let config_type = if temp { + module_config::ConfigType::Temp + } else { + module_config::ConfigType::Persist + }; + module_config::set_config_value(&module_id, &key, &value, config_type) + } + ModuleConfigCmd::List => { + let config = module_config::merge_configs(&module_id)?; + if config.is_empty() { + println!("No config entries found"); + } else { + for (key, value) in config { + println!("{}={}", key, value); + } + } + Ok(()) + } + ModuleConfigCmd::Delete { key, temp } => { + let config_type = if temp { + module_config::ConfigType::Temp + } else { + module_config::ConfigType::Persist + }; + module_config::delete_config_value(&module_id, &key, config_type) + } + ModuleConfigCmd::Clear { temp } => { + let config_type = if temp { + module_config::ConfigType::Temp + } else { + module_config::ConfigType::Persist + }; + module_config::clear_config(&module_id, config_type) + } + } + } } } Commands::Install { magiskboot } => utils::install(magiskboot), @@ -335,6 +636,15 @@ pub fn run() -> Result<()> { Profile::ListTemplates => crate::profile::list_templates(), }, + Commands::Feature { command } => match command { + Feature::Get { id } => crate::feature::get_feature(id), + Feature::Set { id, value } => crate::feature::set_feature(id, value), + Feature::List => crate::feature::list_features(), + Feature::Check { id } => crate::feature::check_feature(id), + Feature::Load => crate::feature::load_config_and_apply(), + Feature::Save => crate::feature::save_config(), + }, + Commands::Debug { command } => match command { Debug::SetManager { apk } => debug::set_manager(&apk), Debug::GetSign { apk } => { @@ -347,8 +657,13 @@ pub fn run() -> Result<()> { Ok(()) } Debug::Su { global_mnt } => crate::su::grant_root(global_mnt), - Debug::Mount => init_event::mount_modules_systemlessly(), Debug::Test => assets::ensure_binaries(false), + Debug::Mark { command } => match command { + MarkCommand::Get { pid } => debug::mark_get(pid), + MarkCommand::Mark { pid } => debug::mark_set(pid), + MarkCommand::Unmark { pid } => debug::mark_unset(pid), + MarkCommand::Refresh => debug::mark_refresh(), + }, }, Commands::BootPatch { @@ -361,7 +676,10 @@ pub fn run() -> Result<()> { out, magiskboot, kmi, - } => crate::boot_patch::patch(boot, kernel, module, init, ota, flash, out, magiskboot, kmi), + partition, + } => crate::boot_patch::patch( + boot, kernel, module, init, ota, flash, out, magiskboot, kmi, partition, + ), Commands::BootInfo { command } => match command { BootInfo::CurrentKmi => { @@ -370,17 +688,80 @@ pub fn run() -> Result<()> { // return here to avoid printing the error message return Ok(()); } - BootInfo::SupportedKmi => { + BootInfo::SupportedKmis => { let kmi = crate::assets::list_supported_kmi()?; kmi.iter().for_each(|kmi| println!("{kmi}")); return Ok(()); } + BootInfo::IsAbDevice => { + let val = crate::utils::getprop("ro.build.ab_update") + .unwrap_or_else(|| String::from("false")); + let is_ab = val.trim().to_lowercase() == "true"; + println!("{}", if is_ab { "true" } else { "false" }); + return Ok(()); + } + BootInfo::DefaultPartition => { + let kmi = crate::boot_patch::get_current_kmi().unwrap_or_else(|_| String::from("")); + let name = crate::boot_patch::choose_boot_partition(&kmi, false, &None); + println!("{name}"); + return Ok(()); + } + BootInfo::SlotSuffix { ota } => { + let suffix = crate::boot_patch::get_slot_suffix(ota); + println!("{suffix}"); + return Ok(()); + } + BootInfo::AvailablePartitions => { + let parts = crate::boot_patch::list_available_partitions(); + parts.iter().for_each(|p| println!("{p}")); + return Ok(()); + } }, Commands::BootRestore { boot, magiskboot, flash, } => crate::boot_patch::restore(boot, magiskboot, flash), + Commands::Kernel { command } => match command { + Kernel::NukeExt4Sysfs { mnt } => ksucalls::nuke_ext4_sysfs(&mnt), + 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), + }, + Kernel::NotifyModuleMounted => { + ksucalls::report_module_mounted(); + Ok(()) + } + }, + #[cfg(target_arch = "aarch64")] + Commands::Kpm { command } => { + use crate::cli::kpm_cmd::Kpm; + match command { + Kpm::Load { path, args } => { + crate::kpm::kpm_load(path.to_str().unwrap(), args.as_deref()) + } + Kpm::Unload { name } => crate::kpm::kpm_unload(&name), + Kpm::Num => crate::kpm::kpm_num().map(|_| ()), + Kpm::List => crate::kpm::kpm_list(), + Kpm::Info { name } => crate::kpm::kpm_info(&name), + Kpm::Control { name, args } => { + let ret = crate::kpm::kpm_control(&name, &args)?; + println!("{}", ret); + Ok(()) + } + Kpm::Version => crate::kpm::kpm_version_loader(), + } + } + Commands::Umount { command } => match command { + Umount::Add { path, flags } => crate::umount_manager::add_umount_path(&path, flags), + Umount::Remove { path } => crate::umount_manager::remove_umount_path(&path), + Umount::List => crate::umount_manager::list_umount_paths(), + Umount::ClearCustom => crate::umount_manager::clear_custom_paths(), + Umount::Save => crate::umount_manager::save_umount_config(), + Umount::Load => crate::umount_manager::load_and_apply_config(), + Umount::Apply => crate::umount_manager::apply_config_to_kernel(), + }, }; if let Err(e) = &result { diff --git a/userspace/ksud/src/debug.rs b/userspace/ksud/src/debug.rs index 6a693c1..fa99688 100644 --- a/userspace/ksud/src/debug.rs +++ b/userspace/ksud/src/debug.rs @@ -1,9 +1,11 @@ -use anyhow::{Context, Ok, Result, ensure}; +use anyhow::{Context, Ok, Result, bail, ensure}; use std::{ path::{Path, PathBuf}, process::Command, }; +use crate::ksucalls; + const KERNEL_PARAM_PATH: &str = "/sys/module/kernelsu"; fn read_u32(path: &PathBuf) -> Result { @@ -50,3 +52,47 @@ pub fn set_manager(pkg: &str) -> Result<()> { let _ = Command::new("am").args(["force-stop", pkg]).status(); Ok(()) } + +/// Get mark status for a process +pub fn mark_get(pid: i32) -> Result<()> { + let result = ksucalls::mark_get(pid)?; + if pid == 0 { + bail!("Please specify a pid to get its mark status"); + } else { + println!( + "Process {} mark status: {}", + pid, + if result != 0 { "marked" } else { "unmarked" } + ); + } + Ok(()) +} + +/// Mark a process +pub fn mark_set(pid: i32) -> Result<()> { + ksucalls::mark_set(pid)?; + if pid == 0 { + println!("All processes marked successfully"); + } else { + println!("Process {} marked successfully", pid); + } + Ok(()) +} + +/// Unmark a process +pub fn mark_unset(pid: i32) -> Result<()> { + ksucalls::mark_unset(pid)?; + if pid == 0 { + println!("All processes unmarked successfully"); + } else { + println!("Process {} unmarked successfully", pid); + } + Ok(()) +} + +/// Refresh mark for all running processes +pub fn mark_refresh() -> Result<()> { + ksucalls::mark_refresh()?; + println!("Refreshed mark for all running processes"); + Ok(()) +} diff --git a/userspace/ksud/src/defs.rs b/userspace/ksud/src/defs.rs index 8033ee2..2792542 100644 --- a/userspace/ksud/src/defs.rs +++ b/userspace/ksud/src/defs.rs @@ -10,7 +10,6 @@ pub const PROFILE_SELINUX_DIR: &str = concatcp!(PROFILE_DIR, "selinux/"); pub const PROFILE_TEMPLATE_DIR: &str = concatcp!(PROFILE_DIR, "templates/"); pub const KSURC_PATH: &str = concatcp!(WORKING_DIR, ".ksurc"); -pub const KSU_MOUNT_SOURCE: &str = "KSU"; pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, "ksud"); pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot"); @@ -18,18 +17,24 @@ pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, "magiskboot"); pub const DAEMON_LINK_PATH: &str = concatcp!(BINARY_DIR, "ksud"); pub const MODULE_DIR: &str = concatcp!(ADB_DIR, "modules/"); - -// warning: this directory should not change, or you need to change the code in module_installer.sh!!! pub const MODULE_UPDATE_DIR: &str = concatcp!(ADB_DIR, "modules_update/"); - -pub const KSUD_VERBOSE_LOG_FILE: &str = concatcp!(ADB_DIR, "verbose"); +pub const METAMODULE_DIR: &str = concatcp!(ADB_DIR, "metamodule/"); pub const MODULE_WEB_DIR: &str = "webroot"; pub const MODULE_ACTION_SH: &str = "action.sh"; pub const DISABLE_FILE_NAME: &str = "disable"; pub const UPDATE_FILE_NAME: &str = "update"; pub const REMOVE_FILE_NAME: &str = "remove"; -pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount"; + +// Module config system +pub const MODULE_CONFIG_DIR: &str = concatcp!(WORKING_DIR, "module_configs/"); +pub const PERSIST_CONFIG_NAME: &str = "persist.config"; +pub const TEMP_CONFIG_NAME: &str = "tmp.config"; + +// Metamodule support +pub const METAMODULE_MOUNT_SCRIPT: &str = "metamount.sh"; +pub const METAMODULE_METAINSTALL_SCRIPT: &str = "metainstall.sh"; +pub const METAMODULE_METAUNINSTALL_SCRIPT: &str = "metauninstall.sh"; pub const VERSION_CODE: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_CODE")); pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_NAME")); @@ -37,6 +42,3 @@ pub const VERSION_NAME: &str = include_str!(concat!(env!("OUT_DIR"), "/VERSION_N pub const KSU_BACKUP_DIR: &str = WORKING_DIR; pub const KSU_BACKUP_FILE_PREFIX: &str = "ksu_backup_"; pub const BACKUP_FILENAME: &str = "stock_image.sha1"; - -pub const NO_TMPFS_PATH: &str = concatcp!(WORKING_DIR, ".notmpfs"); -pub const NO_MOUNT_PATH: &str = concatcp!(WORKING_DIR, ".nomount"); diff --git a/userspace/ksud/src/feature.rs b/userspace/ksud/src/feature.rs new file mode 100644 index 0000000..626828d --- /dev/null +++ b/userspace/ksud/src/feature.rs @@ -0,0 +1,454 @@ +use anyhow::{Context, Result, bail}; +use const_format::concatcp; +use std::collections::HashMap; +use std::fs::File; +use std::io::{Read, Write}; +use std::path::Path; + +use crate::defs; + +const FEATURE_CONFIG_PATH: &str = concatcp!(defs::WORKING_DIR, ".feature_config"); +const FEATURE_MAGIC: u32 = 0x7f4b5355; +const FEATURE_VERSION: u32 = 1; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u32)] +pub enum FeatureId { + SuCompat = 0, + KernelUmount = 1, + EnhancedSecurity = 2, + SuLog = 3, +} + +impl FeatureId { + pub fn from_u32(id: u32) -> Option { + match id { + 0 => Some(FeatureId::SuCompat), + 1 => Some(FeatureId::KernelUmount), + 2 => Some(FeatureId::EnhancedSecurity), + 3 => Some(FeatureId::SuLog), + _ => None, + } + } + + pub fn name(&self) -> &'static str { + match self { + FeatureId::SuCompat => "su_compat", + FeatureId::KernelUmount => "kernel_umount", + FeatureId::EnhancedSecurity => "enhanced_security", + FeatureId::SuLog => "sulog", + } + } + + pub fn description(&self) -> &'static str { + match self { + FeatureId::SuCompat => { + "SU Compatibility Mode - allows authorized apps to gain root via traditional 'su' command" + } + FeatureId::KernelUmount => { + "Kernel Umount - controls whether kernel automatically unmounts modules when not needed" + } + FeatureId::EnhancedSecurity => { + "Enhanced Security - disable non‑KSU root elevation and unauthorized UID downgrades" + } + FeatureId::SuLog => { + "SU Log - enables logging of SU command usage to kernel log for auditing purposes" + } + } + } +} + +fn parse_feature_id(name: &str) -> Result { + match name { + "su_compat" | "0" => Ok(FeatureId::SuCompat), + "kernel_umount" | "1" => Ok(FeatureId::KernelUmount), + "enhanced_security" | "2" => Ok(FeatureId::EnhancedSecurity), + "sulog" | "3" => Ok(FeatureId::SuLog), + _ => bail!("Unknown feature: {}", name), + } +} + +pub fn load_binary_config() -> Result> { + let path = Path::new(FEATURE_CONFIG_PATH); + if !path.exists() { + log::info!("Feature config not found, using defaults"); + return Ok(HashMap::new()); + } + + let mut file = File::open(path).with_context(|| "Failed to open feature config")?; + + let mut magic_buf = [0u8; 4]; + file.read_exact(&mut magic_buf) + .with_context(|| "Failed to read magic")?; + let magic = u32::from_le_bytes(magic_buf); + + if magic != FEATURE_MAGIC { + bail!( + "Invalid feature config magic: expected 0x{:08x}, got 0x{:08x}", + FEATURE_MAGIC, + magic + ); + } + + let mut version_buf = [0u8; 4]; + file.read_exact(&mut version_buf) + .with_context(|| "Failed to read version")?; + let version = u32::from_le_bytes(version_buf); + + if version != FEATURE_VERSION { + log::warn!( + "Feature config version mismatch: expected {}, got {}", + FEATURE_VERSION, + version + ); + } + + let mut count_buf = [0u8; 4]; + file.read_exact(&mut count_buf) + .with_context(|| "Failed to read count")?; + let count = u32::from_le_bytes(count_buf); + + let mut features = HashMap::new(); + for _ in 0..count { + let mut id_buf = [0u8; 4]; + let mut value_buf = [0u8; 8]; + + file.read_exact(&mut id_buf) + .with_context(|| "Failed to read feature id")?; + file.read_exact(&mut value_buf) + .with_context(|| "Failed to read feature value")?; + + let id = u32::from_le_bytes(id_buf); + let value = u64::from_le_bytes(value_buf); + + features.insert(id, value); + } + + log::info!("Loaded {} features from config", features.len()); + Ok(features) +} + +pub fn save_binary_config(features: &HashMap) -> Result<()> { + crate::utils::ensure_dir_exists(Path::new(defs::WORKING_DIR))?; + + let path = Path::new(FEATURE_CONFIG_PATH); + let mut file = File::create(path).with_context(|| "Failed to create feature config")?; + + file.write_all(&FEATURE_MAGIC.to_le_bytes()) + .with_context(|| "Failed to write magic")?; + + file.write_all(&FEATURE_VERSION.to_le_bytes()) + .with_context(|| "Failed to write version")?; + + let count = features.len() as u32; + file.write_all(&count.to_le_bytes()) + .with_context(|| "Failed to write count")?; + + for (&id, &value) in features.iter() { + file.write_all(&id.to_le_bytes()) + .with_context(|| format!("Failed to write feature id {}", id))?; + file.write_all(&value.to_le_bytes()) + .with_context(|| format!("Failed to write feature value for id {}", id))?; + } + + file.sync_all() + .with_context(|| "Failed to sync feature config")?; + + log::info!("Saved {} features to config", features.len()); + Ok(()) +} + +pub fn apply_config(features: &HashMap) -> Result<()> { + log::info!("Applying feature configuration to kernel..."); + + let mut applied = 0; + for (&id, &value) in features.iter() { + match crate::ksucalls::set_feature(id, value) { + Ok(_) => { + if let Some(feature_id) = FeatureId::from_u32(id) { + log::info!("Set feature {} to {}", feature_id.name(), value); + } else { + log::info!("Set feature {} to {}", id, value); + } + applied += 1; + } + Err(e) => { + log::warn!("Failed to set feature {}: {}", id, e); + } + } + } + + log::info!("Applied {} features successfully", applied); + Ok(()) +} + +pub fn get_feature(id: String) -> Result<()> { + let feature_id = parse_feature_id(&id)?; + let (value, supported) = crate::ksucalls::get_feature(feature_id as u32) + .with_context(|| format!("Failed to get feature {}", id))?; + + if !supported { + println!("Feature '{}' is not supported by kernel", id); + return Ok(()); + } + + println!("Feature: {} ({})", feature_id.name(), feature_id as u32); + println!("Description: {}", feature_id.description()); + println!("Value: {}", value); + println!( + "Status: {}", + if value != 0 { "enabled" } else { "disabled" } + ); + + Ok(()) +} + +pub fn set_feature(id: String, value: u64) -> Result<()> { + let feature_id = parse_feature_id(&id)?; + + // Check if this feature is managed by any module + if let Ok(managed_features_map) = crate::module::get_managed_features() { + // Find which modules manage this feature + let managing_modules: Vec<&String> = managed_features_map + .iter() + .filter(|(_, features)| features.iter().any(|f| f == feature_id.name())) + .map(|(module_id, _)| module_id) + .collect(); + + if !managing_modules.is_empty() { + // Feature is managed, check if caller is an authorized module + let caller_module = std::env::var("KSU_MODULE").unwrap_or_default(); + + if caller_module.is_empty() || !managing_modules.contains(&&caller_module) { + bail!( + "Feature '{}' is managed by module(s): {}. Direct modification is not allowed.", + feature_id.name(), + managing_modules + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(", ") + ); + } + + log::info!( + "Module '{}' is setting managed feature '{}'", + caller_module, + feature_id.name() + ); + } + } + + crate::ksucalls::set_feature(feature_id as u32, value) + .with_context(|| format!("Failed to set feature {} to {}", id, value))?; + + println!( + "Feature '{}' set to {} ({})", + feature_id.name(), + value, + if value != 0 { "enabled" } else { "disabled" } + ); + + Ok(()) +} + +pub fn list_features() -> Result<()> { + println!("Available Features:"); + println!("{}", "=".repeat(80)); + + // Get managed features from modules + let managed_features_map = crate::module::get_managed_features().unwrap_or_default(); + + // Build a reverse map: feature_name -> Vec + let mut feature_to_modules: HashMap> = HashMap::new(); + for (module_id, feature_list) in managed_features_map.iter() { + for feature_name in feature_list { + feature_to_modules + .entry(feature_name.clone()) + .or_default() + .push(module_id.clone()); + } + } + + let all_features = [ + FeatureId::SuCompat, + FeatureId::KernelUmount, + FeatureId::EnhancedSecurity, + FeatureId::SuLog, + ]; + + for feature_id in all_features.iter() { + let id = *feature_id as u32; + let (value, supported) = crate::ksucalls::get_feature(id).unwrap_or((0, false)); + + let status = if !supported { + "NOT_SUPPORTED".to_string() + } else if value != 0 { + format!("ENABLED ({})", value) + } else { + "DISABLED".to_string() + }; + + let managed_by = feature_to_modules.get(feature_id.name()); + let managed_mark = if managed_by.is_some() { + " [MODULE_MANAGED]" + } else { + "" + }; + + println!( + "[{}] {} (ID={}){}", + status, + feature_id.name(), + id, + managed_mark + ); + println!(" {}", feature_id.description()); + + if let Some(modules) = managed_by { + println!( + " ⚠️ Managed by module(s): {} (forced to 0 on initialization)", + modules.join(", ") + ); + } + + println!(); + } + + Ok(()) +} + +pub fn load_config_and_apply() -> Result<()> { + let features = load_binary_config()?; + + if features.is_empty() { + println!("No features found in config file"); + return Ok(()); + } + + apply_config(&features)?; + println!("Feature configuration loaded and applied"); + Ok(()) +} + +pub fn save_config() -> Result<()> { + let mut features = HashMap::new(); + + let all_features = [ + FeatureId::SuCompat, + FeatureId::KernelUmount, + FeatureId::EnhancedSecurity, + FeatureId::SuLog, + ]; + + for feature_id in all_features.iter() { + let id = *feature_id as u32; + if let Ok((value, supported)) = crate::ksucalls::get_feature(id) + && supported + { + features.insert(id, value); + log::info!("Saved feature {} = {}", feature_id.name(), value); + } + } + + save_binary_config(&features)?; + println!( + "Current feature states saved to config file ({} features)", + features.len() + ); + Ok(()) +} + +pub fn check_feature(id: String) -> Result<()> { + let feature_id = parse_feature_id(&id)?; + + // Check if this feature is managed by any module + let managed_features_map = crate::module::get_managed_features().unwrap_or_default(); + let is_managed = managed_features_map + .values() + .any(|features| features.iter().any(|f| f == feature_id.name())); + + if is_managed { + println!("managed"); + return Ok(()); + } + + // Check if the feature is supported by kernel + let (_value, supported) = crate::ksucalls::get_feature(feature_id as u32) + .with_context(|| format!("Failed to get feature {}", id))?; + + if supported { + println!("supported"); + } else { + println!("unsupported"); + } + + Ok(()) +} + +pub fn init_features() -> Result<()> { + log::info!("Initializing features from config..."); + + let mut features = load_binary_config()?; + + // Get managed features from active modules and skip them during init + if let Ok(managed_features_map) = crate::module::get_managed_features() { + if !managed_features_map.is_empty() { + log::info!( + "Found {} modules managing features", + managed_features_map.len() + ); + + // Build a set of all managed feature IDs to skip + for (module_id, feature_list) in managed_features_map.iter() { + log::info!( + "Module '{}' manages {} feature(s)", + module_id, + feature_list.len() + ); + for feature_name in feature_list { + if let Ok(feature_id) = parse_feature_id(feature_name) { + let feature_id_u32 = feature_id as u32; + // Remove managed features from config, let modules control them + if features.remove(&feature_id_u32).is_some() { + log::info!( + " - Skipping managed feature '{}' (controlled by module: {})", + feature_name, + module_id + ); + } else { + log::info!( + " - Feature '{}' is managed by module '{}', skipping", + feature_name, + module_id + ); + } + } else { + log::warn!( + " - Unknown managed feature '{}' from module '{}', ignoring", + feature_name, + module_id + ); + } + } + } + } + } else { + log::warn!( + "Failed to get managed features from modules, continuing with normal initialization" + ); + } + + if features.is_empty() { + log::info!("No features to apply, skipping initialization"); + return Ok(()); + } + + apply_config(&features)?; + + // Save the configuration (excluding managed features) + save_binary_config(&features)?; + log::info!("Saved feature configuration to file"); + + Ok(()) +} diff --git a/userspace/ksud/src/init_event.rs b/userspace/ksud/src/init_event.rs index 26a278d..be467c5 100644 --- a/userspace/ksud/src/init_event.rs +++ b/userspace/ksud/src/init_event.rs @@ -1,9 +1,13 @@ -use crate::defs::{KSU_MOUNT_SOURCE, NO_MOUNT_PATH, NO_TMPFS_PATH}; +#[cfg(target_arch = "aarch64")] +use crate::kpm; use crate::module::{handle_updated_modules, prune_modules}; -use crate::{assets, defs, ksucalls, restorecon, utils, kpm, uid_scanner}; +use crate::utils::is_safe_mode; +use crate::{ + assets, defs, ksucalls, metamodule, restorecon, + utils::{self}, +}; use anyhow::{Context, Result}; use log::{info, warn}; -use rustix::fs::{MountFlags, mount}; use std::path::Path; pub fn on_post_data_fs() -> Result<()> { @@ -11,6 +15,11 @@ pub fn on_post_data_fs() -> Result<()> { utils::umask(0); + // Clear all temporary module configs early + if let Err(e) = crate::module_config::clear_all_temp_configs() { + warn!("clear temp configs failed: {e}"); + } + #[cfg(unix)] let _ = catch_bootlog("logcat", vec!["logcat"]); #[cfg(unix)] @@ -21,9 +30,11 @@ pub fn on_post_data_fs() -> Result<()> { return Ok(()); } - let safe_mode = utils::is_safe_mode(); + let safe_mode = crate::utils::is_safe_mode(); if safe_mode { + // we should still ensure module directory exists in safe mode + // because we may need to operate the module dir in safe mode warn!("safe mode, skip common post-fs-data.d scripts"); } else { // Then exec common post-fs-data scripts @@ -32,13 +43,18 @@ pub fn on_post_data_fs() -> Result<()> { } } + let module_dir = defs::MODULE_DIR; + assets::ensure_binaries(true).with_context(|| "Failed to extract bin assets")?; // Start UID scanner daemon with highest priority - uid_scanner::start_uid_scanner_daemon()?; + crate::uid_scanner::start_uid_scanner_daemon()?; - // tell kernel that we've mount the module, so that it can do some optimization - ksucalls::report_module_mounted(); + if is_safe_mode() { + warn!("safe mode, skip load feature config"); + } else if let Err(e) = crate::umount_manager::load_and_apply_config() { + warn!("Failed to load umount config: {e}"); + } // if we are in safe mode, we should disable all modules if safe_mode { @@ -49,12 +65,12 @@ pub fn on_post_data_fs() -> Result<()> { return Ok(()); } - if let Err(e) = prune_modules() { - warn!("prune modules failed: {}", e); + if let Err(e) = handle_updated_modules() { + warn!("handle updated modules failed: {e}"); } - if let Err(e) = handle_updated_modules() { - warn!("handle updated modules failed: {}", e); + if let Err(e) = prune_modules() { + warn!("prune modules failed: {e}"); } if let Err(e) = restorecon::restorecon() { @@ -70,27 +86,26 @@ pub fn on_post_data_fs() -> Result<()> { warn!("apply root profile sepolicy failed: {e}"); } + // load feature config + if is_safe_mode() { + warn!("safe mode, skip load feature config"); + } else if let Err(e) = crate::feature::init_features() { + warn!("init features failed: {e}"); + } + + #[cfg(target_arch = "aarch64")] if let Err(e) = kpm::start_kpm_watcher() { - warn!("KPM: Failed to start KPM watcher: {}", e); + warn!("KPM: Failed to start KPM watcher: {e}"); } + #[cfg(target_arch = "aarch64")] if let Err(e) = kpm::load_kpm_modules() { - warn!("KPM: Failed to load KPM modules: {}", e); + warn!("KPM: Failed to load KPM modules: {e}"); } - // mount temp dir - if !Path::new(NO_TMPFS_PATH).exists() { - if let Err(e) = mount( - KSU_MOUNT_SOURCE, - utils::get_tmp_path(), - "tmpfs", - MountFlags::empty(), - "", - ) { - warn!("do temp dir mount failed: {}", e); - } - } else { - info!("no tmpfs requested"); + // execute metamodule post-fs-data script first (priority) + if let Err(e) = metamodule::exec_stage_script("post-fs-data", true) { + warn!("exec metamodule post-fs-data script failed: {e}"); } // exec modules post-fs-data scripts @@ -104,27 +119,15 @@ pub fn on_post_data_fs() -> Result<()> { warn!("load system.prop failed: {e}"); } - // mount module systemlessly by magic mount - if !Path::new(NO_MOUNT_PATH).exists() { - if let Err(e) = mount_modules_systemlessly() { - warn!("do systemless mount failed: {}", e); - } - } else { - info!("no mount requested"); + // execute metamodule mount script + if let Err(e) = metamodule::exec_mount_script(module_dir) { + warn!("execute metamodule mount failed: {e}"); } run_stage("post-mount", true); - Ok(()) -} + std::env::set_current_dir("/").with_context(|| "failed to chdir to /")?; -#[cfg(target_os = "android")] -pub fn mount_modules_systemlessly() -> Result<()> { - crate::magic_mount::magic_mount() -} - -#[cfg(not(target_os = "android"))] -pub fn mount_modules_systemlessly() -> Result<()> { Ok(()) } @@ -144,6 +147,13 @@ fn run_stage(stage: &str, block: bool) { if let Err(e) = crate::module::exec_common_scripts(&format!("{stage}.d"), block) { warn!("Failed to exec common {stage} scripts: {e}"); } + + // execute metamodule stage script first (priority) + if let Err(e) = metamodule::exec_stage_script(stage, block) { + warn!("Failed to exec metamodule {stage} script: {e}"); + } + + // execute regular modules stage scripts if let Err(e) = crate::module::exec_stage_script(stage, block) { warn!("Failed to exec {stage} scripts: {e}"); } diff --git a/userspace/ksud/src/installer.sh b/userspace/ksud/src/installer.sh index 2ebdddc..00ad86d 100644 --- a/userspace/ksud/src/installer.sh +++ b/userspace/ksud/src/installer.sh @@ -85,7 +85,7 @@ setup_flashable() { $BOOTMODE && return if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then # We will have to manually find out OUTFD - for FD in /proc/$$/fd/*; do + for FD in `ls /proc/$$/fd`; do if readlink /proc/$$/fd/$FD | grep -q pipe; then if ps | grep -v grep | grep -qE " 3 $FD |status_fd=$FD"; then OUTFD=$FD @@ -248,6 +248,42 @@ api_level_arch_detect() { # Module Related ################# +check_managed_features() { + local PROP_FILE=$1 + local MANAGED_FEATURES=$(grep_prop managedFeatures "$PROP_FILE") + + [ -z "$MANAGED_FEATURES" ] && return 0 + + ui_print "- Checking managed features: $MANAGED_FEATURES" + + # Split features by comma + echo "$MANAGED_FEATURES" | tr ',' '\n' | while read -r feature; do + # Trim whitespace + feature=$(echo "$feature" | xargs) + [ -z "$feature" ] && continue + + # Check feature status using ksud + local status=$(/data/adb/ksud feature check "$feature" 2>/dev/null) + + case "$status" in + "unsupported") + ui_print "! WARNING: Feature '$feature' is NOT SUPPORTED by kernel" + ui_print "! This module may not work correctly!" + ;; + "managed") + ui_print "! WARNING: Feature '$feature' is already MANAGED by another module" + ui_print "! Feature conflicts may occur!" + ;; + "supported") + ui_print "- Feature '$feature' is supported and available" + ;; + *) + ui_print "! WARNING: Unable to check feature '$feature' status" + ;; + esac + done +} + set_perm() { chown $2:$3 $1 || return 1 chmod $4 $1 || return 1 @@ -277,14 +313,6 @@ mark_remove() { chmod 644 $1 } -mark_replace() { - # REPLACE must be directory!!! - # https://docs.kernel.org/filesystems/overlayfs.html#whiteouts-and-opaque-directories - mkdir -p $1 2>/dev/null - setfattr -n trusted.overlay.opaque -v y $1 - chmod 644 $1 -} - request_size_check() { reqSizeM=`du -ms "$1" | cut -f1` } @@ -302,16 +330,19 @@ is_legacy_script() { } handle_partition() { - PARTITION="$1" - REQUIRE_SYMLINK="$2" - if [ ! -e "$MODPATH/system/$PARTITION" ]; then + # if /system/vendor is a symlink, we need to move it out of $MODPATH/system + # if /system/vendor is a normal directory, no special handling is needed. + if [ ! -e $MODPATH/system/$1 ]; then # no partition found return; fi - if [ "$REQUIRE_SYMLINK" = "false" ] || [ -L "/system/$PARTITION" ] && [ "$(readlink -f "/system/$PARTITION")" = "/$PARTITION" ]; then - ui_print "- Handle partition /$PARTITION" - ln -sf "./system/$PARTITION" "$MODPATH/$PARTITION" + # we move the folder to / only if it is a native folder that is not a symlink + if [ -d "/$1" ] && [ ! -L "/$1" ]; then + ui_print "- Handle partition /$1" + # we create a symlink if module want to access $MODPATH/system/$1 + # but it doesn't always work(ie. write it in post-fs-data.sh would fail because it is readonly) + mv -f $MODPATH/system/$1 $MODPATH/$1 && ln -sf ../$1 $MODPATH/system/$1 fi } @@ -344,6 +375,9 @@ install_module() { MODAUTH=`grep_prop author $TMPDIR/module.prop` MODPATH=$MODULEROOT/$MODID + # Check managed features + check_managed_features $TMPDIR/module.prop + # Create mod paths rm -rf $MODPATH mkdir -p $MODPATH @@ -389,23 +423,23 @@ install_module() { [ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh fi - handle_partition vendor true - handle_partition system_ext true - handle_partition product true - handle_partition odm false - # Handle replace folders for TARGET in $REPLACE; do ui_print "- Replace target: $TARGET" - mark_replace "$MODPATH$TARGET" + mark_replace $MODPATH$TARGET done # Handle remove files for TARGET in $REMOVE; do ui_print "- Remove target: $TARGET" - mark_remove "$MODPATH$TARGET" + mark_remove $MODPATH$TARGET done + handle_partition vendor + handle_partition system_ext + handle_partition product + handle_partition odm + if $BOOTMODE; then mktouch $NVBASE/modules/$MODID/update rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null diff --git a/userspace/ksud/src/kpm.rs b/userspace/ksud/src/kpm.rs index fba0b18..f4dad19 100644 --- a/userspace/ksud/src/kpm.rs +++ b/userspace/ksud/src/kpm.rs @@ -1,296 +1,331 @@ -use anyhow::{anyhow, Result}; -use libc::{prctl, c_char, c_void, c_int}; +use std::{ + ffi::{CStr, CString, OsStr}, + fs, + os::unix::fs::PermissionsExt, + path::{Path, PathBuf}, +}; + +use anyhow::{Result, bail}; use notify::{RecursiveMode, Watcher}; -use std::ffi::{CStr, CString, OsStr}; -use std::fs; -use std::path::Path; -use std::ptr; -use std::os::unix::fs::PermissionsExt; + +use crate::ksucalls::ksuctl; pub const KPM_DIR: &str = "/data/adb/kpm"; -const KSU_OPTIONS: u32 = 0xdeadbeef; -const SUKISU_KPM_LOAD: i32 = 28; -const SUKISU_KPM_UNLOAD: i32 = 29; -const SUKISU_KPM_VERSION: i32 = 34; +const KPM_LOAD: u64 = 1; +const KPM_UNLOAD: u64 = 2; +const KPM_NUM: u64 = 3; +const KPM_LIST: u64 = 4; +const KPM_INFO: u64 = 5; +const KPM_CONTROL: u64 = 6; +const KPM_VERSION: u64 = 7; -pub fn check_kpm_version() -> Result { - let mut buffer: [u8; 1024] = [0; 1024]; - let mut out: c_int = -1; - - let _ret = unsafe { - prctl( - KSU_OPTIONS as c_int, - SUKISU_KPM_VERSION, - buffer.as_mut_ptr() as *mut c_void, - buffer.len() as *mut c_void, - &mut out as *mut c_int as *mut c_void, - ) - }; +const KSU_IOCTL_KPM: u32 = 0xc0004bc8; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 200, 0) - if out < 0 { - return Err(anyhow!("KPM: prctl returned error: {}", out)); - } - - let version_str = unsafe { - CStr::from_ptr(buffer.as_ptr() as *const c_char) - }.to_string_lossy().to_string(); - - log::info!("KPM: Version check result: {}", version_str); - - // 检查版本是否有效(不为空且不以Error开头) - if version_str.is_empty() || version_str.starts_with("Error") { - return Err(anyhow!("KPM: Invalid version response: {}", version_str)); - } - - Ok(version_str) +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct KsuKpmCmd { + pub control_code: u64, + pub arg1: u64, + pub arg2: u64, + pub result_code: u64, } -// 确保 KPM 目录存在,并设置777权限 +fn kpm_ioctl(cmd: &mut KsuKpmCmd) -> std::io::Result<()> { + ksuctl(KSU_IOCTL_KPM, cmd as *mut _)?; + Ok(()) +} + +/// Convert raw kernel return code to `Result`. +fn check_ret(rc: i32) -> Result { + if rc < 0 { + bail!("KPM error: {}", std::io::Error::from_raw_os_error(-rc)); + } + Ok(rc) +} + +/// Load a `.kpm` into kernel space. +pub fn kpm_load

(path: P, args: Option<&str>) -> Result<()> +where + P: AsRef, +{ + let path_c = CString::new(path.as_ref().to_string_lossy().to_string())?; + let args_c = args.map(CString::new).transpose()?; + + let mut result: i32 = -1; + let mut cmd = KsuKpmCmd { + control_code: KPM_LOAD, + arg1: path_c.as_ptr() as u64, + arg2: args_c.as_ref().map_or(0, |s| s.as_ptr() as u64), + result_code: &mut result as *mut i32 as u64, + }; + + kpm_ioctl(&mut cmd)?; + check_ret(result)?; + println!("Success"); + Ok(()) +} + +/// Unload by module name. +pub fn kpm_unload(name: &str) -> Result<()> { + let name_c = CString::new(name)?; + + let mut result: i32 = -1; + let mut cmd = KsuKpmCmd { + control_code: KPM_UNLOAD, + arg1: name_c.as_ptr() as u64, + arg2: 0, + result_code: &mut result as *mut i32 as u64, + }; + + kpm_ioctl(&mut cmd)?; + check_ret(result)?; + Ok(()) +} + +/// Return loaded module count. +pub fn kpm_num() -> Result { + let mut result: i32 = -1; + let mut cmd = KsuKpmCmd { + control_code: KPM_NUM, + arg1: 0, + arg2: 0, + result_code: &mut result as *mut i32 as u64, + }; + + kpm_ioctl(&mut cmd)?; + let n = check_ret(result)?; + println!("{n}"); + Ok(n) +} + +/// Print name list of loaded modules. +pub fn kpm_list() -> Result<()> { + let mut buf = vec![0u8; 1024]; + + let mut result: i32 = -1; + let mut cmd = KsuKpmCmd { + control_code: KPM_LIST, + arg1: buf.as_mut_ptr() as u64, + arg2: buf.len() as u64, + result_code: &mut result as *mut i32 as u64, + }; + + kpm_ioctl(&mut cmd)?; + check_ret(result)?; + print!("{}", buf2str(&buf)); + Ok(()) +} + +/// Print single module info. +pub fn kpm_info(name: &str) -> Result<()> { + let name_c = CString::new(name)?; + let mut buf = vec![0u8; 256]; + + let mut result: i32 = -1; + let mut cmd = KsuKpmCmd { + control_code: KPM_INFO, + arg1: name_c.as_ptr() as u64, + arg2: buf.as_mut_ptr() as u64, + result_code: &mut result as *mut i32 as u64, + }; + + kpm_ioctl(&mut cmd)?; + check_ret(result)?; + println!("{}", buf2str(&buf)); + Ok(()) +} + +/// Send control string to a module; returns kernel answer. +pub fn kpm_control(name: &str, args: &str) -> Result { + let name_c = CString::new(name)?; + let args_c = CString::new(args)?; + + let mut result: i32 = -1; + let mut cmd = KsuKpmCmd { + control_code: KPM_CONTROL, + arg1: name_c.as_ptr() as u64, + arg2: args_c.as_ptr() as u64, + result_code: &mut result as *mut i32 as u64, + }; + + kpm_ioctl(&mut cmd)?; + check_ret(result) +} + +/// Print loader version string. +pub fn kpm_version_loader() -> Result<()> { + let mut buf = vec![0u8; 1024]; + + let mut result: i32 = -1; + let mut cmd = KsuKpmCmd { + control_code: KPM_VERSION, + arg1: buf.as_mut_ptr() as u64, + arg2: buf.len() as u64, + result_code: &mut result as *mut i32 as u64, + }; + + kpm_ioctl(&mut cmd)?; + check_ret(result)?; + print!("{}", buf2str(&buf)); + Ok(()) +} + +/// Validate loader version; empty or "Error*" => fail. +pub fn check_kpm_version() -> Result { + let mut buf = vec![0u8; 1024]; + + let mut result: i32 = -1; + let mut cmd = KsuKpmCmd { + control_code: KPM_VERSION, + arg1: buf.as_mut_ptr() as u64, + arg2: buf.len() as u64, + result_code: &mut result as *mut i32 as u64, + }; + + kpm_ioctl(&mut cmd)?; + check_ret(result)?; + let ver = buf2str(&buf); + if ver.is_empty() { + bail!("KPM: invalid version response: {ver}"); + } + log::info!("KPM: version check ok: {ver}"); + Ok(ver) +} + +/// Create `/data/adb/kpm` with 0o777 if missing. pub fn ensure_kpm_dir() -> Result<()> { - let path = Path::new(KPM_DIR); - - if path.exists() { - let meta = fs::metadata(path)?; - let current = meta.permissions().mode() & 0o777; - if current != 0o777 { - log::info!("KPM: Fixing permissions to 777 for {}", KPM_DIR); - fs::set_permissions(path, fs::Permissions::from_mode(0o777))?; - } + let _ = fs::create_dir_all(KPM_DIR); + let meta = fs::metadata(KPM_DIR)?; + + if meta.permissions().mode() != 0o777 { + fs::set_permissions(KPM_DIR, fs::Permissions::from_mode(0o777))?; } Ok(()) } +/// Start file watcher for hot-(un)load. pub fn start_kpm_watcher() -> Result<()> { - match check_kpm_version() { - Ok(version) => { - log::info!("KPM: Version check passed, version: {}", version); - } - Err(e) => { - log::warn!("KPM: Version check failed, skipping KPM functionality: {}", e); - return Ok(()) - } - } - + check_kpm_version()?; // bails if loader too old ensure_kpm_dir()?; - // 检查是否处于安全模式 if crate::utils::is_safe_mode() { - log::warn!("KPM: System is in safe mode, removing all KPM modules"); - if let Err(e) = remove_all_kpms() { - log::error!("KPM: Error removing all KPM modules: {}", e); - } + log::warn!("KPM: safe-mode – removing all modules"); + remove_all_kpms()?; return Ok(()); } - let mut watcher = notify::recommended_watcher(|res| match res { - Ok(event) => handle_kpm_event(event), - Err(e) => log::error!("KPM: File monitoring error: {:?}", e), + let mut watcher = notify::recommended_watcher(|res: Result<_, _>| match res { + Ok(evt) => handle_kpm_event(evt), + Err(e) => log::error!("KPM: watcher error: {e}"), })?; - watcher.watch(Path::new(KPM_DIR), RecursiveMode::NonRecursive)?; - log::info!("KPM: Started file watcher for directory: {}", KPM_DIR); + log::info!("KPM: watcher active on {KPM_DIR}"); Ok(()) } -// 处理 KPM 事件 -pub fn handle_kpm_event(event: notify::Event) { - match event.kind { - notify::EventKind::Create(_) => handle_create_event(event.paths), - notify::EventKind::Remove(_) => handle_remove_event(event.paths), - notify::EventKind::Modify(_) => handle_modify_event(event.paths), - _ => {} - } -} - -fn handle_create_event(paths: Vec) { - for path in paths { - if path.extension() == Some(OsStr::new("kpm")) { - log::info!("KPM: Detected new KPM file: {}", path.display()); - if let Err(e) = load_kpm(&path) { - log::warn!("KPM: Failed to load {}: {}", path.display(), e); +fn handle_kpm_event(evt: notify::Event) { + if let notify::EventKind::Create(_) = evt.kind { + for p in evt.paths { + if let Some(ex) = p.extension() + && ex == OsStr::new("kpm") + && kpm_load(&p, None).is_err() + { + log::warn!("KPM: failed to load {}", p.display()); } } } } -fn handle_remove_event(paths: Vec) { - for path in paths { - if let Some(name) = path.file_stem().and_then(|s| s.to_str()) { - log::info!("KPM: Detected KPM file removal: {}", name); - if let Err(e) = unload_kpm(name) { - log::warn!("KPM: Failed to unload {}: {}", name, e); - } - } - } -} +/// Locate `/data/adb/kpm/.kpm`. +fn find_kpm_file(name: &str) -> Result> { + let dir = Path::new(KPM_DIR); -fn handle_modify_event(paths: Vec) { - for path in paths { - log::info!("KPM: Modified file detected: {}", path.display()); - } -} - -// 加载 KPM 模块 -pub fn load_kpm(path: &Path) -> Result<()> { - let path_str = path - .to_str() - .ok_or_else(|| anyhow!("KPM: Invalid path: {}", path.display()))?; - - let path_cstring = CString::new(path_str) - .map_err(|e| anyhow!("KPM: Failed to convert path to CString: {}", e))?; - - let mut out: c_int = -1; - - let _ret = unsafe { - prctl( - KSU_OPTIONS as c_int, - SUKISU_KPM_LOAD, - path_cstring.as_ptr() as *mut c_void, - ptr::null_mut::(), - &mut out as *mut c_int as *mut c_void, - ) - }; - - if out < 0 { - return Err(anyhow!("KPM: prctl returned error: {}", out)); - } - - if out > 0 { - log::info!("KPM: Successfully loaded module: {}", path.display()); - } - - Ok(()) -} - -// 卸载 KPM 模块 -pub fn unload_kpm(name: &str) -> Result<()> { - let name_cstring = CString::new(name) - .map_err(|e| anyhow!("KPM: Failed to convert name to CString: {}", e))?; - - let mut out: c_int = -1; - - let _ret = unsafe { - prctl( - KSU_OPTIONS as c_int, - SUKISU_KPM_UNLOAD, - name_cstring.as_ptr() as *mut c_void, - ptr::null_mut::(), - &mut out as *mut c_int as *mut c_void, - ) - }; - - if out < 0 { - log::warn!("KPM: prctl returned error for unload: {}", out); - return Err(anyhow!("KPM: prctl returned error: {}", out)); - } - - // 尝试删除对应的KPM文件 - if let Ok(Some(path)) = find_kpm_file(name) { - if let Err(e) = fs::remove_file(&path) { - log::warn!("KPM: Failed to delete KPM file {}: {}", path.display(), e); - } else { - log::info!("KPM: Deleted KPM file: {}", path.display()); - } - } - - log::info!("KPM: Successfully unloaded module: {}", name); - Ok(()) -} - -// 通过名称查找 KPM 文件 -fn find_kpm_file(name: &str) -> Result> { - let kpm_dir = Path::new(KPM_DIR); - if !kpm_dir.exists() { + if !dir.is_dir() { return Ok(None); } - for entry in fs::read_dir(kpm_dir)? { - let path = entry?.path(); - if let Some(file_name) = path.file_stem() { - if let Some(file_name_str) = file_name.to_str() { - if file_name_str == name && path.extension() == Some(OsStr::new("kpm")) { - return Ok(Some(path)); - } - } + for entry in fs::read_dir(dir)? { + let p = entry?.path(); + if let Some(ex) = p.extension() + && ex == OsStr::new("kpm") + && let Some(fs) = p.file_stem() + && fs == OsStr::new(name) + { + return Ok(Some(p)); } } Ok(None) } -// 安全模式下删除所有 KPM 模块 +/// Remove every `.kpm` file and unload it. pub fn remove_all_kpms() -> Result<()> { - let kpm_dir = Path::new(KPM_DIR); - if !kpm_dir.exists() { - log::info!("KPM: KPM directory does not exist, nothing to remove"); + let dir = Path::new(KPM_DIR); + if !dir.is_dir() { return Ok(()); } - for entry in fs::read_dir(KPM_DIR)? { - let path = entry?.path(); - if path.extension().is_some_and(|ext| ext == "kpm") { - if let Some(name) = path.file_stem() { - let name_str = name.to_string_lossy(); - log::info!("KPM: Removing module in safe mode: {}", name_str); - if let Err(e) = unload_kpm(&name_str) { - log::error!("KPM: Failed to remove module {}: {}", name_str, e); + for entry in fs::read_dir(dir)? { + let p = entry?.path(); + if let Some(ex) = p.extension() + && ex == OsStr::new("kpm") + && let Some(name) = p.file_stem().and_then(|s| s.to_str()) + && let Err(e) = (|| -> Result<()> { + kpm_unload(name)?; + + if let Some(p) = find_kpm_file(name)? { + if let Err(e) = fs::remove_file(&p) { + log::warn!("KPM: delete {} failed: {e}", p.display()); + return Err(e.into()); + } + log::info!("KPM: deleted {}", p.display()); } - if let Err(e) = fs::remove_file(&path) { - log::error!("KPM: Failed to delete file {}: {}", path.display(), e); - } - } + + Ok(()) + })() + { + log::error!("KPM: unload {name} failed: {e}"); } } Ok(()) } -// 加载所有 KPM 模块 +/// Bulk-load existing `.kpm`s at boot. pub fn load_kpm_modules() -> Result<()> { - match check_kpm_version() { - Ok(version) => { - log::info!("KPM: Version check passed before loading modules, version: {}", version); - } - Err(e) => { - log::warn!("KPM: Version check failed, skipping module loading: {}", e); - return Ok(()); - } - } - + check_kpm_version()?; ensure_kpm_dir()?; - let kpm_dir = Path::new(KPM_DIR); - if !kpm_dir.exists() { - log::info!("KPM: KPM directory does not exist, no modules to load"); + let dir = Path::new(KPM_DIR); + if !dir.is_dir() { return Ok(()); } - let mut loaded_count = 0; - let mut failed_count = 0; + let (mut ok, mut ng) = (0, 0); - for entry in std::fs::read_dir(KPM_DIR)? { - let path = entry?.path(); - if let Some(file_name) = path.file_stem() { - if let Some(file_name_str) = file_name.to_str() { - if file_name_str.is_empty() { - log::warn!("KPM: Invalid KPM file name: {}", path.display()); - continue; - } - } - } - if path.extension().is_some_and(|ext| ext == "kpm") { - match load_kpm(&path) { - Ok(()) => { - log::info!("KPM: Successfully loaded module: {}", path.display()); - loaded_count += 1; - } + for entry in fs::read_dir(dir)? { + let p = entry?.path(); + if let Some(ex) = p.extension() + && ex == OsStr::new("kpm") + { + match kpm_load(&p, None) { + Ok(_) => ok += 1, Err(e) => { - log::warn!("KPM: Failed to load module {}: {}", path.display(), e); - failed_count += 1; + log::warn!("KPM: load {} failed: {e}", p.display()); + ng += 1; } } } } - - log::info!("KPM: Module loading completed - loaded: {}, failed: {}", loaded_count, failed_count); + log::info!("KPM: bulk-load done – ok: {ok}, failed: {ng}"); Ok(()) } + +/// Convert zero-padded kernel buffer to owned String. +fn buf2str(buf: &[u8]) -> String { + // SAFETY: buffer is always NUL-terminated by kernel. + unsafe { + CStr::from_ptr(buf.as_ptr().cast()) + .to_string_lossy() + .into_owned() + } +} diff --git a/userspace/ksud/src/ksucalls.rs b/userspace/ksud/src/ksucalls.rs index 596df54..4a814d2 100644 --- a/userspace/ksud/src/ksucalls.rs +++ b/userspace/ksud/src/ksucalls.rs @@ -1,10 +1,194 @@ -const EVENT_POST_FS_DATA: u64 = 1; -const EVENT_BOOT_COMPLETED: u64 = 2; -const EVENT_MODULE_MOUNTED: u64 = 3; +use std::fs; +#[cfg(any(target_os = "linux", target_os = "android"))] +use std::os::fd::RawFd; +use std::sync::OnceLock; + +// Event constants +const EVENT_POST_FS_DATA: u32 = 1; +const EVENT_BOOT_COMPLETED: u32 = 2; +const EVENT_MODULE_MOUNTED: u32 = 3; + +const KSU_IOCTL_GRANT_ROOT: u32 = 0x00004b01; // _IOC(_IOC_NONE, 'K', 1, 0) +const KSU_IOCTL_GET_INFO: u32 = 0x80004b02; // _IOC(_IOC_READ, 'K', 2, 0) +const KSU_IOCTL_REPORT_EVENT: u32 = 0x40004b03; // _IOC(_IOC_WRITE, 'K', 3, 0) +const KSU_IOCTL_SET_SEPOLICY: u32 = 0xc0004b04; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 4, 0) +const KSU_IOCTL_CHECK_SAFEMODE: u32 = 0x80004b05; // _IOC(_IOC_READ, 'K', 5, 0) +const KSU_IOCTL_GET_FEATURE: u32 = 0xc0004b0d; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 13, 0) +const KSU_IOCTL_SET_FEATURE: u32 = 0x40004b0e; // _IOC(_IOC_WRITE, 'K', 14, 0) +const KSU_IOCTL_GET_WRAPPER_FD: u32 = 0x40004b0f; // _IOC(_IOC_WRITE, 'K', 15, 0) +const KSU_IOCTL_MANAGE_MARK: u32 = 0xc0004b10; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 16, 0) +const KSU_IOCTL_NUKE_EXT4_SYSFS: u32 = 0x40004b11; // _IOC(_IOC_WRITE, 'K', 17, 0) +const KSU_IOCTL_ADD_TRY_UMOUNT: u32 = 0x40004b12; // _IOC(_IOC_WRITE, 'K', 18, 0) + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct GetInfoCmd { + version: u32, + flags: u32, +} + +#[repr(C)] +struct ReportEventCmd { + event: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct SetSepolicyCmd { + pub cmd: u64, + pub arg: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct CheckSafemodeCmd { + in_safe_mode: u8, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct GetFeatureCmd { + feature_id: u32, + value: u64, + supported: u8, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct SetFeatureCmd { + feature_id: u32, + value: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct GetWrapperFdCmd { + fd: i32, + flags: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct ManageMarkCmd { + operation: u32, + pid: i32, + result: u32, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +pub struct NukeExt4SysfsCmd { + pub arg: u64, +} + +#[repr(C)] +#[derive(Clone, Copy, Default)] +struct AddTryUmountCmd { + arg: u64, // char ptr, this is the mountpoint + flags: u32, // this is the flag we use for it + mode: u8, // denotes what to do with it 0:wipe_list 1:add_to_list 2:delete_entry +} + +// Mark operation constants +const KSU_MARK_GET: u32 = 1; +const KSU_MARK_MARK: u32 = 2; +const KSU_MARK_UNMARK: u32 = 3; +const KSU_MARK_REFRESH: u32 = 4; + +// Umount operation constants +const KSU_UMOUNT_WIPE: u8 = 0; +const KSU_UMOUNT_ADD: u8 = 1; +const KSU_UMOUNT_DEL: u8 = 2; + +// Global driver fd cache +#[cfg(any(target_os = "linux", target_os = "android"))] +static DRIVER_FD: OnceLock = OnceLock::new(); +#[cfg(any(target_os = "linux", target_os = "android"))] +static INFO_CACHE: OnceLock = OnceLock::new(); + +const KSU_INSTALL_MAGIC1: u32 = 0xDEADBEEF; +const KSU_INSTALL_MAGIC2: u32 = 0xCAFEBABE; + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn scan_driver_fd() -> Option { + let fd_dir = fs::read_dir("/proc/self/fd").ok()?; + + 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); + } + } + } + } + + None +} + +// Get cached driver fd +#[cfg(any(target_os = "linux", target_os = "android"))] +fn init_driver_fd() -> Option { + let fd = scan_driver_fd(); + if fd.is_none() { + let mut fd = -1; + unsafe { + libc::syscall( + libc::SYS_reboot, + KSU_INSTALL_MAGIC1, + KSU_INSTALL_MAGIC2, + 0, + &mut fd, + ); + }; + if fd >= 0 { Some(fd) } else { None } + } else { + fd + } +} + +// ioctl wrapper using libc +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn ksuctl(request: u32, arg: *mut T) -> std::io::Result { + use std::io; + + let fd = *DRIVER_FD.get_or_init(|| init_driver_fd().unwrap_or(-1)); + unsafe { + #[cfg(not(target_env = "gnu"))] + let ret = libc::ioctl(fd as libc::c_int, request as i32, arg); + #[cfg(target_env = "gnu")] + let ret = libc::ioctl(fd as libc::c_int, request as u64, arg); + if ret < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(ret) + } + } +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +fn ksuctl(_request: u32, _arg: *mut T) -> std::io::Result { + Err(std::io::Error::from_raw_os_error(libc::ENOSYS)) +} + +// API implementations +#[cfg(any(target_os = "linux", target_os = "android"))] +fn get_info() -> GetInfoCmd { + *INFO_CACHE.get_or_init(|| { + let mut cmd = GetInfoCmd { + version: 0, + flags: 0, + }; + let _ = ksuctl(KSU_IOCTL_GET_INFO, &mut cmd as *mut _); + cmd + }) +} #[cfg(any(target_os = "linux", target_os = "android"))] pub fn get_version() -> i32 { - rustix::process::ksu_get_version() + get_info().version as i32 } #[cfg(not(any(target_os = "linux", target_os = "android")))] @@ -13,22 +197,24 @@ pub fn get_version() -> i32 { } #[cfg(any(target_os = "linux", target_os = "android"))] -fn report_event(event: u64) { - rustix::process::ksu_report_event(event) +pub fn grant_root() -> std::io::Result<()> { + ksuctl(KSU_IOCTL_GRANT_ROOT, std::ptr::null_mut::())?; + Ok(()) } #[cfg(not(any(target_os = "linux", target_os = "android")))] -fn report_event(_event: u64) {} +pub fn grant_root() -> std::io::Result<()> { + Err(std::io::Error::from_raw_os_error(libc::ENOSYS)) +} #[cfg(any(target_os = "linux", target_os = "android"))] -pub fn check_kernel_safemode() -> bool { - rustix::process::ksu_check_kernel_safemode() +fn report_event(event: u32) { + let mut cmd = ReportEventCmd { event }; + let _ = ksuctl(KSU_IOCTL_REPORT_EVENT, &mut cmd as *mut _); } #[cfg(not(any(target_os = "linux", target_os = "android")))] -pub fn check_kernel_safemode() -> bool { - false -} +fn report_event(_event: u32) {} pub fn report_post_fs_data() { report_event(EVENT_POST_FS_DATA); @@ -41,3 +227,135 @@ pub fn report_boot_complete() { pub fn report_module_mounted() { report_event(EVENT_MODULE_MOUNTED); } + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn check_kernel_safemode() -> bool { + let mut cmd = CheckSafemodeCmd { in_safe_mode: 0 }; + let _ = ksuctl(KSU_IOCTL_CHECK_SAFEMODE, &mut cmd as *mut _); + cmd.in_safe_mode != 0 +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn check_kernel_safemode() -> bool { + false +} + +pub fn set_sepolicy(cmd: &SetSepolicyCmd) -> std::io::Result<()> { + let mut ioctl_cmd = *cmd; + ksuctl(KSU_IOCTL_SET_SEPOLICY, &mut ioctl_cmd as *mut _)?; + Ok(()) +} + +/// Get feature value and support status from kernel +/// Returns (value, supported) +pub fn get_feature(feature_id: u32) -> std::io::Result<(u64, bool)> { + let mut cmd = GetFeatureCmd { + feature_id, + value: 0, + supported: 0, + }; + ksuctl(KSU_IOCTL_GET_FEATURE, &mut cmd as *mut _)?; + Ok((cmd.value, cmd.supported != 0)) +} + +/// Set feature value in kernel +pub fn set_feature(feature_id: u32, value: u64) -> std::io::Result<()> { + let mut cmd = SetFeatureCmd { feature_id, value }; + ksuctl(KSU_IOCTL_SET_FEATURE, &mut cmd as *mut _)?; + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn get_wrapped_fd(fd: RawFd) -> std::io::Result { + let mut cmd = GetWrapperFdCmd { fd, flags: 0 }; + let result = ksuctl(KSU_IOCTL_GET_WRAPPER_FD, &mut cmd as *mut _)?; + Ok(result) +} + +/// Get mark status for a process (pid=0 returns total marked count) +pub fn mark_get(pid: i32) -> std::io::Result { + let mut cmd = ManageMarkCmd { + operation: KSU_MARK_GET, + pid, + result: 0, + }; + ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?; + Ok(cmd.result) +} + +/// Mark a process (pid=0 marks all processes) +pub fn mark_set(pid: i32) -> std::io::Result<()> { + let mut cmd = ManageMarkCmd { + operation: KSU_MARK_MARK, + pid, + result: 0, + }; + ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?; + Ok(()) +} + +/// Unmark a process (pid=0 unmarks all processes) +pub fn mark_unset(pid: i32) -> std::io::Result<()> { + let mut cmd = ManageMarkCmd { + operation: KSU_MARK_UNMARK, + pid, + result: 0, + }; + ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?; + Ok(()) +} + +/// Refresh mark for all running processes +pub fn mark_refresh() -> std::io::Result<()> { + let mut cmd = ManageMarkCmd { + operation: KSU_MARK_REFRESH, + pid: 0, + result: 0, + }; + ksuctl(KSU_IOCTL_MANAGE_MARK, &mut cmd as *mut _)?; + Ok(()) +} + +pub fn nuke_ext4_sysfs(mnt: &str) -> anyhow::Result<()> { + let c_mnt = std::ffi::CString::new(mnt)?; + let mut ioctl_cmd = NukeExt4SysfsCmd { + arg: c_mnt.as_ptr() as u64, + }; + ksuctl(KSU_IOCTL_NUKE_EXT4_SYSFS, &mut ioctl_cmd as *mut _)?; + Ok(()) +} + +/// Wipe all entries from umount list +pub fn umount_list_wipe() -> std::io::Result<()> { + let mut cmd = AddTryUmountCmd { + arg: 0, + flags: 0, + mode: KSU_UMOUNT_WIPE, + }; + ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?; + Ok(()) +} + +/// Add mount point to umount list +pub fn umount_list_add(path: &str, flags: u32) -> anyhow::Result<()> { + let c_path = std::ffi::CString::new(path)?; + let mut cmd = AddTryUmountCmd { + arg: c_path.as_ptr() as u64, + flags, + mode: KSU_UMOUNT_ADD, + }; + ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?; + Ok(()) +} + +/// Delete mount point from umount list +pub fn umount_list_del(path: &str) -> anyhow::Result<()> { + let c_path = std::ffi::CString::new(path)?; + let mut cmd = AddTryUmountCmd { + arg: c_path.as_ptr() as u64, + flags: 0, + mode: KSU_UMOUNT_DEL, + }; + ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &mut cmd as *mut _)?; + Ok(()) +} diff --git a/userspace/ksud/src/magic_mount.rs b/userspace/ksud/src/magic_mount.rs deleted file mode 100644 index 7404dd7..0000000 --- a/userspace/ksud/src/magic_mount.rs +++ /dev/null @@ -1,450 +0,0 @@ -use crate::defs::{DISABLE_FILE_NAME, KSU_MOUNT_SOURCE, MODULE_DIR, SKIP_MOUNT_FILE_NAME}; -use crate::magic_mount::NodeFileType::{Directory, RegularFile, Symlink, Whiteout}; -use crate::restorecon::{lgetfilecon, lsetfilecon}; -use crate::utils::{ensure_dir_exists, get_work_dir}; -use anyhow::{Context, Result, bail}; -use extattr::lgetxattr; -use rustix::fs::{ - Gid, MetadataExt, Mode, MountFlags, MountPropagationFlags, Uid, UnmountFlags, bind_mount, - chmod, chown, mount, move_mount, remount, unmount, -}; -use rustix::mount::mount_change; -use rustix::path::Arg; -use std::cmp::PartialEq; -use std::collections::HashMap; -use std::collections::hash_map::Entry; -use std::fs; -use std::fs::{DirEntry, FileType, create_dir, create_dir_all, read_dir, read_link}; -use std::os::unix::fs::{FileTypeExt, symlink}; -use std::path::{Path, PathBuf}; - -const REPLACE_DIR_XATTR: &str = "trusted.overlay.opaque"; - -#[derive(PartialEq, Eq, Hash, Clone, Debug)] -enum NodeFileType { - RegularFile, - Directory, - Symlink, - Whiteout, -} - -impl NodeFileType { - fn from_file_type(file_type: FileType) -> Option { - if file_type.is_file() { - Some(RegularFile) - } else if file_type.is_dir() { - Some(Directory) - } else if file_type.is_symlink() { - Some(Symlink) - } else { - None - } - } -} - -#[derive(Debug)] -struct Node { - name: String, - file_type: NodeFileType, - children: HashMap, - // the module that owned this node - module_path: Option, - replace: bool, - skip: bool, -} - -impl Node { - fn collect_module_files>(&mut self, module_dir: T) -> Result { - let dir = module_dir.as_ref(); - let mut has_file = false; - for entry in dir.read_dir()?.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - - let node = match self.children.entry(name.clone()) { - Entry::Occupied(o) => Some(o.into_mut()), - Entry::Vacant(v) => Self::new_module(&name, &entry).map(|it| v.insert(it)), - }; - - if let Some(node) = node { - has_file |= if node.file_type == Directory { - node.collect_module_files(dir.join(&node.name))? || node.replace - } else { - true - } - } - } - - Ok(has_file) - } - - fn new_root(name: T) -> Self { - Node { - name: name.to_string(), - file_type: Directory, - children: Default::default(), - module_path: None, - replace: false, - skip: false, - } - } - - fn new_module(name: T, entry: &DirEntry) -> Option { - if let Ok(metadata) = entry.metadata() { - let path = entry.path(); - let file_type = if metadata.file_type().is_char_device() && metadata.rdev() == 0 { - Some(Whiteout) - } else { - NodeFileType::from_file_type(metadata.file_type()) - }; - if let Some(file_type) = file_type { - let mut replace = false; - if file_type == Directory { - if let Ok(v) = lgetxattr(&path, REPLACE_DIR_XATTR) { - if String::from_utf8_lossy(&v) == "y" { - replace = true; - } - } - } - return Some(Node { - name: name.to_string(), - file_type, - children: Default::default(), - module_path: Some(path), - replace, - skip: false, - }); - } - } - - None - } -} - -fn collect_module_files() -> Result> { - let mut root = Node::new_root(""); - let mut system = Node::new_root("system"); - let module_root = Path::new(MODULE_DIR); - let mut has_file = false; - for entry in module_root.read_dir()?.flatten() { - if !entry.file_type()?.is_dir() { - continue; - } - - if entry.path().join(DISABLE_FILE_NAME).exists() - || entry.path().join(SKIP_MOUNT_FILE_NAME).exists() - { - continue; - } - - let mod_system = entry.path().join("system"); - if !mod_system.is_dir() { - continue; - } - - log::debug!("collecting {}", entry.path().display()); - - has_file |= system.collect_module_files(&mod_system)?; - } - - if has_file { - for (partition, require_symlink) in [ - ("vendor", true), - ("system_ext", true), - ("product", true), - ("odm", false), - ] { - let path_of_root = Path::new("/").join(partition); - let path_of_system = Path::new("/system").join(partition); - if path_of_root.is_dir() && (!require_symlink || path_of_system.is_symlink()) { - let name = partition.to_string(); - if let Some(node) = system.children.remove(&name) { - root.children.insert(name, node); - } - } - } - root.children.insert("system".to_string(), system); - Ok(Some(root)) - } else { - Ok(None) - } -} - -fn clone_symlink, Dst: AsRef>(src: Src, dst: Dst) -> Result<()> { - let src_symlink = read_link(src.as_ref())?; - symlink(&src_symlink, dst.as_ref())?; - lsetfilecon(dst.as_ref(), lgetfilecon(src.as_ref())?.as_str())?; - log::debug!( - "clone symlink {} -> {}({})", - dst.as_ref().display(), - dst.as_ref().display(), - src_symlink.display() - ); - Ok(()) -} - -fn mount_mirror, WP: AsRef>( - path: P, - work_dir_path: WP, - entry: &DirEntry, -) -> Result<()> { - let path = path.as_ref().join(entry.file_name()); - let work_dir_path = work_dir_path.as_ref().join(entry.file_name()); - let file_type = entry.file_type()?; - - if file_type.is_file() { - log::debug!( - "mount mirror file {} -> {}", - path.display(), - work_dir_path.display() - ); - fs::File::create(&work_dir_path)?; - bind_mount(&path, &work_dir_path)?; - } else if file_type.is_dir() { - log::debug!( - "mount mirror dir {} -> {}", - path.display(), - work_dir_path.display() - ); - create_dir(&work_dir_path)?; - let metadata = entry.metadata()?; - chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?; - unsafe { - chown( - &work_dir_path, - Some(Uid::from_raw(metadata.uid())), - Some(Gid::from_raw(metadata.gid())), - )?; - } - lsetfilecon(&work_dir_path, lgetfilecon(&path)?.as_str())?; - for entry in read_dir(&path)?.flatten() { - mount_mirror(&path, &work_dir_path, &entry)?; - } - } else if file_type.is_symlink() { - log::debug!( - "create mirror symlink {} -> {}", - path.display(), - work_dir_path.display() - ); - clone_symlink(&path, &work_dir_path)?; - } - - Ok(()) -} - -fn do_magic_mount, WP: AsRef>( - path: P, - work_dir_path: WP, - current: Node, - has_tmpfs: bool, -) -> Result<()> { - let mut current = current; - let path = path.as_ref().join(¤t.name); - let work_dir_path = work_dir_path.as_ref().join(¤t.name); - match current.file_type { - RegularFile => { - let target_path = if has_tmpfs { - fs::File::create(&work_dir_path)?; - &work_dir_path - } else { - &path - }; - if let Some(module_path) = ¤t.module_path { - log::debug!( - "mount module file {} -> {}", - module_path.display(), - work_dir_path.display() - ); - bind_mount(module_path, target_path).with_context(|| { - format!("mount module file {module_path:?} -> {work_dir_path:?}") - })?; - // we should use MS_REMOUNT | MS_BIND | MS_xxx to change mount flags - if let Err(e) = remount(target_path, MountFlags::RDONLY | MountFlags::BIND, "") { - log::warn!("make file {target_path:?} ro: {e:#?}"); - } - } else { - bail!("cannot mount root file {}!", path.display()); - } - } - Symlink => { - if let Some(module_path) = ¤t.module_path { - log::debug!( - "create module symlink {} -> {}", - module_path.display(), - work_dir_path.display() - ); - clone_symlink(module_path, &work_dir_path).with_context(|| { - format!("create module symlink {module_path:?} -> {work_dir_path:?}") - })?; - } else { - bail!("cannot mount root symlink {}!", path.display()); - } - } - Directory => { - let mut create_tmpfs = !has_tmpfs && current.replace && current.module_path.is_some(); - if !has_tmpfs && !create_tmpfs { - for it in &mut current.children { - let (name, node) = it; - let real_path = path.join(name); - let need = match node.file_type { - Symlink => true, - Whiteout => real_path.exists(), - _ => { - if let Ok(metadata) = real_path.symlink_metadata() { - let file_type = NodeFileType::from_file_type(metadata.file_type()) - .unwrap_or(Whiteout); - file_type != node.file_type || file_type == Symlink - } else { - // real path not exists - true - } - } - }; - if need { - if current.module_path.is_none() { - log::error!( - "cannot create tmpfs on {}, ignore: {name}", - path.display() - ); - node.skip = true; - continue; - } - create_tmpfs = true; - break; - } - } - } - - let has_tmpfs = has_tmpfs || create_tmpfs; - - if has_tmpfs { - log::debug!( - "creating tmpfs skeleton for {} at {}", - path.display(), - work_dir_path.display() - ); - create_dir_all(&work_dir_path)?; - let (metadata, path) = if path.exists() { - (path.metadata()?, &path) - } else if let Some(module_path) = ¤t.module_path { - (module_path.metadata()?, module_path) - } else { - bail!("cannot mount root dir {}!", path.display()); - }; - chmod(&work_dir_path, Mode::from_raw_mode(metadata.mode()))?; - unsafe { - chown( - &work_dir_path, - Some(Uid::from_raw(metadata.uid())), - Some(Gid::from_raw(metadata.gid())), - )?; - } - lsetfilecon(&work_dir_path, lgetfilecon(path)?.as_str())?; - } - - if create_tmpfs { - log::debug!( - "creating tmpfs for {} at {}", - path.display(), - work_dir_path.display() - ); - bind_mount(&work_dir_path, &work_dir_path) - .context("bind self") - .with_context(|| format!("creating tmpfs for {path:?} at {work_dir_path:?}"))?; - } - - if path.exists() && !current.replace { - for entry in path.read_dir()?.flatten() { - let name = entry.file_name().to_string_lossy().to_string(); - let result = if let Some(node) = current.children.remove(&name) { - if node.skip { - continue; - } - do_magic_mount(&path, &work_dir_path, node, has_tmpfs) - .with_context(|| format!("magic mount {}/{name}", path.display())) - } else if has_tmpfs { - mount_mirror(&path, &work_dir_path, &entry) - .with_context(|| format!("mount mirror {}/{name}", path.display())) - } else { - Ok(()) - }; - - if let Err(e) = result { - if has_tmpfs { - return Err(e); - } else { - log::error!("mount child {}/{name} failed: {e:#?}", path.display()); - } - } - } - } - - if current.replace { - if current.module_path.is_none() { - bail!( - "dir {} is declared as replaced but it is root!", - path.display() - ); - } else { - log::debug!("dir {} is replaced", path.display()); - } - } - - for (name, node) in current.children.into_iter() { - if node.skip { - continue; - } - if let Err(e) = do_magic_mount(&path, &work_dir_path, node, has_tmpfs) - .with_context(|| format!("magic mount {}/{name}", path.display())) - { - if has_tmpfs { - return Err(e); - } else { - log::error!("mount child {}/{name} failed: {e:#?}", path.display()); - } - } - } - - if create_tmpfs { - log::debug!( - "moving tmpfs {} -> {}", - work_dir_path.display(), - path.display() - ); - if let Err(e) = remount(&work_dir_path, MountFlags::RDONLY | MountFlags::BIND, "") { - log::warn!("make dir {path:?} ro: {e:#?}"); - } - move_mount(&work_dir_path, &path) - .context("move self") - .with_context(|| format!("moving tmpfs {work_dir_path:?} -> {path:?}"))?; - // make private to reduce peer group count - if let Err(e) = mount_change(&path, MountPropagationFlags::PRIVATE) { - log::warn!("make dir {path:?} private: {e:#?}"); - } - } - } - Whiteout => { - log::debug!("file {} is removed", path.display()); - } - } - - Ok(()) -} - -pub fn magic_mount() -> Result<()> { - if let Some(root) = collect_module_files()? { - log::debug!("collected: {:#?}", root); - let tmp_dir = PathBuf::from(get_work_dir()); - ensure_dir_exists(&tmp_dir)?; - mount(KSU_MOUNT_SOURCE, &tmp_dir, "tmpfs", MountFlags::empty(), "").context("mount tmp")?; - mount_change(&tmp_dir, MountPropagationFlags::PRIVATE).context("make tmp private")?; - let result = do_magic_mount("/", &tmp_dir, root, false); - if let Err(e) = unmount(&tmp_dir, UnmountFlags::DETACH) { - log::error!("failed to unmount tmp {}", e); - } - fs::remove_dir(tmp_dir).ok(); - result - } else { - log::info!("no modules to mount, skipping!"); - Ok(()) - } -} diff --git a/userspace/ksud/src/main.rs b/userspace/ksud/src/main.rs index 9e2f35c..945aeae 100644 --- a/userspace/ksud/src/main.rs +++ b/userspace/ksud/src/main.rs @@ -4,18 +4,22 @@ mod boot_patch; mod cli; mod debug; mod defs; +mod feature; mod init_event; +#[cfg(target_arch = "aarch64")] mod kpm; mod ksucalls; -#[cfg(target_os = "android")] -mod magic_mount; +mod metamodule; mod module; +mod module_config; mod profile; mod restorecon; mod sepolicy; mod su; -mod utils; +#[cfg(target_os = "android")] mod uid_scanner; +mod umount_manager; +mod utils; fn main() -> anyhow::Result<()> { cli::run() diff --git a/userspace/ksud/src/metamodule.rs b/userspace/ksud/src/metamodule.rs new file mode 100644 index 0000000..a49a9b6 --- /dev/null +++ b/userspace/ksud/src/metamodule.rs @@ -0,0 +1,287 @@ +//! Metamodule management +//! +//! This module handles all metamodule-related functionality. +//! Metamodules are special modules that manage how regular modules are mounted +//! and provide hooks for module installation/uninstallation. + +use anyhow::{Context, Result, ensure}; +use log::{info, warn}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + process::Command, +}; + +use crate::module::ModuleType::All; +use crate::{assets, defs}; + +/// Determine whether the provided module properties mark it as a metamodule +pub fn is_metamodule(props: &HashMap) -> bool { + props + .get("metamodule") + .map(|s| { + let trimmed = s.trim(); + trimmed == "1" || trimmed.eq_ignore_ascii_case("true") + }) + .unwrap_or(false) +} + +/// Get metamodule path if it exists +/// The metamodule is stored in /data/adb/modules/{id} with a symlink at /data/adb/metamodule +pub fn get_metamodule_path() -> Option { + let path = Path::new(defs::METAMODULE_DIR); + + // Check if symlink exists and resolve it + if path.is_symlink() + && let Ok(target) = std::fs::read_link(path) + { + // If target is relative, resolve it + let resolved = if target.is_absolute() { + target + } else { + path.parent()?.join(target) + }; + + if resolved.exists() && resolved.is_dir() { + return Some(resolved); + } else { + warn!( + "Metamodule symlink points to non-existent path: {:?}", + resolved + ); + } + } + + // Fallback: search for metamodule=1 in modules directory + let mut result = None; + let _ = crate::module::foreach_module(All, |module_path| { + if let Ok(props) = crate::module::read_module_prop(module_path) + && is_metamodule(&props) + { + info!("Found metamodule in modules directory: {:?}", module_path); + result = Some(module_path.to_path_buf()); + } + Ok(()) + }); + + result +} + +/// Check if metamodule exists +pub fn has_metamodule() -> bool { + get_metamodule_path().is_some() +} + +/// Check if it's safe to install a regular module +/// Returns Ok(()) if safe, Err(is_disabled) if blocked +/// - Err(true) means metamodule is disabled +/// - Err(false) means metamodule is in other unstable state +pub fn check_install_safety() -> Result<(), bool> { + // No metamodule → safe + let Some(metamodule_path) = get_metamodule_path() else { + return Ok(()); + }; + + // No metainstall.sh → safe (uses default installer) + // The staged update directory may contain the latest scripts, so check both locations + let has_metainstall = metamodule_path + .join(defs::METAMODULE_METAINSTALL_SCRIPT) + .exists() + || metamodule_path.file_name().is_some_and(|module_id| { + Path::new(defs::MODULE_UPDATE_DIR) + .join(module_id) + .join(defs::METAMODULE_METAINSTALL_SCRIPT) + .exists() + }); + if !has_metainstall { + return Ok(()); + } + + // Check for marker files + let has_update = metamodule_path.join(defs::UPDATE_FILE_NAME).exists(); + let has_remove = metamodule_path.join(defs::REMOVE_FILE_NAME).exists(); + let has_disable = metamodule_path.join(defs::DISABLE_FILE_NAME).exists(); + + // Stable state (no markers) → safe + if !has_update && !has_remove && !has_disable { + return Ok(()); + } + + // Return true if disabled, false for other unstable states + Err(has_disable && !has_update && !has_remove) +} + +/// Create or update the metamodule symlink +/// Points /data/adb/metamodule -> /data/adb/modules/{module_id} +pub(crate) fn ensure_symlink(module_path: &Path) -> Result<()> { + // METAMODULE_DIR might have trailing slash, so we need to trim it + let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/')); + + info!( + "Creating metamodule symlink: {:?} -> {:?}", + symlink_path, module_path + ); + + // Remove existing symlink if it exists + if symlink_path.exists() || symlink_path.is_symlink() { + info!("Removing old metamodule symlink/path"); + if symlink_path.is_symlink() { + std::fs::remove_file(symlink_path).with_context(|| "Failed to remove old symlink")?; + } else { + // Could be a directory, remove it + std::fs::remove_dir_all(symlink_path) + .with_context(|| "Failed to remove old directory")?; + } + } + + // Create symlink + #[cfg(unix)] + std::os::unix::fs::symlink(module_path, symlink_path) + .with_context(|| format!("Failed to create symlink to {:?}", module_path))?; + + info!("Metamodule symlink created successfully"); + Ok(()) +} + +/// Remove the metamodule symlink +pub(crate) fn remove_symlink() -> Result<()> { + let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/')); + + if symlink_path.is_symlink() { + std::fs::remove_file(symlink_path) + .with_context(|| "Failed to remove metamodule symlink")?; + info!("Metamodule symlink removed"); + } + + Ok(()) +} + +/// Get the install script content, using metainstall.sh from metamodule if available +/// Returns the script content to be executed +pub(crate) fn get_install_script( + is_metamodule: bool, + installer_content: &str, + install_module_script: &str, +) -> Result { + // Check if there's a metamodule with metainstall.sh + // Only apply this logic for regular modules (not when installing metamodule itself) + let install_script = if !is_metamodule { + if let Some(metamodule_path) = get_metamodule_path() { + if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() { + info!("Metamodule is disabled, using default installer"); + install_module_script.to_string() + } else { + let metainstall_path = metamodule_path.join(defs::METAMODULE_METAINSTALL_SCRIPT); + + if metainstall_path.exists() { + info!("Using metainstall.sh from metamodule"); + let metamodule_content = std::fs::read_to_string(&metainstall_path) + .with_context(|| "Failed to read metamodule metainstall.sh")?; + format!("{}\n{}\nexit 0\n", installer_content, metamodule_content) + } else { + info!("Metamodule exists but has no metainstall.sh, using default installer"); + install_module_script.to_string() + } + } + } else { + info!("No metamodule found, using default installer"); + install_module_script.to_string() + } + } else { + info!("Installing metamodule, using default installer"); + install_module_script.to_string() + }; + + Ok(install_script) +} + +/// Check if metamodule script exists and is ready to execute +/// Returns None if metamodule doesn't exist, is disabled, or script is missing +/// Returns Some(script_path) if script is ready to execute +fn check_metamodule_script(script_name: &str) -> Option { + // Check if metamodule exists + let metamodule_path = get_metamodule_path()?; + + // Check if metamodule is disabled + if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() { + info!("Metamodule is disabled, skipping {}", script_name); + return None; + } + + // Check if script exists + let script_path = metamodule_path.join(script_name); + if !script_path.exists() { + return None; + } + + Some(script_path) +} + +/// Execute metamodule's metauninstall.sh for a specific module +pub(crate) fn exec_metauninstall_script(module_id: &str) -> Result<()> { + let Some(metauninstall_path) = check_metamodule_script(defs::METAMODULE_METAUNINSTALL_SCRIPT) + else { + return Ok(()); + }; + + info!( + "Executing metamodule metauninstall.sh for module: {}", + module_id + ); + + let result = Command::new(assets::BUSYBOX_PATH) + .args(["sh", metauninstall_path.to_str().unwrap()]) + .current_dir(metauninstall_path.parent().unwrap()) + .envs(crate::module::get_common_script_envs()) + .env("MODULE_ID", module_id) + .status()?; + + ensure!( + result.success(), + "Metamodule metauninstall.sh failed for module {}: {:?}", + module_id, + result + ); + + info!( + "Metamodule metauninstall.sh executed successfully for {}", + module_id + ); + Ok(()) +} + +/// Execute metamodule mount script +pub fn exec_mount_script(module_dir: &str) -> Result<()> { + let Some(mount_script) = check_metamodule_script(defs::METAMODULE_MOUNT_SCRIPT) else { + return Ok(()); + }; + + info!("Executing mount script for metamodule"); + + let result = Command::new(assets::BUSYBOX_PATH) + .args(["sh", mount_script.to_str().unwrap()]) + .envs(crate::module::get_common_script_envs()) + .env("MODULE_DIR", module_dir) + .status()?; + + ensure!( + result.success(), + "Metamodule mount script failed with status: {:?}", + result + ); + + info!("Metamodule mount script executed successfully"); + Ok(()) +} + +/// Execute metamodule script for a specific stage +pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> { + let Some(script_path) = check_metamodule_script(&format!("{}.sh", stage)) else { + return Ok(()); + }; + + info!("Executing metamodule {}.sh", stage); + crate::module::exec_script(&script_path, block)?; + info!("Metamodule {}.sh executed successfully", stage); + Ok(()) +} diff --git a/userspace/ksud/src/module.rs b/userspace/ksud/src/module.rs index f84f316..5202e0d 100644 --- a/userspace/ksud/src/module.rs +++ b/userspace/ksud/src/module.rs @@ -1,7 +1,7 @@ #[allow(clippy::wildcard_imports)] use crate::utils::*; use crate::{ - assets, defs, ksucalls, + assets, defs, ksucalls, metamodule, restorecon::{restore_syscon, setsyscon}, sepolicy, }; @@ -10,13 +10,13 @@ use anyhow::{Context, Result, anyhow, bail, ensure}; use const_format::concatcp; use is_executable::is_executable; use java_properties::PropertiesIter; -use log::{info, warn}; +use log::{debug, info, warn}; use std::fs::{copy, rename}; use std::{ collections::HashMap, env::var as env_var, - fs::{File, Permissions, remove_dir_all, remove_file, set_permissions}, + fs::{File, Permissions, canonicalize, remove_dir_all, set_permissions}, io::Cursor, path::{Path, PathBuf}, process::Command, @@ -25,6 +25,7 @@ use std::{ use zip_extensions::zip_extract_file_to_memory; use crate::defs::{MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME}; +use crate::module::ModuleType::{Active, All}; #[cfg(unix)] use std::os::unix::{prelude::PermissionsExt, process::CommandExt}; @@ -38,27 +39,93 @@ const INSTALL_MODULE_SCRIPT: &str = concatcp!( "\n" ); -fn exec_install_script(module_file: &str) -> Result<()> { - let realpath = std::fs::canonicalize(module_file) - .with_context(|| format!("realpath: {module_file} failed"))?; +/// Validate module_id format and security +/// Module ID must match: ^[a-zA-Z][a-zA-Z0-9._-]+$ +/// - Must start with a letter (a-zA-Z) +/// - Followed by one or more alphanumeric, dot, underscore, or hyphen characters +/// - Minimum length: 2 characters +pub fn validate_module_id(module_id: &str) -> Result<()> { + if module_id.is_empty() { + bail!("Module ID cannot be empty"); + } - let result = Command::new(assets::BUSYBOX_PATH) - .args(["sh", "-c", INSTALL_MODULE_SCRIPT]) - .env("ASH_STANDALONE", "1") - .env( + if module_id.len() < 2 { + bail!("Module ID too short: must be at least 2 characters"); + } + + if module_id.len() > 64 { + bail!( + "Module ID too long: {} characters (max: 64)", + module_id.len() + ); + } + + // Check first character: must be a letter + let first_char = module_id.chars().next().unwrap(); + if !first_char.is_ascii_alphabetic() { + bail!( + "Module ID must start with a letter (a-zA-Z), got: '{}'", + first_char + ); + } + + // Check remaining characters: alphanumeric, dot, underscore, or hyphen + for (i, ch) in module_id.chars().enumerate() { + if i == 0 { + continue; // Already checked + } + + if !ch.is_ascii_alphanumeric() && ch != '.' && ch != '_' && ch != '-' { + bail!( + "Module ID contains invalid character '{}' at position {}. Only letters, digits, '.', '_', and '-' are allowed", + ch, + i + ); + } + } + + // Additional security checks + if module_id.contains("..") { + bail!("Module ID cannot contain '..' sequence"); + } + + if module_id == "." || module_id == ".." { + bail!("Module ID cannot be '.' or '..'"); + } + + Ok(()) +} + +/// Get common environment variables for script execution +pub(crate) fn get_common_script_envs() -> Vec<(&'static str, String)> { + vec![ + ("ASH_STANDALONE", "1".to_string()), + ("KSU", "true".to_string()), + ("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()), + ("KSU_VER_CODE", defs::VERSION_CODE.to_string()), + ("KSU_VER", defs::VERSION_NAME.to_string()), + ( "PATH", format!( "{}:{}", - env_var("PATH").unwrap(), + env_var("PATH").unwrap_or_default(), defs::BINARY_DIR.trim_end_matches('/') ), - ) - .env("KSU", "true") - .env("KSU_SUKISU", "true") - .env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()) - .env("KSU_VER", defs::VERSION_NAME) - .env("KSU_VER_CODE", defs::VERSION_CODE) - .env("KSU_MAGIC_MOUNT", "true") + ), + ] +} + +fn exec_install_script(module_file: &str, is_metamodule: bool) -> Result<()> { + let realpath = std::fs::canonicalize(module_file) + .with_context(|| format!("realpath: {module_file} failed"))?; + + // Get install script from metamodule module + let install_script = + metamodule::get_install_script(is_metamodule, INSTALLER_CONTENT, INSTALL_MODULE_SCRIPT)?; + + let result = Command::new(assets::BUSYBOX_PATH) + .args(["sh", "-c", &install_script]) + .envs(get_common_script_envs()) .env("OUTFD", "1") .env("ZIPFILE", realpath) .status()?; @@ -66,10 +133,7 @@ fn exec_install_script(module_file: &str) -> Result<()> { Ok(()) } -// becuase we use something like A-B update -// we need to update the module state after the boot_completed -// if someone(such as the module) install a module before the boot_completed -// then it may cause some problems, just forbid it +// Check if Android boot is completed before installing modules fn ensure_boot_completed() -> Result<()> { // ensure getprop sys.boot_completed == 1 if getprop("sys.boot_completed").as_deref() != Some("1") { @@ -78,34 +142,21 @@ fn ensure_boot_completed() -> Result<()> { Ok(()) } -fn mark_module_state(module: &str, flag_file: &str, create: bool) -> Result<()> { - let module_state_file = Path::new(MODULE_DIR).join(module).join(flag_file); - if create { - ensure_file_exists(module_state_file) - } else { - if module_state_file.exists() { - remove_file(module_state_file)?; - } - Ok(()) - } -} - #[derive(PartialEq, Eq)] -enum ModuleType { +pub(crate) enum ModuleType { All, Active, Updated, } -fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<()>) -> Result<()> { +pub(crate) fn foreach_module( + module_type: ModuleType, + mut f: impl FnMut(&Path) -> Result<()>, +) -> Result<()> { let modules_dir = Path::new(match module_type { ModuleType::Updated => MODULE_UPDATE_DIR, _ => defs::MODULE_DIR, }); - if !modules_dir.is_dir() { - warn!("{} is not a directory, skip", modules_dir.display()); - return Ok(()); - } let dir = std::fs::read_dir(modules_dir)?; for entry in dir.flatten() { let path = entry.path(); @@ -114,11 +165,11 @@ fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<() continue; } - if module_type == ModuleType::Active && path.join(defs::DISABLE_FILE_NAME).exists() { + if module_type == Active && path.join(defs::DISABLE_FILE_NAME).exists() { info!("{} is disabled, skip", path.display()); continue; } - if module_type == ModuleType::Active && path.join(defs::REMOVE_FILE_NAME).exists() { + if module_type == Active && path.join(defs::REMOVE_FILE_NAME).exists() { warn!("{} is removed, skip", path.display()); continue; } @@ -130,7 +181,7 @@ fn foreach_module(module_type: ModuleType, mut f: impl FnMut(&Path) -> Result<() } fn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> { - foreach_module(ModuleType::Active, f) + foreach_module(Active, f) } pub fn load_sepolicy_rule() -> Result<()> { @@ -150,9 +201,44 @@ pub fn load_sepolicy_rule() -> Result<()> { Ok(()) } -fn exec_script>(path: T, wait: bool) -> Result<()> { +pub fn exec_script>(path: T, wait: bool) -> Result<()> { info!("exec {}", path.as_ref().display()); + // Extract module_id from path if it matches /data/adb/modules/{id}/... + let module_id = path + .as_ref() + .strip_prefix(defs::MODULE_DIR) + .ok() + .and_then(|p| p.components().next()) + .and_then(|c| c.as_os_str().to_str()) + .map(|s| s.to_string()); + + // Validate and log module_id extraction + let validated_module_id = module_id + .as_ref() + .and_then(|id| match validate_module_id(id) { + Ok(_) => { + debug!("Module ID extracted from script path: '{}'", id); + Some(id.as_str()) + } + Err(e) => { + warn!( + "Invalid module ID '{}' extracted from script path '{}': {}", + id, + path.as_ref().display(), + e + ); + None + } + }); + + if module_id.is_none() { + debug!( + "Failed to extract module_id from script path '{}'. Script will run without KSU_MODULE environment variable.", + path.as_ref().display() + ); + } + let mut command = &mut Command::new(assets::BUSYBOX_PATH); #[cfg(unix)] { @@ -169,21 +255,12 @@ fn exec_script>(path: T, wait: bool) -> Result<()> { .current_dir(path.as_ref().parent().unwrap()) .arg("sh") .arg(path.as_ref()) - .env("ASH_STANDALONE", "1") - .env("KSU", "true") - .env("KSU_SUKISU", "true") - .env("KSU_KERNEL_VER_CODE", ksucalls::get_version().to_string()) - .env("KSU_VER_CODE", defs::VERSION_CODE) - .env("KSU_VER", defs::VERSION_NAME) - .env("KSU_MAGIC_MOUNT", "true") - .env( - "PATH", - format!( - "{}:{}", - env_var("PATH").unwrap(), - defs::BINARY_DIR.trim_end_matches('/') - ), - ); + .envs(get_common_script_envs()); + + // Set KSU_MODULE environment variable if module_id was validated successfully + if let Some(id) = validated_module_id { + command = command.env("KSU_MODULE", id); + } let result = if wait { command.status().map(|_| ()) @@ -194,7 +271,17 @@ fn exec_script>(path: T, wait: bool) -> Result<()> { } pub fn exec_stage_script(stage: &str, block: bool) -> Result<()> { + let metamodule_dir = metamodule::get_metamodule_path().and_then(|path| canonicalize(path).ok()); + foreach_active_module(|module| { + if metamodule_dir.as_ref().is_some_and(|meta_dir| { + canonicalize(module) + .map(|resolved| resolved == *meta_dir) + .unwrap_or(false) + }) { + return Ok(()); + } + let script_path = module.join(format!("{stage}.sh")); if !script_path.exists() { return Ok(()); @@ -251,45 +338,95 @@ pub fn load_system_prop() -> Result<()> { } pub fn prune_modules() -> Result<()> { - foreach_module(ModuleType::All, |module| { - if module.join(defs::REMOVE_FILE_NAME).exists() { - info!("remove module: {}", module.display()); - - let uninstaller = module.join("uninstall.sh"); - if uninstaller.exists() { - if let Err(e) = exec_script(uninstaller, true) { - warn!("Failed to exec uninstaller: {}", e); - } - } - - if let Err(e) = remove_dir_all(module) { - warn!("Failed to remove {}: {}", module.display(), e); - } - } else { - remove_file(module.join(defs::UPDATE_FILE_NAME)).ok(); + foreach_module(All, |module| { + if !module.join(defs::REMOVE_FILE_NAME).exists() { + return Ok(()); } + + info!("remove module: {}", module.display()); + + // Execute metamodule's metauninstall.sh first + let module_id = module.file_name().and_then(|n| n.to_str()).unwrap_or(""); + + // Check if this is a metamodule + let is_metamodule = read_module_prop(module) + .map(|props| metamodule::is_metamodule(&props)) + .unwrap_or(false); + + if is_metamodule { + info!("Removing metamodule symlink"); + if let Err(e) = metamodule::remove_symlink() { + warn!("Failed to remove metamodule symlink: {}", e); + } + } else if let Err(e) = metamodule::exec_metauninstall_script(module_id) { + warn!( + "Failed to exec metamodule uninstall for {}: {}", + module_id, e + ); + } + + // Then execute module's own uninstall.sh + let uninstaller = module.join("uninstall.sh"); + if uninstaller.exists() + && let Err(e) = exec_script(uninstaller, true) + { + warn!("Failed to exec uninstaller: {e}"); + } + + // Clear module configs before removing module directory + if let Err(e) = crate::module_config::clear_module_configs(module_id) { + warn!("Failed to clear configs for {}: {}", module_id, e); + } + + // Finally remove the module directory + if let Err(e) = remove_dir_all(module) { + warn!("Failed to remove {}: {}", module.display(), e); + } + Ok(()) })?; + // collect remaining modules, if none, clean up metamodule record + let remaining_modules: Vec<_> = std::fs::read_dir(defs::MODULE_DIR)? + .filter_map(|entry| entry.ok()) + .filter(|entry| entry.path().join("module.prop").exists()) + .collect(); + + if remaining_modules.is_empty() { + info!("no remaining modules."); + } + Ok(()) } pub fn handle_updated_modules() -> Result<()> { let modules_root = Path::new(MODULE_DIR); - foreach_module(ModuleType::Updated, |module| { - if !module.is_dir() { + foreach_module(ModuleType::Updated, |updated_module| { + if !updated_module.is_dir() { return Ok(()); } - if let Some(name) = module.file_name() { - let old_dir = modules_root.join(name); - if old_dir.exists() { - if let Err(e) = remove_dir_all(&old_dir) { - log::error!("Failed to remove old {}: {}", old_dir.display(), e); - } + if let Some(name) = updated_module.file_name() { + let module_dir = modules_root.join(name); + let mut disabled = false; + let mut removed = false; + if module_dir.exists() { + // If the old module is disabled, we need to also disable the new one + disabled = module_dir.join(defs::DISABLE_FILE_NAME).exists(); + removed = module_dir.join(defs::REMOVE_FILE_NAME).exists(); + remove_dir_all(&module_dir)?; } - if let Err(e) = rename(module, &old_dir) { - log::error!("Failed to move new module {}: {}", module.display(), e); + rename(updated_module, &module_dir)?; + if removed { + let path = module_dir.join(defs::REMOVE_FILE_NAME); + if let Err(e) = ensure_file_exists(&path) { + warn!("Failed to create {}: {}", path.display(), e); + } + } else if disabled { + let path = module_dir.join(defs::DISABLE_FILE_NAME); + if let Err(e) = ensure_file_exists(&path) { + warn!("Failed to create {}: {}", path.display(), e); + } } } Ok(()) @@ -297,120 +434,226 @@ pub fn handle_updated_modules() -> Result<()> { Ok(()) } -pub fn install_module(zip: &str) -> Result<()> { - fn inner(zip: &str) -> Result<()> { - ensure_boot_completed()?; +fn _install_module(zip: &str) -> Result<()> { + ensure_boot_completed()?; - // print banner - println!(include_str!("banner")); + // print banner + println!(include_str!("banner")); - assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?; + assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?; - // first check if working dir is usable - ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?; - ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?; + // first check if working dir is usable + ensure_dir_exists(defs::WORKING_DIR).with_context(|| "Failed to create working dir")?; + ensure_dir_exists(defs::BINARY_DIR).with_context(|| "Failed to create bin dir")?; - // read the module_id from zip, if failed it will return early. - let mut buffer: Vec = Vec::new(); - let entry_path = PathBuf::from_str("module.prop")?; - let zip_path = PathBuf::from_str(zip)?; - let zip_path = zip_path.canonicalize()?; - zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?; + // read the module_id from zip, if failed it will return early. + let mut buffer: Vec = Vec::new(); + let entry_path = PathBuf::from_str("module.prop")?; + let zip_path = PathBuf::from_str(zip)?; + let zip_path = zip_path.canonicalize()?; + zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?; - let mut module_prop = HashMap::new(); - PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into( - |k, v| { - module_prop.insert(k, v); - }, - )?; - info!("module prop: {:?}", module_prop); + let mut module_prop = HashMap::new(); + PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into( + |k, v| { + module_prop.insert(k, v); + }, + )?; + info!("module prop: {module_prop:?}"); - let Some(module_id) = module_prop.get("id") else { - bail!("module id not found in module.prop!"); - }; - let module_id = module_id.trim(); + let Some(module_id) = module_prop.get("id") else { + bail!("module id not found in module.prop!"); + }; + let module_id = module_id.trim(); - let zip_uncompressed_size = get_zip_uncompressed_size(zip)?; + // Validate module_id format + validate_module_id(module_id) + .with_context(|| format!("Invalid module ID in module.prop: '{}'", module_id))?; - info!( - "zip uncompressed size: {}", - humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) - ); + // Check if this module is a metamodule + let is_metamodule = metamodule::is_metamodule(&module_prop); - println!("- Preparing Zip"); - println!( - "- Module size: {}", - humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) - ); - - // ensure modules_update exists - ensure_dir_exists(MODULE_UPDATE_DIR)?; - setsyscon(MODULE_UPDATE_DIR)?; - - let update_module_dir = Path::new(MODULE_UPDATE_DIR).join(module_id); - ensure_clean_dir(&update_module_dir)?; - info!("module dir: {}", update_module_dir.display()); - - let do_install = || -> Result<()> { - // unzip the image and move it to modules_update/ dir - let file = File::open(zip)?; - let mut archive = zip::ZipArchive::new(file)?; - archive.extract(&update_module_dir)?; - - // set permission and selinux context for $MOD/system - let module_system_dir = update_module_dir.join("system"); - if module_system_dir.exists() { - #[cfg(unix)] - set_permissions(&module_system_dir, Permissions::from_mode(0o755))?; - restore_syscon(&module_system_dir)?; - } - - exec_install_script(zip)?; - - let module_dir = Path::new(MODULE_DIR).join(module_id); - ensure_dir_exists(&module_dir)?; - copy( - update_module_dir.join("module.prop"), - module_dir.join("module.prop"), - )?; - ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?; - - info!("Module install successfully!"); - - Ok(()) - }; - let result = do_install(); - if result.is_err() { - remove_dir_all(&update_module_dir).ok(); + // Check if it's safe to install regular module + if !is_metamodule && let Err(is_disabled) = metamodule::check_install_safety() { + println!("\n❌ Installation Blocked"); + println!("┌────────────────────────────────"); + println!("│ A metamodule with custom installer is active"); + println!("│"); + if is_disabled { + println!("│ Current state: Disabled"); + println!("│ Action required: Re-enable or uninstall it, then reboot"); + } else { + println!("│ Current state: Pending changes"); + println!("│ Action required: Reboot to apply changes first"); } - result + println!("└─────────────────────────────────\n"); + bail!("Metamodule installation blocked"); } - let result = inner(zip); + + // All modules (including metamodules) are installed to MODULE_UPDATE_DIR + let updated_dir = Path::new(defs::MODULE_UPDATE_DIR).join(module_id); + + if is_metamodule { + info!("Installing metamodule: {}", module_id); + + // Check if there's already a metamodule installed + if metamodule::has_metamodule() + && let Some(existing_path) = metamodule::get_metamodule_path() + { + let existing_id = read_module_prop(&existing_path) + .ok() + .and_then(|m| m.get("id").cloned()) + .unwrap_or_else(|| "unknown".to_string()); + + if existing_id != module_id { + println!("\n❌ Installation Failed"); + println!("┌────────────────────────────────"); + println!("│ A metamodule is already installed"); + println!("│ Current metamodule: {}", existing_id); + println!("│"); + println!("│ Only one metamodule can be active at a time."); + println!("│"); + println!("│ To install this metamodule:"); + println!("│ 1. Uninstall the current metamodule"); + println!("│ 2. Reboot your device"); + println!("│ 3. Install the new metamodule"); + println!("└─────────────────────────────────\n"); + bail!("Cannot install multiple metamodules"); + } + } + } + + let zip_uncompressed_size = get_zip_uncompressed_size(zip)?; + info!( + "zip uncompressed size: {}", + humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) + ); + println!( + "- Module size: {}", + humansize::format_size(zip_uncompressed_size, humansize::DECIMAL) + ); + + // Ensure module directory exists and set SELinux context + ensure_dir_exists(defs::MODULE_UPDATE_DIR)?; + setsyscon(defs::MODULE_UPDATE_DIR)?; + + // Prepare target directory + println!("- Installing to {}", updated_dir.display()); + ensure_clean_dir(&updated_dir)?; + info!("target dir: {}", updated_dir.display()); + + // Extract zip to target directory + println!("- Extracting module files"); + let file = File::open(zip)?; + let mut archive = zip::ZipArchive::new(file)?; + archive.extract(&updated_dir)?; + + // Set permission and selinux context for $MOD/system + let module_system_dir = updated_dir.join("system"); + if module_system_dir.exists() { + #[cfg(unix)] + set_permissions(&module_system_dir, Permissions::from_mode(0o755))?; + restore_syscon(&module_system_dir)?; + } + + // Execute install script + println!("- Running module installer"); + exec_install_script(zip, is_metamodule)?; + + let module_dir = Path::new(MODULE_DIR).join(module_id); + ensure_dir_exists(&module_dir)?; + copy( + updated_dir.join("module.prop"), + module_dir.join("module.prop"), + )?; + ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?; + + // Create symlink for metamodule + if is_metamodule { + println!("- Creating metamodule symlink"); + metamodule::ensure_symlink(&module_dir)?; + } + + println!("- Module installed successfully!"); + info!("Module {} installed successfully!", module_id); + + Ok(()) +} + +pub fn install_module(zip: &str) -> Result<()> { + let result = _install_module(zip); if let Err(ref e) = result { println!("- Error: {e}"); } result } -pub fn uninstall_module(id: &str) -> Result<()> { - mark_module_state(id, defs::REMOVE_FILE_NAME, true) +pub fn undo_uninstall_module(id: &str) -> Result<()> { + validate_module_id(id)?; + + let module_path = Path::new(defs::MODULE_DIR).join(id); + ensure!(module_path.exists(), "Module {} not found", id); + + // Remove the remove mark + let remove_file = module_path.join(defs::REMOVE_FILE_NAME); + if remove_file.exists() { + std::fs::remove_file(&remove_file) + .with_context(|| format!("Failed to delete remove file for module '{}'", id))?; + info!("Removed the remove mark for module {}", id); + } + + Ok(()) } -pub fn restore_uninstall_module(id: &str) -> Result<()> { - mark_module_state(id, defs::REMOVE_FILE_NAME, false) +pub fn uninstall_module(id: &str) -> Result<()> { + validate_module_id(id)?; + + let module_path = Path::new(defs::MODULE_DIR).join(id); + ensure!(module_path.exists(), "Module {} not found", id); + + // Mark for removal + let remove_file = module_path.join(defs::REMOVE_FILE_NAME); + File::create(remove_file).with_context(|| "Failed to create remove file")?; + + info!("Module {} marked for removal", id); + + Ok(()) } pub fn run_action(id: &str) -> Result<()> { + validate_module_id(id)?; + let action_script_path = format!("/data/adb/modules/{id}/action.sh"); exec_script(&action_script_path, true) } pub fn enable_module(id: &str) -> Result<()> { - mark_module_state(id, defs::DISABLE_FILE_NAME, false) + validate_module_id(id)?; + + let module_path = Path::new(defs::MODULE_DIR).join(id); + ensure!(module_path.exists(), "Module {} not found", id); + + let disable_path = module_path.join(defs::DISABLE_FILE_NAME); + if disable_path.exists() { + std::fs::remove_file(&disable_path).with_context(|| { + format!("Failed to remove disable file: {}", disable_path.display()) + })?; + info!("Module {} enabled", id); + } + + Ok(()) } pub fn disable_module(id: &str) -> Result<()> { - mark_module_state(id, defs::DISABLE_FILE_NAME, true) + let module_path = Path::new(defs::MODULE_DIR).join(id); + ensure!(module_path.exists(), "Module {} not found", id); + + let disable_path = module_path.join(defs::DISABLE_FILE_NAME); + ensure_file_exists(disable_path)?; + + info!("Module {} disabled", id); + + Ok(()) } pub fn disable_all_modules() -> Result<()> { @@ -418,11 +661,13 @@ pub fn disable_all_modules() -> Result<()> { } pub fn uninstall_all_modules() -> Result<()> { + info!("Uninstalling all modules"); mark_all_modules(defs::REMOVE_FILE_NAME) } fn mark_all_modules(flag_file: &str) -> Result<()> { - let dir = std::fs::read_dir(MODULE_DIR)?; + // we assume the module dir is already mounted + let dir = std::fs::read_dir(defs::MODULE_DIR)?; for entry in dir.flatten() { let path = entry.path(); let flag = path.join(flag_file); @@ -434,7 +679,38 @@ fn mark_all_modules(flag_file: &str) -> Result<()> { Ok(()) } +/// Read module.prop from the given module path and return as a HashMap +pub fn read_module_prop(module_path: &Path) -> Result> { + let module_prop = module_path.join("module.prop"); + ensure!( + module_prop.exists(), + "module.prop not found in {}", + module_path.display() + ); + + let content = std::fs::read(&module_prop) + .with_context(|| format!("Failed to read module.prop: {}", module_prop.display()))?; + + let mut prop_map: HashMap = HashMap::new(); + PropertiesIter::new_with_encoding(Cursor::new(content), encoding_rs::UTF_8) + .read_into(|k, v| { + prop_map.insert(k, v); + }) + .with_context(|| format!("Failed to parse module.prop: {}", module_prop.display()))?; + + Ok(prop_map) +} + fn _list_modules(path: &str) -> Vec> { + // Load all module configs once to minimize I/O overhead + let all_configs = match crate::module_config::get_all_module_configs() { + Ok(configs) => configs, + Err(e) => { + warn!("Failed to load module configs: {}", e); + HashMap::new() + } + }; + // first check enabled modules let dir = std::fs::read_dir(path); let Ok(dir) = dir else { @@ -446,47 +722,74 @@ fn _list_modules(path: &str) -> Vec> { for entry in dir.flatten() { let path = entry.path(); info!("path: {}", path.display()); - let module_prop = path.join("module.prop"); - if !module_prop.exists() { + + if !path.join("module.prop").exists() { continue; } - let content = std::fs::read(&module_prop); - let Ok(content) = content else { - warn!("Failed to read file: {}", module_prop.display()); - continue; + + let mut module_prop_map = match read_module_prop(&path) { + Ok(prop) => prop, + Err(e) => { + warn!("Failed to read module.prop for {}: {}", path.display(), e); + continue; + } }; - let mut module_prop_map: HashMap = HashMap::new(); - let encoding = encoding_rs::UTF_8; - let result = - PropertiesIter::new_with_encoding(Cursor::new(content), encoding).read_into(|k, v| { - module_prop_map.insert(k, v); - }); - - let dir_id = entry.file_name().to_string_lossy().to_string(); - module_prop_map.insert("dir_id".to_owned(), dir_id.clone()); + // If id is missing or empty, use directory name as fallback if !module_prop_map.contains_key("id") || module_prop_map["id"].is_empty() { - info!("Use dir name as module id: {dir_id}"); - module_prop_map.insert("id".to_owned(), dir_id.clone()); + match entry.file_name().to_str() { + Some(id) => { + info!("Use dir name as module id: {id}"); + module_prop_map.insert("id".to_owned(), id.to_owned()); + } + _ => { + info!("Failed to get module id from dir name"); + continue; + } + } } - // Add enabled, update, remove flags + // Add enabled, update, remove, web, action flags let enabled = !path.join(defs::DISABLE_FILE_NAME).exists(); let update = path.join(defs::UPDATE_FILE_NAME).exists(); let remove = path.join(defs::REMOVE_FILE_NAME).exists(); let web = path.join(defs::MODULE_WEB_DIR).exists(); let action = path.join(defs::MODULE_ACTION_SH).exists(); + let need_mount = path.join("system").exists() && !path.join("skip_mount").exists(); module_prop_map.insert("enabled".to_owned(), enabled.to_string()); module_prop_map.insert("update".to_owned(), update.to_string()); module_prop_map.insert("remove".to_owned(), remove.to_string()); module_prop_map.insert("web".to_owned(), web.to_string()); module_prop_map.insert("action".to_owned(), action.to_string()); + module_prop_map.insert("mount".to_owned(), need_mount.to_string()); - if result.is_err() { - warn!("Failed to parse module.prop: {}", module_prop.display()); - continue; + // Apply module config overrides and extract managed features + if let Some(module_id) = module_prop_map.get("id") + && let Some(config) = all_configs.get(module_id.as_str()) + { + // Apply override.description + if let Some(desc) = config.get("override.description") { + module_prop_map.insert("description".to_owned(), desc.clone()); + } + + // Extract managed features from manage.* config entries + let managed_features: Vec = config + .iter() + .filter_map(|(k, v)| { + if k.starts_with("manage.") && crate::module_config::parse_bool_config(v) { + k.strip_prefix("manage.").map(|f| f.to_string()) + } else { + None + } + }) + .collect(); + + if !managed_features.is_empty() { + module_prop_map.insert("managedFeatures".to_owned(), managed_features.join(",")); + } } + modules.push(module_prop_map); } @@ -498,3 +801,55 @@ pub fn list_modules() -> Result<()> { println!("{}", serde_json::to_string_pretty(&modules)?); Ok(()) } + +/// Get all managed features from active modules +/// Modules declare managed features via config system (manage.=true) +/// Returns: HashMap> +pub fn get_managed_features() -> Result>> { + let mut managed_features_map: HashMap> = HashMap::new(); + + foreach_active_module(|module_path| { + // Get module ID + let module_id = match module_path.file_name().and_then(|n| n.to_str()) { + Some(id) => id, + None => { + warn!( + "Failed to get module id from path: {}", + module_path.display() + ); + return Ok(()); + } + }; + + // Read module config + let config = match crate::module_config::merge_configs(module_id) { + Ok(c) => c, + Err(e) => { + warn!("Failed to merge configs for module '{}': {}", module_id, e); + return Ok(()); // Skip this module + } + }; + + // Extract manage.* config entries + let mut feature_list = Vec::new(); + for (key, value) in config.iter() { + if key.starts_with("manage.") { + // Parse feature name + if let Some(feature_name) = key.strip_prefix("manage.") + && crate::module_config::parse_bool_config(value) + { + info!("Module {} manages feature: {}", module_id, feature_name); + feature_list.push(feature_name.to_string()); + } + } + } + + if !feature_list.is_empty() { + managed_features_map.insert(module_id.to_string(), feature_list); + } + + Ok(()) + })?; + + Ok(managed_features_map) +} diff --git a/userspace/ksud/src/module_config.rs b/userspace/ksud/src/module_config.rs new file mode 100644 index 0000000..90518a3 --- /dev/null +++ b/userspace/ksud/src/module_config.rs @@ -0,0 +1,474 @@ +use anyhow::{Context, Result, bail}; +use log::{debug, warn}; +use std::collections::HashMap; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; + +use crate::defs; +use crate::utils::ensure_dir_exists; + +const MODULE_CONFIG_MAGIC: u32 = 0x4b53554d; // "KSUM" +const MODULE_CONFIG_VERSION: u32 = 1; + +// Validation limits +pub const MAX_CONFIG_KEY_LEN: usize = 256; +pub const MAX_CONFIG_VALUE_LEN: usize = 256; +pub const MAX_CONFIG_COUNT: usize = 32; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ConfigType { + Persist, + Temp, +} + +impl ConfigType { + fn filename(&self) -> &'static str { + match self { + ConfigType::Persist => defs::PERSIST_CONFIG_NAME, + ConfigType::Temp => defs::TEMP_CONFIG_NAME, + } + } +} + +/// Parse a boolean config value +/// Accepts "true", "1" (case-insensitive) as true, everything else as false +pub fn parse_bool_config(value: &str) -> bool { + let trimmed = value.trim(); + trimmed.eq_ignore_ascii_case("true") || trimmed == "1" +} + +/// Validate config key +/// Rejects keys with control characters, newlines, or excessive length +pub fn validate_config_key(key: &str) -> Result<()> { + if key.is_empty() { + bail!("Config key cannot be empty"); + } + + if key.len() > MAX_CONFIG_KEY_LEN { + bail!( + "Config key too long: {} bytes (max: {})", + key.len(), + MAX_CONFIG_KEY_LEN + ); + } + + // Check for control characters and newlines + for ch in key.chars() { + if ch.is_control() { + bail!( + "Config key contains control character: {:?} (U+{:04X})", + ch, + ch as u32 + ); + } + } + + // Reject keys with path separators to prevent path traversal + if key.contains('/') || key.contains('\\') { + bail!("Config key cannot contain path separators"); + } + + Ok(()) +} + +/// Validate config value +/// Rejects values with control characters (except tab), newlines, or excessive length +pub fn validate_config_value(value: &str) -> Result<()> { + if value.len() > MAX_CONFIG_VALUE_LEN { + bail!( + "Config value too long: {} bytes (max: {})", + value.len(), + MAX_CONFIG_VALUE_LEN + ); + } + + // Check for control characters (allow tab but reject newlines and others) + for ch in value.chars() { + if ch.is_control() && ch != '\t' { + bail!( + "Config value contains invalid control character: {:?} (U+{:04X})", + ch, + ch as u32 + ); + } + } + + Ok(()) +} + +/// Validate config count +fn validate_config_count(config: &HashMap) -> Result<()> { + if config.len() > MAX_CONFIG_COUNT { + bail!( + "Too many config entries: {} (max: {})", + config.len(), + MAX_CONFIG_COUNT + ); + } + Ok(()) +} + +/// Get the config directory path for a module +fn get_config_dir(module_id: &str) -> PathBuf { + Path::new(defs::MODULE_CONFIG_DIR).join(module_id) +} + +/// Get the config file path for a module +fn get_config_path(module_id: &str, config_type: ConfigType) -> PathBuf { + get_config_dir(module_id).join(config_type.filename()) +} + +/// Ensure the config directory exists +fn ensure_config_dir(module_id: &str) -> Result { + let dir = get_config_dir(module_id); + ensure_dir_exists(&dir)?; + Ok(dir) +} + +/// Load config from binary file +pub fn load_config(module_id: &str, config_type: ConfigType) -> Result> { + crate::module::validate_module_id(module_id)?; + + let config_path = get_config_path(module_id, config_type); + + if !config_path.exists() { + debug!("Config file not found: {:?}", config_path); + return Ok(HashMap::new()); + } + + let mut file = File::open(&config_path) + .with_context(|| format!("Failed to open config file: {:?}", config_path))?; + + // Read magic + let mut magic_buf = [0u8; 4]; + file.read_exact(&mut magic_buf) + .with_context(|| "Failed to read magic")?; + let magic = u32::from_le_bytes(magic_buf); + + if magic != MODULE_CONFIG_MAGIC { + bail!( + "Invalid config magic: expected 0x{:08x}, got 0x{:08x}", + MODULE_CONFIG_MAGIC, + magic + ); + } + + // Read version + let mut version_buf = [0u8; 4]; + file.read_exact(&mut version_buf) + .with_context(|| "Failed to read version")?; + let version = u32::from_le_bytes(version_buf); + + if version != MODULE_CONFIG_VERSION { + bail!( + "Unsupported config version: expected {}, got {}", + MODULE_CONFIG_VERSION, + version + ); + } + + // Read count + let mut count_buf = [0u8; 4]; + file.read_exact(&mut count_buf) + .with_context(|| "Failed to read count")?; + let count = u32::from_le_bytes(count_buf); + + // Read entries + let mut config = HashMap::new(); + for i in 0..count { + // Read key length + let mut key_len_buf = [0u8; 4]; + file.read_exact(&mut key_len_buf) + .with_context(|| format!("Failed to read key length for entry {}", i))?; + let key_len = u32::from_le_bytes(key_len_buf) as usize; + + // Read key data + let mut key_buf = vec![0u8; key_len]; + file.read_exact(&mut key_buf) + .with_context(|| format!("Failed to read key data for entry {}", i))?; + let key = String::from_utf8(key_buf) + .with_context(|| format!("Invalid UTF-8 in key for entry {}", i))?; + + // Read value length + let mut value_len_buf = [0u8; 4]; + file.read_exact(&mut value_len_buf) + .with_context(|| format!("Failed to read value length for entry {}", i))?; + let value_len = u32::from_le_bytes(value_len_buf) as usize; + + // Read value data + let mut value_buf = vec![0u8; value_len]; + file.read_exact(&mut value_buf) + .with_context(|| format!("Failed to read value data for entry {}", i))?; + let value = String::from_utf8(value_buf) + .with_context(|| format!("Invalid UTF-8 in value for entry {}", i))?; + + config.insert(key, value); + } + + debug!("Loaded {} entries from {:?}", config.len(), config_path); + Ok(config) +} + +/// Save config to binary file +pub fn save_config( + module_id: &str, + config_type: ConfigType, + config: &HashMap, +) -> Result<()> { + crate::module::validate_module_id(module_id)?; + + // Validate config count + validate_config_count(config)?; + + // Validate all keys and values + for (key, value) in config { + validate_config_key(key).with_context(|| format!("Invalid config key: '{}'", key))?; + validate_config_value(value) + .with_context(|| format!("Invalid config value for key '{}'", key))?; + } + + ensure_config_dir(module_id)?; + + let config_path = get_config_path(module_id, config_type); + let temp_path = config_path.with_extension("tmp"); + + // Write to temporary file first + let mut file = File::create(&temp_path) + .with_context(|| format!("Failed to create temp config file: {:?}", temp_path))?; + + // Write magic + file.write_all(&MODULE_CONFIG_MAGIC.to_le_bytes()) + .with_context(|| "Failed to write magic")?; + + // Write version + file.write_all(&MODULE_CONFIG_VERSION.to_le_bytes()) + .with_context(|| "Failed to write version")?; + + // Write count + let count = config.len() as u32; + file.write_all(&count.to_le_bytes()) + .with_context(|| "Failed to write count")?; + + // Write entries + for (key, value) in config { + // Write key length + let key_bytes = key.as_bytes(); + let key_len = key_bytes.len() as u32; + file.write_all(&key_len.to_le_bytes()) + .with_context(|| format!("Failed to write key length for '{}'", key))?; + + // Write key data + file.write_all(key_bytes) + .with_context(|| format!("Failed to write key data for '{}'", key))?; + + // Write value length + let value_bytes = value.as_bytes(); + let value_len = value_bytes.len() as u32; + file.write_all(&value_len.to_le_bytes()) + .with_context(|| format!("Failed to write value length for '{}'", key))?; + + // Write value data + file.write_all(value_bytes) + .with_context(|| format!("Failed to write value data for '{}'", key))?; + } + + file.sync_all() + .with_context(|| "Failed to sync config file")?; + + // Atomic rename + fs::rename(&temp_path, &config_path).with_context(|| { + format!( + "Failed to rename config file: {:?} -> {:?}", + temp_path, config_path + ) + })?; + + debug!("Saved {} entries to {:?}", config.len(), config_path); + Ok(()) +} + +/// Get a single config value +#[allow(dead_code)] +pub fn get_config_value( + module_id: &str, + key: &str, + config_type: ConfigType, +) -> Result> { + let config = load_config(module_id, config_type)?; + Ok(config.get(key).cloned()) +} + +/// Set a single config value +pub fn set_config_value( + module_id: &str, + key: &str, + value: &str, + config_type: ConfigType, +) -> Result<()> { + // Validate input early for better error messages + validate_config_key(key)?; + validate_config_value(value)?; + + let mut config = load_config(module_id, config_type)?; + config.insert(key.to_string(), value.to_string()); + + // Note: save_config will also validate, but this provides earlier feedback + save_config(module_id, config_type, &config)?; + Ok(()) +} + +/// Delete a single config value +pub fn delete_config_value(module_id: &str, key: &str, config_type: ConfigType) -> Result<()> { + let mut config = load_config(module_id, config_type)?; + + if config.remove(key).is_none() { + bail!("Key '{}' not found in config", key); + } + + save_config(module_id, config_type, &config)?; + Ok(()) +} + +/// Clear all config values +pub fn clear_config(module_id: &str, config_type: ConfigType) -> Result<()> { + let config_path = get_config_path(module_id, config_type); + + if config_path.exists() { + fs::remove_file(&config_path) + .with_context(|| format!("Failed to remove config file: {:?}", config_path))?; + debug!("Cleared config: {:?}", config_path); + } + + Ok(()) +} + +/// Merge persist and temp configs (temp takes priority) +pub fn merge_configs(module_id: &str) -> Result> { + crate::module::validate_module_id(module_id)?; + + let mut merged = match load_config(module_id, ConfigType::Persist) { + Ok(config) => config, + Err(e) => { + warn!( + "Failed to load persist config for module '{}': {}", + module_id, e + ); + HashMap::new() + } + }; + + let temp = match load_config(module_id, ConfigType::Temp) { + Ok(config) => config, + Err(e) => { + warn!( + "Failed to load temp config for module '{}': {}", + module_id, e + ); + HashMap::new() + } + }; + + // Temp config overrides persist config + for (key, value) in temp { + merged.insert(key, value); + } + + Ok(merged) +} + +/// Get all module configs (for iteration) +/// Loads all configs in a single pass to minimize I/O overhead +pub fn get_all_module_configs() -> Result>> { + let config_root = Path::new(defs::MODULE_CONFIG_DIR); + + if !config_root.exists() { + return Ok(HashMap::new()); + } + + let mut all_configs = HashMap::new(); + + for entry in fs::read_dir(config_root) + .with_context(|| format!("Failed to read config directory: {:?}", config_root))? + { + let entry = entry?; + let path = entry.path(); + + if !path.is_dir() { + continue; + } + + if let Some(module_id) = path.file_name().and_then(|n| n.to_str()) { + match merge_configs(module_id) { + Ok(config) => { + if !config.is_empty() { + all_configs.insert(module_id.to_string(), config); + } + } + Err(e) => { + warn!("Failed to load config for module '{}': {}", module_id, e); + // Continue processing other modules + } + } + } + } + + Ok(all_configs) +} + +/// Clear all temporary configs (called during post-fs-data) +pub fn clear_all_temp_configs() -> Result<()> { + let config_root = Path::new(defs::MODULE_CONFIG_DIR); + + if !config_root.exists() { + debug!("Config directory does not exist, nothing to clear"); + return Ok(()); + } + + let mut cleared_count = 0; + + for entry in fs::read_dir(config_root) + .with_context(|| format!("Failed to read config directory: {:?}", config_root))? + { + let entry = entry?; + let path = entry.path(); + + if !path.is_dir() { + continue; + } + + let temp_config = path.join(defs::TEMP_CONFIG_NAME); + if temp_config.exists() { + match fs::remove_file(&temp_config) { + Ok(_) => { + debug!("Cleared temp config: {:?}", temp_config); + cleared_count += 1; + } + Err(e) => { + warn!("Failed to clear temp config {:?}: {}", temp_config, e); + } + } + } + } + + if cleared_count > 0 { + debug!("Cleared {} temp config file(s)", cleared_count); + } + + Ok(()) +} + +/// Clear all configs for a module (called during uninstall) +pub fn clear_module_configs(module_id: &str) -> Result<()> { + crate::module::validate_module_id(module_id)?; + + let config_dir = get_config_dir(module_id); + + if config_dir.exists() { + fs::remove_dir_all(&config_dir) + .with_context(|| format!("Failed to remove config directory: {:?}", config_dir))?; + debug!("Cleared all configs for module: {}", module_id); + } + + Ok(()) +} diff --git a/userspace/ksud/src/profile.rs b/userspace/ksud/src/profile.rs index e2b3f35..0584f94 100644 --- a/userspace/ksud/src/profile.rs +++ b/userspace/ksud/src/profile.rs @@ -1,8 +1,9 @@ -use crate::utils::ensure_dir_exists; -use crate::{defs, sepolicy}; -use anyhow::{Context, Result}; use std::path::Path; +use anyhow::{Context, Result}; + +use crate::{defs, sepolicy, utils::ensure_dir_exists}; + pub fn set_sepolicy(pkg: String, policy: String) -> Result<()> { ensure_dir_exists(defs::PROFILE_SELINUX_DIR)?; let policy_file = Path::new(defs::PROFILE_SELINUX_DIR).join(pkg); diff --git a/userspace/ksud/src/restorecon.rs b/userspace/ksud/src/restorecon.rs index 91ccfb1..a953a65 100644 --- a/userspace/ksud/src/restorecon.rs +++ b/userspace/ksud/src/restorecon.rs @@ -1,12 +1,13 @@ -use crate::defs; -use anyhow::Result; -use jwalk::{Parallelism::Serial, WalkDir}; use std::path::Path; +use anyhow::Result; #[cfg(any(target_os = "linux", target_os = "android"))] use anyhow::{Context, Ok}; #[cfg(any(target_os = "linux", target_os = "android"))] use extattr::{Flags as XattrFlags, lsetxattr}; +use jwalk::{Parallelism::Serial, WalkDir}; + +use crate::defs; pub const SYSTEM_CON: &str = "u:object_r:system_file:s0"; pub const ADB_CON: &str = "u:object_r:adb_data_file:s0"; @@ -61,14 +62,13 @@ pub fn restore_syscon>(dir: P) -> Result<()> { Ok(()) } -fn restore_modules_con>(dir: P) -> Result<()> { +fn restore_syscon_if_unlabeled>(dir: P) -> Result<()> { for dir_entry in WalkDir::new(dir).parallelism(Serial) { - if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) { - if let Result::Ok(con) = lgetfilecon(&path) { - if con == ADB_CON || con == UNLABEL_CON || con.is_empty() { - lsetfilecon(&path, SYSTEM_CON)?; - } - } + if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) + && let Result::Ok(con) = lgetfilecon(&path) + && (con == UNLABEL_CON || con.is_empty()) + { + lsetfilecon(&path, SYSTEM_CON)?; } } Ok(()) @@ -76,6 +76,6 @@ fn restore_modules_con>(dir: P) -> Result<()> { pub fn restorecon() -> Result<()> { lsetfilecon(defs::DAEMON_PATH, ADB_CON)?; - restore_modules_con(defs::MODULE_DIR)?; + restore_syscon_if_unlabeled(defs::MODULE_DIR)?; Ok(()) } diff --git a/userspace/ksud/src/sepolicy.rs b/userspace/ksud/src/sepolicy.rs index b38ad2c..5c4d35f 100644 --- a/userspace/ksud/src/sepolicy.rs +++ b/userspace/ksud/src/sepolicy.rs @@ -1,3 +1,5 @@ +use std::{ffi, path::Path, vec}; + use anyhow::{Result, bail}; use derive_new::new; use nom::{ @@ -7,7 +9,6 @@ use nom::{ character::complete::{space0, space1}, combinator::map, }; -use std::{ffi, path::Path, vec}; type SeObject<'a> = Vec<&'a str>; @@ -19,7 +20,7 @@ fn parse_single_word(input: &str) -> IResult<&str, &str> { take_while1(is_sepolicy_char).parse(input) } -fn parse_bracket_objs<'a>(input: &'a str) -> IResult<&'a str, SeObject<'a>> { +fn parse_bracket_objs(input: &str) -> IResult<&str, SeObject<'_>> { let (input, (_, words, _)) = ( tag("{"), take_while_m_n(1, 100, |c: char| is_sepolicy_char(c) || c.is_whitespace()), @@ -29,12 +30,12 @@ fn parse_bracket_objs<'a>(input: &'a str) -> IResult<&'a str, SeObject<'a>> { Ok((input, words.split_whitespace().collect())) } -fn parse_single_obj<'a>(input: &'a str) -> IResult<&'a str, SeObject<'a>> { +fn parse_single_obj(input: &str) -> IResult<&str, SeObject<'_>> { let (input, word) = take_while1(is_sepolicy_char).parse(input)?; Ok((input, vec![word])) } -fn parse_star<'a>(input: &'a str) -> IResult<&'a str, SeObject<'a>> { +fn parse_star(input: &str) -> IResult<&str, SeObject<'_>> { let (input, _) = tag("*").parse(input)?; Ok((input, vec!["*"])) } @@ -42,12 +43,12 @@ fn parse_star<'a>(input: &'a str) -> IResult<&'a str, SeObject<'a>> { // 1. a single sepolicy word // 2. { obj1 obj2 obj3 ...} // 3. * -fn parse_seobj<'a>(input: &'a str) -> IResult<&'a str, SeObject<'a>> { +fn parse_seobj(input: &str) -> IResult<&str, SeObject<'_>> { let (input, strs) = alt((parse_single_obj, parse_bracket_objs, parse_star)).parse(input)?; Ok((input, strs)) } -fn parse_seobj_no_star<'a>(input: &'a str) -> IResult<&'a str, SeObject<'a>> { +fn parse_seobj_no_star(input: &str) -> IResult<&str, SeObject<'_>> { let (input, strs) = alt((parse_single_obj, parse_bracket_objs)).parse(input)?; Ok((input, strs)) } @@ -696,10 +697,15 @@ fn apply_one_rule<'a>(statement: &'a PolicyStatement<'a>, strict: bool) -> Resul let policies: Vec = statement.try_into()?; for policy in policies { - if !rustix::process::ksu_set_policy(&FfiPolicy::from(policy)) { - log::warn!("apply rule: {statement:?} failed."); + let ffi_policy = FfiPolicy::from(policy); + let cmd = crate::ksucalls::SetSepolicyCmd { + cmd: 0, + arg: &ffi_policy as *const _ as u64, + }; + if let Err(e) = crate::ksucalls::set_sepolicy(&cmd) { + log::warn!("apply rule {:?} failed: {}", statement, e); if strict { - return Err(anyhow::anyhow!("apply rule {:?} failed.", statement)); + return Err(anyhow::anyhow!("apply rule {:?} failed: {}", statement, e)); } } } diff --git a/userspace/ksud/src/su.rs b/userspace/ksud/src/su.rs index 7f31c68..60a0967 100644 --- a/userspace/ksud/src/su.rs +++ b/userspace/ksud/src/su.rs @@ -1,15 +1,17 @@ -use anyhow::{Ok, Result}; -use getopts::Options; -use std::env; -#[cfg(unix)] -use std::os::unix::process::CommandExt; -use std::path::PathBuf; -use std::{ffi::CStr, process::Command}; - use crate::{ defs, utils::{self, umask}, }; +use anyhow::{Context, Ok, Result, bail}; +use getopts::Options; +use libc::c_int; +use log::{error, warn}; +#[cfg(unix)] +use std::os::unix::process::CommandExt; +use std::{env, ffi::CStr, path::PathBuf, process::Command}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +use crate::ksucalls::get_wrapped_fd; #[cfg(any(target_os = "linux", target_os = "android"))] use rustix::{ @@ -19,7 +21,7 @@ use rustix::{ #[cfg(any(target_os = "linux", target_os = "android"))] pub fn grant_root(global_mnt: bool) -> Result<()> { - rustix::process::ksu_grant_root()?; + crate::ksucalls::grant_root()?; let mut command = Command::new("sh"); let command = unsafe { @@ -64,6 +66,29 @@ fn set_identity(uid: u32, gid: u32, groups: &[u32]) { } } +#[cfg(target_os = "android")] +fn wrap_tty(fd: c_int) { + let inner_fn = move || -> Result<()> { + if unsafe { libc::isatty(fd) != 1 } { + warn!("not a tty: {fd}"); + return Ok(()); + } + 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() + }); + } else { + unsafe { libc::close(new_fd) }; + Ok(()) + } + }; + + if let Err(e) = inner_fn() { + error!("wrap tty {fd}: {e:?}"); + } +} + #[cfg(not(any(target_os = "linux", target_os = "android")))] pub fn root_shell() -> Result<()> { unimplemented!() @@ -124,6 +149,7 @@ pub fn root_shell() -> Result<()> { "Specify a supplementary group. The first specified supplementary group is also used as a primary group if the option -g is not specified.", "GROUP", ); + opts.optflag("W", "no-wrapper", "don't use ksu fd wrapper"); // Replace -cn with -z, -mm with -M for supporting getopt_long let args = args @@ -167,6 +193,7 @@ pub fn root_shell() -> Result<()> { let mut is_login = matches.opt_present("l"); let preserve_env = matches.opt_present("p"); let mount_master = matches.opt_present("M"); + let use_fd_wrapper = !matches.opt_present("W"); let groups = matches .opt_strs("G") @@ -265,6 +292,13 @@ pub fn root_shell() -> Result<()> { let _ = utils::switch_mnt_ns(1); } + #[cfg(target_os = "android")] + if use_fd_wrapper { + wrap_tty(0); + wrap_tty(1); + wrap_tty(2); + } + set_identity(uid, gid, &groups); Result::Ok(()) diff --git a/userspace/ksud/src/uid_scanner.rs b/userspace/ksud/src/uid_scanner.rs index 7083d77..64be9d9 100644 --- a/userspace/ksud/src/uid_scanner.rs +++ b/userspace/ksud/src/uid_scanner.rs @@ -1,24 +1,24 @@ -use anyhow::Result; -use log::{info, warn}; use std::{ fs, io::Write, os::unix::{ - fs::{symlink, PermissionsExt}, + fs::{PermissionsExt, symlink}, process::CommandExt, }, - path::Path, process::{Command, Stdio}, }; -pub fn start_uid_scanner_daemon() -> Result<()> { - const SCANNER_PATH: &str = "/data/adb/uid_scanner"; - const LINK_DIR: &str = "/data/adb/ksu/bin"; - const LINK_PATH: &str = "/data/adb/ksu/bin/uid_scanner"; - const SERVICE_DIR: &str = "/data/adb/service.d"; - const SERVICE_PATH: &str = "/data/adb/service.d/uid_scanner.sh"; +use anyhow::Result; +use log::{info, warn}; - if !Path::new(SCANNER_PATH).exists() { +const SCANNER_PATH: &str = "/data/adb/uid_scanner"; +const LINK_DIR: &str = "/data/adb/ksu/bin"; +const LINK_PATH: &str = "/data/adb/ksu/bin/uid_scanner"; +const SERVICE_DIR: &str = "/data/adb/service.d"; +const SERVICE_PATH: &str = "/data/adb/service.d/uid_scanner.sh"; + +pub fn start_uid_scanner_daemon() -> Result<()> { + if !fs::exists(SCANNER_PATH)? { warn!("uid scanner binary not found at {}", SCANNER_PATH); return Ok(()); } @@ -31,7 +31,7 @@ pub fn start_uid_scanner_daemon() -> Result<()> { { if let Err(e) = fs::create_dir_all(LINK_DIR) { warn!("failed to create {}: {}", LINK_DIR, e); - } else if !Path::new(LINK_PATH).exists() { + } else if !fs::exists(LINK_PATH)? { match symlink(SCANNER_PATH, LINK_PATH) { Ok(_) => info!("created symlink {} -> {}", SCANNER_PATH, LINK_PATH), Err(e) => warn!("failed to create symlink: {}", e), @@ -41,13 +41,11 @@ pub fn start_uid_scanner_daemon() -> Result<()> { if let Err(e) = fs::create_dir_all(SERVICE_DIR) { warn!("failed to create {}: {}", SERVICE_DIR, e); - } else if !Path::new(SERVICE_PATH).exists() { - let content = r#"#!/system/bin/sh -# KSU uid_scanner auto-restart script -until [ -d "/sdcard/Android" ]; do sleep 1; done -sleep 10 -/data/adb/uid_scanner restart -"#; + } + + if !fs::exists(SERVICE_PATH)? { + let content = include_str!("uid_scanner.sh"); + match fs::OpenOptions::new() .write(true) .create_new(true) diff --git a/userspace/ksud/src/uid_scanner.sh b/userspace/ksud/src/uid_scanner.sh new file mode 100644 index 0000000..6c7000e --- /dev/null +++ b/userspace/ksud/src/uid_scanner.sh @@ -0,0 +1,5 @@ +#!/system/bin/sh +# KSU uid_scanner auto-restart script +until [ -d "/sdcard/Android" ]; do sleep 1; done +sleep 10 +/data/adb/uid_scanner restart \ No newline at end of file diff --git a/userspace/ksud/src/umount_manager.rs b/userspace/ksud/src/umount_manager.rs new file mode 100644 index 0000000..c671119 --- /dev/null +++ b/userspace/ksud/src/umount_manager.rs @@ -0,0 +1,264 @@ +use anyhow::{Context, Result, anyhow}; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::ksucalls::ksuctl; + +const MAGIC_NUMBER_HEADER: &[u8; 4] = b"KUMT"; +const MAGIC_VERSION: u32 = 1; +const CONFIG_FILE: &str = "/data/adb/ksu/.umount"; +const KSU_IOCTL_UMOUNT_MANAGER: u32 = 0xc0004b6b; // _IOC(_IOC_READ|_IOC_WRITE, 'K', 107, 0) + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct UmountEntry { + pub path: String, + pub flags: i32, + pub is_default: bool, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct UmountConfig { + pub entries: Vec, +} + +pub struct UmountManager { + config: UmountConfig, + config_path: PathBuf, +} + +#[repr(C)] +#[derive(Clone, Copy)] +struct UmountManagerCmd { + pub operation: u32, + pub path: [u8; 256], + pub flags: i32, + pub count: u32, + pub entries_ptr: u64, +} + +impl Default for UmountManagerCmd { + fn default() -> Self { + UmountManagerCmd { + operation: 0, + path: [0; 256], + flags: 0, + count: 0, + entries_ptr: 0, + } + } +} + +impl UmountManager { + pub fn new(config_path: Option) -> Result { + let path = config_path.unwrap_or_else(|| PathBuf::from(CONFIG_FILE)); + + let config = if path.exists() { + Self::load_config(&path)? + } else { + UmountConfig { + entries: Vec::new(), + } + }; + + Ok(UmountManager { + config, + config_path: path, + }) + } + + fn load_config(path: &Path) -> Result { + let data = fs::read(path).context("Failed to read config file")?; + + if data.len() < 8 { + return Err(anyhow!("Invalid config file: too small")); + } + + let header = &data[0..4]; + if header != MAGIC_NUMBER_HEADER { + return Err(anyhow!("Invalid config file: wrong magic number")); + } + + let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]); + if version != MAGIC_VERSION { + return Err(anyhow!("Unsupported config version: {}", version)); + } + + let json_data = &data[8..]; + let config: UmountConfig = + serde_json::from_slice(json_data).context("Failed to parse config JSON")?; + + Ok(config) + } + + pub fn save_config(&self) -> Result<()> { + let dir = self.config_path.parent().unwrap(); + fs::create_dir_all(dir).context("Failed to create config directory")?; + + let mut data = Vec::new(); + data.extend_from_slice(MAGIC_NUMBER_HEADER); + data.extend_from_slice(&MAGIC_VERSION.to_le_bytes()); + + let json = serde_json::to_vec(&self.config).context("Failed to serialize config")?; + data.extend_from_slice(&json); + + fs::write(&self.config_path, &data).context("Failed to write config file")?; + + Ok(()) + } + + pub fn add_entry(&mut self, path: &str, flags: i32) -> Result<()> { + let exists = self.config.entries.iter().any(|e| e.path == path); + if exists { + return Err(anyhow!("Entry already exists: {}", path)); + } + + let entry = UmountEntry { + path: path.to_string(), + flags, + is_default: false, + }; + + self.config.entries.push(entry); + Ok(()) + } + + pub fn remove_entry(&mut self, path: &str) -> Result<()> { + let before = self.config.entries.len(); + self.config.entries.retain(|e| e.path != path); + + if before == self.config.entries.len() { + return Err(anyhow!("Entry not found: {}", path)); + } + Ok(()) + } + + pub fn list_entries(&self) -> Vec { + self.config.entries.clone() + } + + pub fn clear_custom_entries(&mut self) -> Result<()> { + self.config.entries.retain(|e| e.is_default); + Ok(()) + } + + pub fn apply_to_kernel(&self) -> Result<()> { + for entry in &self.config.entries { + Self::kernel_add_entry(entry)?; + } + Ok(()) + } + + fn kernel_add_entry(entry: &UmountEntry) -> Result<()> { + let mut cmd = UmountManagerCmd { + operation: 0, + flags: entry.flags, + ..Default::default() + }; + + let path_bytes = entry.path.as_bytes(); + if path_bytes.len() >= cmd.path.len() { + return Err(anyhow!("Path too long: {}", entry.path)); + } + + cmd.path[..path_bytes.len()].copy_from_slice(path_bytes); + + umount_manager_ioctl(&cmd).context(format!("Failed to add entry: {}", entry.path))?; + + Ok(()) + } +} + +pub fn init_umount_manager() -> Result { + let manager = UmountManager::new(None)?; + + if !Path::new(CONFIG_FILE).exists() { + manager.save_config()?; + } + + Ok(manager) +} + +pub fn add_umount_path(path: &str, flags: i32) -> Result<()> { + let mut manager = init_umount_manager()?; + manager.add_entry(path, flags)?; + manager.save_config()?; + println!("✓ Added umount path: {}", path); + Ok(()) +} + +pub fn remove_umount_path(path: &str) -> Result<()> { + let mut manager = init_umount_manager()?; + manager.remove_entry(path)?; + manager.save_config()?; + println!("✓ Removed umount path: {}", path); + Ok(()) +} + +pub fn list_umount_paths() -> Result<()> { + let manager = init_umount_manager()?; + let entries = manager.list_entries(); + + if entries.is_empty() { + println!("No umount paths configured"); + return Ok(()); + } + + println!("{:<30} {:<8} {:<10}", "Path", "Flags", "Default"); + println!("{}", "=".repeat(60)); + + for entry in entries { + println!( + "{:<30} {:<8} {:<10}", + entry.path, + entry.flags, + if entry.is_default { "Yes" } else { "No" } + ); + } + + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn umount_manager_ioctl(cmd: &UmountManagerCmd) -> std::io::Result<()> { + let mut ioctl_cmd = *cmd; + ksuctl(KSU_IOCTL_UMOUNT_MANAGER, &mut ioctl_cmd as *mut _)?; + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn umount_manager_ioctl(_cmd: &UmountManagerCmd) -> std::io::Result<()> { + Err(std::io::Error::from_raw_os_error(libc::ENOSYS)) +} + +pub fn clear_custom_paths() -> Result<()> { + let mut manager = init_umount_manager()?; + manager.clear_custom_entries()?; + manager.save_config()?; + println!("✓ Cleared all custom paths"); + Ok(()) +} + +pub fn save_umount_config() -> Result<()> { + let manager = init_umount_manager()?; + manager.save_config()?; + println!("✓ Configuration saved to: {}", CONFIG_FILE); + Ok(()) +} + +pub fn load_and_apply_config() -> Result<()> { + let manager = init_umount_manager()?; + manager.apply_to_kernel()?; + println!("✓ Configuration applied to kernel"); + Ok(()) +} + +pub fn apply_config_to_kernel() -> Result<()> { + let manager = init_umount_manager()?; + manager.apply_to_kernel()?; + println!( + "✓ Applied {} entries to kernel", + manager.list_entries().len() + ); + Ok(()) +} diff --git a/userspace/ksud/src/utils.rs b/userspace/ksud/src/utils.rs index e31c338..5b93944 100644 --- a/userspace/ksud/src/utils.rs +++ b/userspace/ksud/src/utils.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Error, Ok, Result, bail}; use std::{ - fs::{self, File, OpenOptions, create_dir_all, remove_file, write}, + fs::{File, OpenOptions, create_dir_all, remove_file, write}, io::{ ErrorKind::{AlreadyExists, NotFound}, Write, @@ -35,7 +35,7 @@ pub fn ensure_clean_dir(dir: impl AsRef) -> Result<()> { pub fn ensure_file_exists>(file: T) -> Result<()> { match File::options().write(true).create_new(true).open(&file) { - Result::Ok(_) => Ok(()), + std::result::Result::Ok(_) => Ok(()), Err(err) => { if err.kind() == AlreadyExists && file.as_ref().is_file() { Ok(()) @@ -48,13 +48,11 @@ pub fn ensure_file_exists>(file: T) -> Result<()> { } pub fn ensure_dir_exists>(dir: T) -> Result<()> { - let result = create_dir_all(&dir).map_err(Error::from); - if dir.as_ref().is_dir() { - result - } else if result.is_ok() { - bail!("{} is not a regular directory", dir.as_ref().display()) + let result = create_dir_all(&dir); + if dir.as_ref().is_dir() && result.is_ok() { + Ok(()) } else { - result + bail!("{} is not a regular directory", dir.as_ref().display()) } } @@ -74,11 +72,11 @@ pub fn ensure_binary>( ) })?)?; - if let Err(e) = remove_file(path.as_ref()) { - if e.kind() != NotFound { - return Err(Error::from(e)) - .with_context(|| format!("failed to unlink {}", path.as_ref().display())); - } + if let Err(e) = remove_file(path.as_ref()) + && e.kind() != NotFound + { + return Err(Error::from(e)) + .with_context(|| format!("failed to unlink {}", path.as_ref().display())); } write(&path, contents)?; @@ -145,7 +143,7 @@ fn switch_cgroup(grp: &str, pid: u32) { let fp = OpenOptions::new().append(true).open(path); if let std::result::Result::Ok(mut fp) = fp { - let _ = writeln!(fp, "{pid}"); + let _ = write!(fp, "{pid}"); } } @@ -177,32 +175,6 @@ pub fn has_magisk() -> bool { which::which("magisk").is_ok() } -fn is_ok_empty(dir: &str) -> bool { - use std::result::Result::Ok; - - match fs::read_dir(dir) { - Ok(mut entries) => entries.next().is_none(), - Err(_) => false, - } -} - -pub fn get_tmp_path() -> String { - let dirs = ["/debug_ramdisk", "/patch_hw", "/oem", "/root", "/sbin"]; - - // find empty directory - for dir in dirs { - if is_ok_empty(dir) { - return dir.to_string(); - } - } - "".to_string() -} - -pub fn get_work_dir() -> String { - let tmp_path = get_tmp_path(); - format!("{}/workdir/", tmp_path) -} - #[cfg(target_os = "android")] fn link_ksud_to_bin() -> Result<()> { let ksu_bin = PathBuf::from(defs::DAEMON_PATH); @@ -215,7 +187,10 @@ fn link_ksud_to_bin() -> Result<()> { pub fn install(magiskboot: Option) -> Result<()> { ensure_dir_exists(defs::ADB_DIR)?; - std::fs::copy("/proc/self/exe", defs::DAEMON_PATH)?; + std::fs::copy( + std::env::current_exe().with_context(|| "Failed to get self exe path")?, + defs::DAEMON_PATH, + )?; restorecon::lsetfilecon(defs::DAEMON_PATH, restorecon::ADB_CON)?; // install binary assets assets::ensure_binaries(false).with_context(|| "Failed to extract assets")?; @@ -245,7 +220,7 @@ pub fn uninstall(magiskboot_path: Option) -> Result<()> { boot_patch::restore(None, magiskboot_path, true)?; println!("- Uninstall KernelSU manager.."); Command::new("pm") - .args(["uninstall", "me.weishu.kernelsu"]) + .args(["uninstall", "com.sukisu.ultra"]) .spawn()?; println!("- Rebooting in 5 seconds.."); std::thread::sleep(std::time::Duration::from_secs(5)); diff --git a/userspace/meta-overlayfs/.gitignore b/userspace/meta-overlayfs/.gitignore new file mode 100644 index 0000000..6bfc5c7 --- /dev/null +++ b/userspace/meta-overlayfs/.gitignore @@ -0,0 +1,4 @@ +/target +/out +Cargo.lock +*.log diff --git a/userspace/meta-overlayfs/Cargo.toml b/userspace/meta-overlayfs/Cargo.toml new file mode 100644 index 0000000..7160c4f --- /dev/null +++ b/userspace/meta-overlayfs/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "meta-overlayfs" +version = "1.0.0" +edition = "2024" +authors = ["KernelSU Developers"] +description = "An implementation of a metamodule using OverlayFS for KernelSU" +license = "GPL-3.0" + +[dependencies] +anyhow = "1" +log = "0.4" +env_logger = { version = "0.11", default-features = false } +hole-punch = { git = "https://github.com/tiann/hole-punch" } + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dependencies] +rustix = { git = "https://github.com/Kernel-SU/rustix.git", rev = "4a53fbc7cb7a07cabe87125cc21dbc27db316259", features = ["all-apis"] } +procfs = "0.17" + +[profile.release] +strip = true +opt-level = "z" # Minimize binary size +lto = true # Link-time optimization +codegen-units = 1 # Maximum optimization +panic = "abort" # Reduce binary size diff --git a/userspace/meta-overlayfs/README.md b/userspace/meta-overlayfs/README.md new file mode 100644 index 0000000..ace6f52 --- /dev/null +++ b/userspace/meta-overlayfs/README.md @@ -0,0 +1,58 @@ +# meta-overlayfs + +Official overlayfs mount handler for KernelSU metamodules. + +## Installation + +```bash +adb push meta-overlayfs-v1.0.0.zip /sdcard/ +adb shell su -c 'ksud module install /sdcard/meta-overlayfs-v1.0.0.zip' +adb reboot +``` + +Or install via KernelSU Manager → Modules. + +**Note**: The metamodule is now installed as a regular module to `/data/adb/modules/meta-overlay/`, with a symlink created at `/data/adb/metamodule` pointing to it. + +## How It Works + +Uses dual-directory architecture for ext4 image support: + +- **Metadata**: `/data/adb/modules/` - Contains `module.prop`, `disable`, `skip_mount` markers +- **Content**: `/data/adb/metamodule/mnt/` - Contains `system/`, `vendor/` etc. directories from ext4 images + +Scans metadata directory for enabled modules, then mounts their content directories as overlayfs layers. + +### Supported Partitions + +system, vendor, product, system_ext, odm, oem + +### Read-Write Layer + +Optional upperdir/workdir support via `/data/adb/modules/.rw/`: + +```bash +mkdir -p /data/adb/modules/.rw/system/{upperdir,workdir} +``` + +## Environment Variables + +- `MODULE_METADATA_DIR` - Metadata location (default: `/data/adb/modules/`) +- `MODULE_CONTENT_DIR` - Content location (default: `/data/adb/metamodule/mnt/`) +- `RUST_LOG` - Log level (debug, info, warn, error) + +## Architecture + +Automatically selects aarch64 or x86_64 binary during installation (~500KB). + +## Building + +```bash +./build.sh +``` + +Output: `target/meta-overlayfs-v1.0.0.zip` + +## License + +GPL-3.0 diff --git a/userspace/meta-overlayfs/build.sh b/userspace/meta-overlayfs/build.sh new file mode 100644 index 0000000..9f70e8b --- /dev/null +++ b/userspace/meta-overlayfs/build.sh @@ -0,0 +1,92 @@ +#!/bin/bash +set -e + +# Configuration +VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') +OUTPUT_DIR="target" +METAMODULE_DIR="metamodule" +MODULE_OUTPUT_DIR="$OUTPUT_DIR/module" + +echo "==========================================" +echo "Building meta-overlayfs v${VERSION}" +echo "==========================================" + +# Detect build tool +if command -v cross >/dev/null 2>&1; then + BUILD_TOOL="cross" + echo "Using cross for compilation" +else + BUILD_TOOL="cargo-ndk" + echo "Using cargo ndk for compilation" + if ! command -v cargo-ndk >/dev/null 2>&1; then + echo "Error: Neither cross nor cargo-ndk found!" + echo "Please install one of them:" + echo " - cross: cargo install cross" + echo " - cargo-ndk: cargo install cargo-ndk" + exit 1 + fi +fi + +# Clean output directory +echo "Cleaning output directory..." +rm -rf "$OUTPUT_DIR" +mkdir -p "$MODULE_OUTPUT_DIR" + +# Build for multiple architectures +echo "" +echo "Building for aarch64-linux-android..." +if [ "$BUILD_TOOL" = "cross" ]; then + cross build --release --target aarch64-linux-android +else + cargo ndk build -t arm64-v8a --release +fi + +echo "" +echo "Building for x86_64-linux-android..." +if [ "$BUILD_TOOL" = "cross" ]; then + cross build --release --target x86_64-linux-android +else + cargo ndk build -t x86_64 --release +fi + +# Copy binaries +echo "" +echo "Copying binaries..." +cp target/aarch64-linux-android/release/meta-overlayfs \ + "$MODULE_OUTPUT_DIR/meta-overlayfs-aarch64" +cp target/x86_64-linux-android/release/meta-overlayfs \ + "$MODULE_OUTPUT_DIR/meta-overlayfs-x86_64" + +# Copy metamodule files +echo "Copying metamodule files..." +cp "$METAMODULE_DIR"/module.prop "$MODULE_OUTPUT_DIR/" +cp "$METAMODULE_DIR"/*.sh "$MODULE_OUTPUT_DIR/" + +# Set permissions +echo "Setting permissions..." +chmod 755 "$MODULE_OUTPUT_DIR"/*.sh +chmod 755 "$MODULE_OUTPUT_DIR"/meta-overlayfs-* + +# Display binary sizes +echo "" +echo "Binary sizes:" +echo " aarch64: $(du -h "$MODULE_OUTPUT_DIR"/meta-overlayfs-aarch64 | awk '{print $1}')" +echo " x86_64: $(du -h "$MODULE_OUTPUT_DIR"/meta-overlayfs-x86_64 | awk '{print $1}')" + +# Package +echo "" +echo "Packaging..." +cd "$MODULE_OUTPUT_DIR" +ZIP_NAME="meta-overlayfs-v${VERSION}.zip" +zip -r "../$ZIP_NAME" . +cd ../.. + +echo "" +echo "==========================================" +echo "Build completed successfully!" +echo "Output: $OUTPUT_DIR/$ZIP_NAME" +echo "==========================================" +echo "" +echo "To install:" +echo " adb push $OUTPUT_DIR/$ZIP_NAME /sdcard/" +echo " adb shell su -c 'ksud module install /sdcard/$ZIP_NAME'" diff --git a/userspace/meta-overlayfs/metamodule/.gitkeep b/userspace/meta-overlayfs/metamodule/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/userspace/meta-overlayfs/metamodule/customize.sh b/userspace/meta-overlayfs/metamodule/customize.sh new file mode 100644 index 0000000..128a5bd --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/customize.sh @@ -0,0 +1,67 @@ +#!/system/bin/sh + +ui_print "- Detecting device architecture..." + +# Detect architecture using ro.product.cpu.abi +ABI=$(grep_get_prop ro.product.cpu.abi) +ui_print "- Detected ABI: $ABI" + +# Select the correct binary based on architecture +case "$ABI" in + arm64-v8a) + ARCH_BINARY="meta-overlayfs-aarch64" + REMOVE_BINARY="meta-overlayfs-x86_64" + ui_print "- Selected architecture: ARM64" + ;; + x86_64) + ARCH_BINARY="meta-overlayfs-x86_64" + REMOVE_BINARY="meta-overlayfs-aarch64" + ui_print "- Selected architecture: x86_64" + ;; + *) + abort "! Unsupported architecture: $ABI" + ;; +esac + +# Verify the selected binary exists +if [ ! -f "$MODPATH/$ARCH_BINARY" ]; then + abort "! Binary not found: $ARCH_BINARY" +fi + +ui_print "- Installing $ARCH_BINARY as meta-overlayfs" + +# Rename the selected binary to the generic name +mv "$MODPATH/$ARCH_BINARY" "$MODPATH/meta-overlayfs" || abort "! Failed to rename binary" + +# Remove the unused binary +rm -f "$MODPATH/$REMOVE_BINARY" + +# Ensure the binary is executable +chmod 755 "$MODPATH/meta-overlayfs" || abort "! Failed to set permissions" + +ui_print "- Architecture-specific binary installed successfully" + +# Create ext4 image for module content storage +IMG_FILE="$MODPATH/modules.img" +IMG_SIZE_MB=2048 +EXISTING_IMG="/data/adb/modules/$MODID/modules.img" + +if [ -f "$EXISTING_IMG" ]; then + ui_print "- Reusing modules image from previous install" + "$MODPATH/meta-overlayfs" xcp "$EXISTING_IMG" "$IMG_FILE" || \ + abort "! Failed to copy existing modules image" +else + ui_print "- Creating 2GB ext4 image for module storage" + + # Create sparse file (2GB logical size, 0 bytes actual) + truncate -s ${IMG_SIZE_MB}M "$IMG_FILE" || \ + abort "! Failed to create image file" + + # Format as ext4 with small journal (8MB) for safety with minimal overhead + /system/bin/mke2fs -t ext4 -J size=8 -F "$IMG_FILE" >/dev/null 2>&1 || \ + abort "! Failed to format ext4 image" + + ui_print "- Image created successfully (sparse file)" +fi + +ui_print "- Installation complete" diff --git a/userspace/meta-overlayfs/metamodule/metainstall.sh b/userspace/meta-overlayfs/metamodule/metainstall.sh new file mode 100644 index 0000000..531e85e --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/metainstall.sh @@ -0,0 +1,99 @@ +#!/system/bin/sh +############################################ +# meta-overlayfs metainstall.sh +# Module installation hook for ext4 image support +############################################ + +# Constants +IMG_FILE="/data/adb/metamodule/modules.img" +MNT_DIR="/data/adb/metamodule/mnt" + +# Ensure ext4 image is mounted +ensure_image_mounted() { + if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then + ui_print "- Mounting modules image" + mkdir -p "$MNT_DIR" + mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || { + abort "! Failed to mount modules image" + } + ui_print "- Image mounted successfully" + else + ui_print "- Image already mounted" + fi +} + +# Determine whether this module should be moved into the ext4 image. +# We only relocate payloads that expose system/ overlays and do not opt out via skip_mount. +module_requires_overlay_move() { + if [ -f "$MODPATH/skip_mount" ]; then + ui_print "- skip_mount flag detected; keeping files under /data/adb/modules" + return 1 + fi + + if [ ! -d "$MODPATH/system" ]; then + ui_print "- No system/ directory detected; keeping files under /data/adb/modules" + return 1 + fi + + return 0 +} + +# Copy SELinux contexts from src tree to destination by mirroring each entry. +copy_selinux_contexts() { + command -v chcon >/dev/null 2>&1 || return 0 + + SRC="$1" + DST="$2" + + if [ -z "$SRC" ] || [ -z "$DST" ] || [ ! -e "$SRC" ] || [ ! -e "$DST" ]; then + return 0 + fi + + chcon --reference="$SRC" "$DST" 2>/dev/null || true + + find "$SRC" -print | while IFS= read -r PATH_SRC; do + if [ "$PATH_SRC" = "$SRC" ]; then + continue + fi + REL_PATH="${PATH_SRC#"${SRC}/"}" + PATH_DST="$DST/$REL_PATH" + if [ -e "$PATH_DST" ] || [ -L "$PATH_DST" ]; then + chcon --reference="$PATH_SRC" "$PATH_DST" 2>/dev/null || true + fi + done +} + +# Post-installation: move partition directories to ext4 image +post_install_to_image() { + ui_print "- Copying module content to image" + + set_perm_recursive "$MNT_DIR" 0 0 0755 0644 + + MOD_IMG_DIR="$MNT_DIR/$MODID" + mkdir -p "$MOD_IMG_DIR" + + # Move all partition directories + for partition in system vendor product system_ext odm oem; do + if [ -d "$MODPATH/$partition" ]; then + ui_print "- Copying $partition/" + cp -af "$MODPATH/$partition" "$MOD_IMG_DIR/" || { + ui_print "! Warning: Failed to move $partition" + continue + } + copy_selinux_contexts "$MODPATH/$partition" "$MOD_IMG_DIR/$partition" + fi + done +} + +ui_print "- Using meta-overlayfs metainstall" + +install_module + +if module_requires_overlay_move; then + ensure_image_mounted + post_install_to_image +else + ui_print "- Skipping move to modules image" +fi + +ui_print "- Installation complete" diff --git a/userspace/meta-overlayfs/metamodule/metamount.sh b/userspace/meta-overlayfs/metamodule/metamount.sh new file mode 100644 index 0000000..de6a615 --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/metamount.sh @@ -0,0 +1,65 @@ +#!/system/bin/sh +# meta-overlayfs Module Mount Handler +# This script is the entry point for dual-directory module mounting + +MODDIR="${0%/*}" +IMG_FILE="$MODDIR/modules.img" +MNT_DIR="$MODDIR/mnt" + +# Log function +log() { + echo "[meta-overlayfs] $1" +} + +log "Starting module mount process" + +# Ensure ext4 image is mounted +if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then + log "Image not mounted, mounting now..." + + # Check if image file exists + if [ ! -f "$IMG_FILE" ]; then + log "ERROR: Image file not found at $IMG_FILE" + exit 1 + fi + + # Create mount point + mkdir -p "$MNT_DIR" + + # Mount the ext4 image + mount -t ext4 -o loop,rw,noatime "$IMG_FILE" "$MNT_DIR" || { + log "ERROR: Failed to mount image" + exit 1 + } + log "Image mounted successfully at $MNT_DIR" +else + log "Image already mounted at $MNT_DIR" +fi + +# Binary path (architecture-specific binary selected during installation) +BINARY="$MODDIR/meta-overlayfs" + +if [ ! -f "$BINARY" ]; then + log "ERROR: Binary not found: $BINARY" + exit 1 +fi + +# Set dual-directory environment variables +export MODULE_METADATA_DIR="/data/adb/modules" +export MODULE_CONTENT_DIR="$MNT_DIR" + +log "Metadata directory: $MODULE_METADATA_DIR" +log "Content directory: $MODULE_CONTENT_DIR" +log "Executing $BINARY" + +# Execute the mount binary +"$BINARY" +EXIT_CODE=$? + +if [ $EXIT_CODE -ne 0 ]; then + log "Mount failed with exit code $EXIT_CODE" + exit $EXIT_CODE +fi + +log "Mount completed successfully" +exit 0 diff --git a/userspace/meta-overlayfs/metamodule/metauninstall.sh b/userspace/meta-overlayfs/metamodule/metauninstall.sh new file mode 100644 index 0000000..f30df49 --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/metauninstall.sh @@ -0,0 +1,35 @@ +#!/system/bin/sh +############################################ +# mm-overlayfs metauninstall.sh +# Module uninstallation hook for ext4 image cleanup +############################################ + +# Constants +MNT_DIR="/data/adb/metamodule/mnt" + +if [ -z "$MODULE_ID" ]; then + echo "! Error: MODULE_ID not provided" + exit 1 +fi + +echo "- Cleaning up module content from image: $MODULE_ID" + +# Check if image is mounted +if ! mountpoint -q "$MNT_DIR" 2>/dev/null; then + echo "! Warning: Image not mounted, skipping cleanup" + exit 0 +fi + +# Remove module content from image +MOD_IMG_DIR="$MNT_DIR/$MODULE_ID" +if [ -d "$MOD_IMG_DIR" ]; then + echo " Removing $MOD_IMG_DIR" + rm -rf "$MOD_IMG_DIR" || { + echo "! Warning: Failed to remove module content from image" + } + echo "- Module content removed from image" +else + echo "- No module content found in image, skipping" +fi + +exit 0 diff --git a/userspace/meta-overlayfs/metamodule/module.prop b/userspace/meta-overlayfs/metamodule/module.prop new file mode 100644 index 0000000..cb56015 --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/module.prop @@ -0,0 +1,8 @@ +id=meta-overlayfs +metamodule=1 +name=OverlayFS MetaModule +version=1.0.0 +versionCode=1 +author=KernelSU Developers +description=An implementation of a metamodule using OverlayFS for KernelSU +updateJson=https://raw.githubusercontent.com/tiann/KernelSU/main/userspace/meta-overlayfs/update.json diff --git a/userspace/meta-overlayfs/metamodule/post-mount.sh b/userspace/meta-overlayfs/metamodule/post-mount.sh new file mode 100644 index 0000000..6234b6a --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/post-mount.sh @@ -0,0 +1,3 @@ +#!/system/bin/sh + +ksud kernel nuke-ext4-sysfs /data/adb/modules/meta-overlayfs/mnt diff --git a/userspace/meta-overlayfs/metamodule/uninstall.sh b/userspace/meta-overlayfs/metamodule/uninstall.sh new file mode 100644 index 0000000..90d41d4 --- /dev/null +++ b/userspace/meta-overlayfs/metamodule/uninstall.sh @@ -0,0 +1,24 @@ +#!/system/bin/sh +############################################ +# mm-overlayfs uninstall.sh +# Cleanup script for metamodule removal +############################################ + +MODDIR="${0%/*}" +MNT_DIR="$MODDIR/mnt" + +echo "- Uninstalling metamodule..." + +# Unmount the ext4 image if mounted +if mountpoint -q "$MNT_DIR" 2>/dev/null; then + echo "- Unmounting image..." + umount "$MNT_DIR" 2>/dev/null || { + echo "- Warning: Failed to unmount cleanly" + umount -l "$MNT_DIR" 2>/dev/null + } + echo "- Image unmounted" +fi + +echo "- Uninstall complete" + +exit 0 diff --git a/userspace/meta-overlayfs/src/defs.rs b/userspace/meta-overlayfs/src/defs.rs new file mode 100644 index 0000000..d54c5e6 --- /dev/null +++ b/userspace/meta-overlayfs/src/defs.rs @@ -0,0 +1,17 @@ +// Constants for KernelSU module mounting + +// Dual-directory support for ext4 image +pub const MODULE_METADATA_DIR: &str = "/data/adb/modules/"; +pub const MODULE_CONTENT_DIR: &str = "/data/adb/metamodule/mnt/"; + +// Legacy constant (for backwards compatibility) +pub const _MODULE_DIR: &str = "/data/adb/modules/"; + +// Status marker files +pub const DISABLE_FILE_NAME: &str = "disable"; +pub const _REMOVE_FILE_NAME: &str = "remove"; +pub const SKIP_MOUNT_FILE_NAME: &str = "skip_mount"; + +// System directories +pub const SYSTEM_RW_DIR: &str = "/data/adb/modules/.rw/"; +pub const KSU_OVERLAY_SOURCE: &str = "KSU"; diff --git a/userspace/meta-overlayfs/src/main.rs b/userspace/meta-overlayfs/src/main.rs new file mode 100644 index 0000000..2d764f0 --- /dev/null +++ b/userspace/meta-overlayfs/src/main.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use log::info; + +mod defs; +mod mount; +mod xcp; + +fn main() -> Result<()> { + let args: Vec = std::env::args().collect(); + if matches!(args.get(1), Some(cmd) if cmd == "xcp") { + return xcp::run(&args[2..]); + } + + // Initialize logger + env_logger::builder() + .filter_level(log::LevelFilter::Info) + .init(); + + info!("meta-overlayfs v{}", env!("CARGO_PKG_VERSION")); + + // Dual-directory support: metadata + content + let metadata_dir = std::env::var("MODULE_METADATA_DIR") + .unwrap_or_else(|_| defs::MODULE_METADATA_DIR.to_string()); + let content_dir = std::env::var("MODULE_CONTENT_DIR") + .unwrap_or_else(|_| defs::MODULE_CONTENT_DIR.to_string()); + + info!("Metadata directory: {}", metadata_dir); + info!("Content directory: {}", content_dir); + + // Execute dual-directory mounting + mount::mount_modules_systemlessly(&metadata_dir, &content_dir)?; + + info!("Mount completed successfully"); + Ok(()) +} diff --git a/userspace/meta-overlayfs/src/mount.rs b/userspace/meta-overlayfs/src/mount.rs new file mode 100644 index 0000000..07fd92f --- /dev/null +++ b/userspace/meta-overlayfs/src/mount.rs @@ -0,0 +1,376 @@ +// Overlayfs mounting implementation +// Migrated from ksud/src/mount.rs and ksud/src/init_event.rs + +use anyhow::{Context, Result, bail}; +use log::{info, warn}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +use procfs::process::Process; +#[cfg(any(target_os = "linux", target_os = "android"))] +use rustix::{fd::AsFd, fs::CWD, mount::*}; + +use crate::defs::{DISABLE_FILE_NAME, KSU_OVERLAY_SOURCE, SKIP_MOUNT_FILE_NAME, SYSTEM_RW_DIR}; + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_overlayfs( + lower_dirs: &[String], + lowest: &str, + upperdir: Option, + workdir: Option, + dest: impl AsRef, +) -> Result<()> { + let lowerdir_config = lower_dirs + .iter() + .map(|s| s.as_ref()) + .chain(std::iter::once(lowest)) + .collect::>() + .join(":"); + info!( + "mount overlayfs on {:?}, lowerdir={}, upperdir={:?}, workdir={:?}", + dest.as_ref(), + lowerdir_config, + upperdir, + workdir + ); + + let upperdir = upperdir + .filter(|up| up.exists()) + .map(|e| e.display().to_string()); + let workdir = workdir + .filter(|wd| wd.exists()) + .map(|e| e.display().to_string()); + + let result = (|| { + let fs = fsopen("overlay", FsOpenFlags::FSOPEN_CLOEXEC)?; + let fs = fs.as_fd(); + fsconfig_set_string(fs, "lowerdir", &lowerdir_config)?; + if let (Some(upperdir), Some(workdir)) = (&upperdir, &workdir) { + fsconfig_set_string(fs, "upperdir", upperdir)?; + fsconfig_set_string(fs, "workdir", workdir)?; + } + fsconfig_set_string(fs, "source", KSU_OVERLAY_SOURCE)?; + fsconfig_create(fs)?; + let mount = fsmount(fs, FsMountFlags::FSMOUNT_CLOEXEC, MountAttrFlags::empty())?; + move_mount( + mount.as_fd(), + "", + CWD, + dest.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + ) + })(); + + if let Err(e) = result { + warn!("fsopen mount failed: {e:#}, fallback to mount"); + let mut data = format!("lowerdir={lowerdir_config}"); + if let (Some(upperdir), Some(workdir)) = (upperdir, workdir) { + data = format!("{data},upperdir={upperdir},workdir={workdir}"); + } + mount( + KSU_OVERLAY_SOURCE, + dest.as_ref(), + "overlay", + MountFlags::empty(), + data, + )?; + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn bind_mount(from: impl AsRef, to: impl AsRef) -> Result<()> { + info!( + "bind mount {} -> {}", + from.as_ref().display(), + to.as_ref().display() + ); + let tree = open_tree( + CWD, + from.as_ref(), + OpenTreeFlags::OPEN_TREE_CLOEXEC + | OpenTreeFlags::OPEN_TREE_CLONE + | OpenTreeFlags::AT_RECURSIVE, + )?; + move_mount( + tree.as_fd(), + "", + CWD, + to.as_ref(), + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + )?; + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn mount_overlay_child( + mount_point: &str, + relative: &String, + module_roots: &Vec, + stock_root: &String, +) -> Result<()> { + if !module_roots + .iter() + .any(|lower| Path::new(&format!("{lower}{relative}")).exists()) + { + return bind_mount(stock_root, mount_point); + } + if !Path::new(&stock_root).is_dir() { + return Ok(()); + } + let mut lower_dirs: Vec = vec![]; + for lower in module_roots { + let lower_dir = format!("{lower}{relative}"); + let path = Path::new(&lower_dir); + if path.is_dir() { + lower_dirs.push(lower_dir); + } else if path.exists() { + // stock root has been blocked by this file + return Ok(()); + } + } + if lower_dirs.is_empty() { + return Ok(()); + } + // merge modules and stock + if let Err(e) = mount_overlayfs(&lower_dirs, stock_root, None, None, mount_point) { + warn!("failed: {e:#}, fallback to bind mount"); + bind_mount(stock_root, mount_point)?; + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_overlay( + root: &String, + module_roots: &Vec, + workdir: Option, + upperdir: Option, +) -> Result<()> { + info!("mount overlay for {root}"); + std::env::set_current_dir(root).with_context(|| format!("failed to chdir to {root}"))?; + let stock_root = "."; + + // collect child mounts before mounting the root + let mounts = Process::myself()? + .mountinfo() + .with_context(|| "get mountinfo")?; + let mut mount_seq = mounts + .0 + .iter() + .filter(|m| { + m.mount_point.starts_with(root) && !Path::new(&root).starts_with(&m.mount_point) + }) + .map(|m| m.mount_point.to_str()) + .collect::>(); + mount_seq.sort(); + mount_seq.dedup(); + + mount_overlayfs(module_roots, root, upperdir, workdir, root) + .with_context(|| "mount overlayfs for root failed")?; + for mount_point in mount_seq.iter() { + let Some(mount_point) = mount_point else { + continue; + }; + let relative = mount_point.replacen(root, "", 1); + let stock_root: String = format!("{stock_root}{relative}"); + if !Path::new(&stock_root).exists() { + continue; + } + if let Err(e) = mount_overlay_child(mount_point, &relative, module_roots, &stock_root) { + warn!("failed to mount overlay for child {mount_point}: {e:#}, revert"); + umount_dir(root).with_context(|| format!("failed to revert {root}"))?; + bail!(e); + } + } + Ok(()) +} + +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn umount_dir(src: impl AsRef) -> Result<()> { + unmount(src.as_ref(), UnmountFlags::empty()) + .with_context(|| format!("Failed to umount {}", src.as_ref().display()))?; + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_overlay( + _root: &String, + _module_roots: &Vec, + _workdir: Option, + _upperdir: Option, +) -> Result<()> { + unimplemented!("mount_overlay is only supported on Linux/Android") +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_overlayfs( + _lower_dirs: &[String], + _lowest: &str, + _upperdir: Option, + _workdir: Option, + _dest: impl AsRef, +) -> Result<()> { + unimplemented!("mount_overlayfs is only supported on Linux/Android") +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn bind_mount(_from: impl AsRef, _to: impl AsRef) -> Result<()> { + unimplemented!("bind_mount is only supported on Linux/Android") +} + +// ========== Mount coordination logic (from init_event.rs) ========== + +#[cfg(any(target_os = "linux", target_os = "android"))] +fn mount_partition(partition_name: &str, lowerdir: &Vec) -> Result<()> { + if lowerdir.is_empty() { + warn!("partition: {partition_name} lowerdir is empty"); + return Ok(()); + } + + let partition = format!("/{partition_name}"); + + // if /partition is a symlink and linked to /system/partition, then we don't need to overlay it separately + if Path::new(&partition).read_link().is_ok() { + warn!("partition: {partition} is a symlink"); + return Ok(()); + } + + let mut workdir = None; + let mut upperdir = None; + let system_rw_dir = Path::new(SYSTEM_RW_DIR); + if system_rw_dir.exists() { + workdir = Some(system_rw_dir.join(partition_name).join("workdir")); + upperdir = Some(system_rw_dir.join(partition_name).join("upperdir")); + } + + mount_overlay(&partition, lowerdir, workdir, upperdir) +} + +/// Collect enabled module IDs from metadata directory +/// +/// Reads module list and status from metadata directory, returns enabled module IDs +#[cfg(any(target_os = "linux", target_os = "android"))] +fn collect_enabled_modules(metadata_dir: &str) -> Result> { + let dir = std::fs::read_dir(metadata_dir) + .with_context(|| format!("Failed to read metadata directory: {}", metadata_dir))?; + + let mut enabled = Vec::new(); + + for entry in dir.flatten() { + let path = entry.path(); + if !path.is_dir() { + continue; + } + + let module_id = match entry.file_name().to_str() { + Some(id) => id.to_string(), + None => continue, + }; + + // Check status markers + if path.join(DISABLE_FILE_NAME).exists() { + info!("Module {} is disabled, skipping", module_id); + continue; + } + + if path.join(SKIP_MOUNT_FILE_NAME).exists() { + info!("Module {} has skip_mount, skipping", module_id); + continue; + } + + // Optional: verify module.prop exists + if !path.join("module.prop").exists() { + warn!("Module {} has no module.prop, skipping", module_id); + continue; + } + + info!("Module {} enabled", module_id); + enabled.push(module_id); + } + + Ok(enabled) +} + +/// Dual-directory version of mount_modules_systemlessly +/// +/// Parameters: +/// - metadata_dir: Metadata directory, stores module.prop, disable, skip_mount, etc. +/// - content_dir: Content directory, stores system/, vendor/ and other partition content (ext4 image mount point) +#[cfg(any(target_os = "linux", target_os = "android"))] +pub fn mount_modules_systemlessly(metadata_dir: &str, content_dir: &str) -> Result<()> { + info!("Scanning modules (dual-directory mode)"); + info!(" Metadata: {}", metadata_dir); + info!(" Content: {}", content_dir); + + // 1. Traverse metadata directory, collect enabled module IDs + let enabled_modules = collect_enabled_modules(metadata_dir)?; + + if enabled_modules.is_empty() { + info!("No enabled modules found"); + return Ok(()); + } + + info!("Found {} enabled module(s)", enabled_modules.len()); + + // 2. Initialize partition lowerdir lists + let partition = vec!["vendor", "product", "system_ext", "odm", "oem"]; + let mut system_lowerdir: Vec = Vec::new(); + let mut partition_lowerdir: HashMap> = HashMap::new(); + + for part in &partition { + partition_lowerdir.insert((*part).to_string(), Vec::new()); + } + + // 3. Read module content from content directory + for module_id in &enabled_modules { + let module_content_path = Path::new(content_dir).join(module_id); + + if !module_content_path.exists() { + warn!("Module {} has no content directory, skipping", module_id); + continue; + } + + info!("Processing module: {}", module_id); + + // Collect system partition + let system_path = module_content_path.join("system"); + if system_path.is_dir() { + system_lowerdir.push(system_path.display().to_string()); + info!(" + system/"); + } + + // Collect other partitions + for part in &partition { + let part_path = module_content_path.join(part); + if part_path.is_dir() + && let Some(v) = partition_lowerdir.get_mut(*part) + { + v.push(part_path.display().to_string()); + info!(" + {}/", part); + } + } + } + + // 4. Mount partitions + info!("Mounting partitions..."); + + if let Err(e) = mount_partition("system", &system_lowerdir) { + warn!("mount system failed: {e:#}"); + } + + for (k, v) in partition_lowerdir { + if let Err(e) = mount_partition(&k, &v) { + warn!("mount {k} failed: {e:#}"); + } + } + + info!("All partitions processed"); + Ok(()) +} + +#[cfg(not(any(target_os = "linux", target_os = "android")))] +pub fn mount_modules_systemlessly(_metadata_dir: &str, _content_dir: &str) -> Result<()> { + unimplemented!("mount_modules_systemlessly is only supported on Linux/Android") +} diff --git a/userspace/meta-overlayfs/src/xcp.rs b/userspace/meta-overlayfs/src/xcp.rs new file mode 100644 index 0000000..7593b77 --- /dev/null +++ b/userspace/meta-overlayfs/src/xcp.rs @@ -0,0 +1,90 @@ +use anyhow::{bail, Context, Result}; +use hole_punch::*; +use std::{ + fs::{File, OpenOptions}, + io::{Read, Seek, SeekFrom, Write}, + path::Path, +}; + +/// Handle the `xcp` command: copy sparse file with optional hole punching. +pub fn run(args: &[String]) -> Result<()> { + let mut positional: Vec<&str> = Vec::with_capacity(2); + let mut punch_hole = false; + + for arg in args { + match arg.as_str() { + "--punch-hole" => punch_hole = true, + "-h" | "--help" => { + print_usage(); + return Ok(()); + } + _ => positional.push(arg), + } + } + + if positional.len() < 2 { + print_usage(); + bail!("xcp requires source and destination paths"); + } + if positional.len() > 2 { + bail!("unexpected argument: {}", positional[2]); + } + + copy_sparse_file(positional[0], positional[1], punch_hole) +} + +fn print_usage() { + eprintln!("Usage: meta-overlayfs xcp [--punch-hole]"); +} + +// TODO: use libxcp to improve the speed if cross's MSRV is 1.70 +pub fn copy_sparse_file, Q: AsRef>( + src: P, + dst: Q, + punch_hole: bool, +) -> Result<()> { + let mut src_file = File::open(src.as_ref()) + .with_context(|| format!("failed to open {}", src.as_ref().display()))?; + let mut dst_file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(dst.as_ref()) + .with_context(|| format!("failed to open {}", dst.as_ref().display()))?; + + dst_file.set_len(src_file.metadata()?.len())?; + + let segments = src_file.scan_chunks()?; + for segment in segments { + if let SegmentType::Data = segment.segment_type { + let start = segment.start; + let end = segment.end + 1; + + src_file.seek(SeekFrom::Start(start))?; + dst_file.seek(SeekFrom::Start(start))?; + + let mut buffer = [0; 4096]; + let mut total_bytes_copied = 0; + + while total_bytes_copied < end - start { + let bytes_to_read = + std::cmp::min(buffer.len() as u64, end - start - total_bytes_copied); + let bytes_read = src_file.read(&mut buffer[..bytes_to_read as usize])?; + + if bytes_read == 0 { + break; + } + + if punch_hole && buffer[..bytes_read].iter().all(|&x| x == 0) { + dst_file.seek(SeekFrom::Current(bytes_read as i64))?; + total_bytes_copied += bytes_read as u64; + continue; + } + dst_file.write_all(&buffer[..bytes_read])?; + total_bytes_copied += bytes_read as u64; + } + } + } + + Ok(()) +} diff --git a/userspace/su/jni/su.c b/userspace/su/jni/su.c index 206542b..75b01f8 100644 --- a/userspace/su/jni/su.c +++ b/userspace/su/jni/su.c @@ -1,11 +1,54 @@ +#include +#include #include -#include #include +#include +#include +#include +#include +#include +#include -// This is a simple example. If you want a full-featured "su", please use "/data/adb/ksud debug su". -int main(){ - int32_t result = 0; - prctl(0xdeadbeef, 0, 0, 0, &result); - system("/system/bin/sh"); - return 0; +#define KERNEL_SU_OPTION 0xDEADBEEF +#define CMD_GRANT_ROOT 0 +#define CMD_ENABLE_SU 15 + +int main(int argc, char **argv, char **envp) { + unsigned long result = 0; + + if (argc >= 2 && strcmp(argv[1], "--disable-sucompat") == 0) { + prctl(KERNEL_SU_OPTION, CMD_ENABLE_SU, 0L, 0L, (unsigned long)&result); + return 0; + } + + prctl(KERNEL_SU_OPTION, CMD_GRANT_ROOT, 0L, 0L, (unsigned long)&result); + if (result != KERNEL_SU_OPTION) { + const char *error = "Access Denied: sucompat not permitted\n"; + write(STDERR_FILENO, error, strlen(error)); + return 1; + } + + struct termios term; + if (ioctl(STDIN_FILENO, TCGETS, &term) == 0) { + char tty_path[PATH_MAX]; + ssize_t len = readlink("/proc/self/fd/0", tty_path, sizeof(tty_path) - 1); + if (len > 0) { + tty_path[len] = '\0'; + const char *selinux_ctx = "u:object_r:devpts:s0"; + setxattr(tty_path, "security.selinux", selinux_ctx, strlen(selinux_ctx) + 1, 0); + } + } + + const char *default_args[] = { "/system/bin/su", NULL }; + if (argc < 1 || !argv) { + argv = (char **)default_args; + } else { + argv[0] = "/system/bin/su"; + } + + execve("/data/adb/ksud", argv, envp); + + const char *error = "Error: Failed to execve /data/adb/ksud\n"; + write(STDERR_FILENO, error, strlen(error)); + return 1; } diff --git a/userspace/susfs/.gitignore b/userspace/susfs/.gitignore deleted file mode 100644 index 720289c..0000000 --- a/userspace/susfs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/obj -/libs diff --git a/userspace/susfs/jni/Android.mk b/userspace/susfs/jni/Android.mk deleted file mode 100644 index 1eeffee..0000000 --- a/userspace/susfs/jni/Android.mk +++ /dev/null @@ -1,6 +0,0 @@ -LOCAL_PATH := $(call my-dir) - -include $(CLEAR_VARS) -LOCAL_MODULE := zakozakozako -LOCAL_SRC_FILES := susfs.c -include $(BUILD_EXECUTABLE) diff --git a/userspace/susfs/jni/Application.mk b/userspace/susfs/jni/Application.mk deleted file mode 100644 index 61d3123..0000000 --- a/userspace/susfs/jni/Application.mk +++ /dev/null @@ -1,3 +0,0 @@ -APP_ABI := arm64-v8a -APP_PLATFORM := android-24 -APP_STL := none diff --git a/userspace/susfs/jni/susfs.c b/userspace/susfs/jni/susfs.c deleted file mode 100644 index b8e3fa8..0000000 --- a/userspace/susfs/jni/susfs.c +++ /dev/null @@ -1,137 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include - -#define KERNEL_SU_OPTION 0xDEADBEEF - -// Command definitions -#define CMD_SUSFS_SHOW_VERSION 0x555e1 -#define CMD_SUSFS_SHOW_ENABLED_FEATURES 0x555e2 -#define CMD_SUSFS_SHOW_VARIANT 0x555e3 -#define CMD_SUSFS_SHOW_SUS_SU_WORKING_MODE 0x555e4 -#define CMD_SUSFS_IS_SUS_SU_READY 0x555f0 -#define CMD_SUSFS_SUS_SU 0x60000 - -// SUS_SU modes -#define SUS_SU_DISABLED 0 -#define SUS_SU_WITH_HOOKS 2 - -struct st_sus_su { - int mode; -}; - -// Function prototypes -int enable_sus_su(int last_working_mode, int target_working_mode); -int get_sus_su_working_mode(int* mode); - -int main(int argc, char* argv[]) { - if (argc < 2) { - fprintf(stderr, "Usage: %s >\n", argv[0]); - return 1; - } - - int error = -1; - - if (strcmp(argv[1], "version") == 0) { - char version[16]; - prctl(KERNEL_SU_OPTION, CMD_SUSFS_SHOW_VERSION, version, NULL, &error); - printf("%s\n", error ? "Invalid" : version); - } else if (strcmp(argv[1], "variant") == 0) { - char variant[16]; - prctl(KERNEL_SU_OPTION, CMD_SUSFS_SHOW_VARIANT, variant, NULL, &error); - printf("%s\n", error ? "Invalid" : variant); - } else if (strcmp(argv[1], "features") == 0) { - char *enabled_features; - size_t bufsize = getpagesize() * 2; - enabled_features = (char *)malloc(bufsize); - if (!enabled_features) { - perror("malloc"); - return -ENOMEM; - } - prctl(KERNEL_SU_OPTION, CMD_SUSFS_SHOW_ENABLED_FEATURES, enabled_features, bufsize, &error); - if (!error) { - printf("%s", enabled_features); - } else { - printf("Invalid\n"); - } - free(enabled_features); - } else if (strcmp(argv[1], "support") == 0) { - char *enabled_features; - size_t bufsize = getpagesize() * 2; - enabled_features = (char *)malloc(bufsize); - if (!enabled_features) { - perror("malloc"); - return -ENOMEM; - } - prctl(KERNEL_SU_OPTION, CMD_SUSFS_SHOW_ENABLED_FEATURES, enabled_features, bufsize, &error); - printf("%s\n", error || !strlen(enabled_features) ? "Unsupported" : "Supported"); - free(enabled_features); - } else if (argc == 3 && strcmp(argv[1], "sus_su") == 0) { - int last_working_mode, target_working_mode; - char* endptr; - - if (get_sus_su_working_mode(&last_working_mode)) { - return 1; - } - - if (strcmp(argv[2], "mode") == 0) { - printf("%d\n", last_working_mode); - return 0; - } - - target_working_mode = strtol(argv[2], &endptr, 10); - if (*endptr != '\0') { - fprintf(stderr, "Invalid argument: %s\n", argv[2]); - return 1; - } - - if (target_working_mode == SUS_SU_WITH_HOOKS) { - bool is_sus_su_ready; - prctl(KERNEL_SU_OPTION, CMD_SUSFS_IS_SUS_SU_READY, &is_sus_su_ready, NULL, &error); - if (error || !is_sus_su_ready) { - printf("[-] sus_su mode %d must be run during or after service stage\n", SUS_SU_WITH_HOOKS); - return 1; - } - if (last_working_mode == SUS_SU_WITH_HOOKS) { - printf("[-] sus_su is already in mode %d\n", last_working_mode); - return 1; - } - enable_sus_su(last_working_mode, SUS_SU_WITH_HOOKS); - } else if (target_working_mode == SUS_SU_DISABLED) { - if (last_working_mode == SUS_SU_DISABLED) { - printf("[-] sus_su is already in mode %d\n", last_working_mode); - return 1; - } - enable_sus_su(last_working_mode, SUS_SU_DISABLED); - } else { - fprintf(stderr, "Invalid mode: %d\n", target_working_mode); - return 1; - } - } else { - fprintf(stderr, "Invalid argument: %s\n", argv[1]); - return 1; - } - - return 0; -} - -// Helper functions -int enable_sus_su(int last_working_mode, int target_working_mode) { - struct st_sus_su info = {target_working_mode}; - int error = -1; - prctl(KERNEL_SU_OPTION, CMD_SUSFS_SUS_SU, &info, NULL, &error); - if (!error) { - printf("[+] sus_su mode %d is enabled\n", target_working_mode); - } - return error; -} - -int get_sus_su_working_mode(int* mode) { - int error = -1; - prctl(KERNEL_SU_OPTION, CMD_SUSFS_SHOW_SUS_SU_WORKING_MODE, mode, NULL, &error); - return error; -} \ No newline at end of file