diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..95ff27aa3a42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.bat eol=crlf \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index b511e9106ca3..000000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behaviour: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See the error - -**Expected behaviour** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000000..11a1f961281b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,72 @@ +name: Bug report +description: Create a report to help us improve KernelSU +labels: [Bug] + +body: + - type: checkboxes + attributes: + label: Please check before submitting an issue + options: + - label: I have searched the issues and haven't found anything relevant + required: true + + - label: I will upload bugreport file in KernelSU Manager - Settings - Report log + required: true + + - label: I know how to reproduce the issue which may not be specific to my device + required: false + + + - type: textarea + attributes: + label: Describe the bug + description: A clear and concise description of what the bug is + validations: + required: true + + + - type: textarea + attributes: + label: To Reproduce + description: Steps to reproduce the behaviour + placeholder: | + - 1. Go to '...' + - 2. Click on '....' + - 3. Scroll down to '....' + - 4. See error + + + - type: textarea + attributes: + label: Expected behavior + description: A clear and concise description of what you expected to happen. + + + - type: textarea + attributes: + label: Screenshots + description: If applicable, add screenshots to help explain your problem. + + + - type: textarea + attributes: + label: Logs + description: If applicable, add crash or any other logs to help us figure out the problem. + + + - type: textarea + attributes: + label: Device info + value: | + - Device: + - OS Version: + - KernelSU Version: + - Kernel Version: + validations: + required: true + + + - type: textarea + attributes: + label: Additional context + description: Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..d4f07b614a0e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Feature Request + url: https://github.com/tiann/KernelSU/issues/1705 + about: "We do not accept external Feature Requests, see this link for more details." diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml deleted file mode 100644 index ec69d8d4c277..000000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Feature Request -description: "Suggest an idea for this project" -title: "[Feature]" -labels: "feature" -assignees: tiann -body: - - type: markdown - id: feature-info - attributes: - value: "## Feature Infomation" - - type: textarea - id: feature-main - validations: - required: true - attributes: - label: "Is your feature request related to a problem? Please describe." - description: "A clear and concise description of what the problem is." - placeholder: "I'm always frustrated when [...]" - - type: textarea - id: feature-solution - validations: - required: true - attributes: - label: "Describe the solution you'd like." - description: "A clear and concise description of what you want to happen." - - type: textarea - id: feature-describe - validations: - required: true - attributes: - label: "Describe alternatives you've considered." - description: "A clear and concise description of any alternative solutions or features you've considered." - - type: textarea - id: feature-extra - validations: - required: false - attributes: - label: "Additional context" - description: "Add any other context or screenshots about the feature request here." - diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..95e3839ef7ad --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,38 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + groups: + actions: + patterns: + - "*" + - package-ecosystem: cargo + directory: userspace/ksud + schedule: + interval: daily + allow: + - dependency-type: "all" + groups: + crates: + patterns: + - "*" + - package-ecosystem: gradle + directory: manager + schedule: + interval: daily + groups: + maven: + patterns: + - "*" + - package-ecosystem: npm + directory: website + schedule: + interval: daily + allow: + - dependency-type: "all" + groups: + npm: + patterns: + - "*" diff --git a/.github/manifests/android-14-avd_x86_64.xml b/.github/manifests/android-14-avd_x86_64.xml new file mode 100644 index 000000000000..db2a6c06cc2e --- /dev/null +++ b/.github/manifests/android-14-avd_x86_64.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/manifests/android-15-avd_aarch64.xml b/.github/manifests/android-15-avd_aarch64.xml new file mode 100644 index 000000000000..e3a5e086e202 --- /dev/null +++ b/.github/manifests/android-15-avd_aarch64.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/manifests/android-15-avd_x86_64.xml b/.github/manifests/android-15-avd_x86_64.xml new file mode 100644 index 000000000000..0504d8a546b6 --- /dev/null +++ b/.github/manifests/android-15-avd_x86_64.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.github/patches/5.10/0001-Makefile-Use-CCACHE-for-faster-compilation.patch b/.github/patches/5.10/0001-Makefile-Use-CCACHE-for-faster-compilation.patch deleted file mode 100644 index a5e3d99432d3..000000000000 --- a/.github/patches/5.10/0001-Makefile-Use-CCACHE-for-faster-compilation.patch +++ /dev/null @@ -1,48 +0,0 @@ -From f1e398602b989ac197cdd0fda4a7c4c323b03eb9 Mon Sep 17 00:00:00 2001 -From: DozNaka -Date: Mon, 11 Apr 2022 20:43:45 -0400 -Subject: [PATCH] Makefile: Use CCACHE for faster compilation - ---- - Makefile | 20 ++++++++++---------- - 1 file changed, 10 insertions(+), 10 deletions(-) - -diff --git a/Makefile b/Makefile -index e8b8d5894..51e8aac6e 100644 ---- a/Makefile -+++ b/Makefile -@@ -442,21 +442,21 @@ KBUILD_HOSTLDLIBS := $(HOST_LFS_LIBS) $(HOSTLDLIBS) - # Make variables (CC, etc...) - CPP = $(CC) -E - ifneq ($(LLVM),) --CC = clang --LD = ld.lld --AR = llvm-ar -+CC = $(CCACHE) clang -+LD = $(CCACHE) ld.lld -+AR = $(CCACHE) llvm-ar - NM = llvm-nm --OBJCOPY = llvm-objcopy --OBJDUMP = llvm-objdump -+OBJCOPY = $(CCACHE) llvm-objcopy -+OBJDUMP = $(CCACHE) llvm-objdump - READELF = llvm-readelf - STRIP = llvm-strip - else --CC = $(CROSS_COMPILE)gcc --LD = $(CROSS_COMPILE)ld --AR = $(CROSS_COMPILE)ar -+CC = $(CCACHE) $(CROSS_COMPILE)gcc -+LD = $(CCACHE) $(CROSS_COMPILE)ld -+AR = $(CCACHE) $(CROSS_COMPILE)ar - NM = $(CROSS_COMPILE)nm --OBJCOPY = $(CROSS_COMPILE)objcopy --OBJDUMP = $(CROSS_COMPILE)objdump -+OBJCOPY = $(CCACHE) $(CROSS_COMPILE)objcopy -+OBJDUMP = $(CCACHE) $(CROSS_COMPILE)objdump - READELF = $(CROSS_COMPILE)readelf - STRIP = $(CROSS_COMPILE)strip - endif --- -2.37.2 - diff --git a/.github/patches/5.15/0001-Makefile-Use-CCACHE-for-faster-compilation.patch b/.github/patches/5.15/0001-Makefile-Use-CCACHE-for-faster-compilation.patch deleted file mode 100644 index a5e3d99432d3..000000000000 --- a/.github/patches/5.15/0001-Makefile-Use-CCACHE-for-faster-compilation.patch +++ /dev/null @@ -1,48 +0,0 @@ -From f1e398602b989ac197cdd0fda4a7c4c323b03eb9 Mon Sep 17 00:00:00 2001 -From: DozNaka -Date: Mon, 11 Apr 2022 20:43:45 -0400 -Subject: [PATCH] Makefile: Use CCACHE for faster compilation - ---- - Makefile | 20 ++++++++++---------- - 1 file changed, 10 insertions(+), 10 deletions(-) - -diff --git a/Makefile b/Makefile -index e8b8d5894..51e8aac6e 100644 ---- a/Makefile -+++ b/Makefile -@@ -442,21 +442,21 @@ KBUILD_HOSTLDLIBS := $(HOST_LFS_LIBS) $(HOSTLDLIBS) - # Make variables (CC, etc...) - CPP = $(CC) -E - ifneq ($(LLVM),) --CC = clang --LD = ld.lld --AR = llvm-ar -+CC = $(CCACHE) clang -+LD = $(CCACHE) ld.lld -+AR = $(CCACHE) llvm-ar - NM = llvm-nm --OBJCOPY = llvm-objcopy --OBJDUMP = llvm-objdump -+OBJCOPY = $(CCACHE) llvm-objcopy -+OBJDUMP = $(CCACHE) llvm-objdump - READELF = llvm-readelf - STRIP = llvm-strip - else --CC = $(CROSS_COMPILE)gcc --LD = $(CROSS_COMPILE)ld --AR = $(CROSS_COMPILE)ar -+CC = $(CCACHE) $(CROSS_COMPILE)gcc -+LD = $(CCACHE) $(CROSS_COMPILE)ld -+AR = $(CCACHE) $(CROSS_COMPILE)ar - NM = $(CROSS_COMPILE)nm --OBJCOPY = $(CROSS_COMPILE)objcopy --OBJDUMP = $(CROSS_COMPILE)objdump -+OBJCOPY = $(CCACHE) $(CROSS_COMPILE)objcopy -+OBJDUMP = $(CCACHE) $(CROSS_COMPILE)objdump - READELF = $(CROSS_COMPILE)readelf - STRIP = $(CROSS_COMPILE)strip - endif --- -2.37.2 - diff --git a/.github/scripts/build_a12.sh b/.github/scripts/build_a12.sh index 83ed1b12b42d..d6abf750cfee 100644 --- a/.github/scripts/build_a12.sh +++ b/.github/scripts/build_a12.sh @@ -51,7 +51,7 @@ build_from_image() { echo "[+] Images to upload" find . -type f -name "*.gz" - find . -type f -name "*.gz" -exec python3 "$GITHUB_WORKSPACE"/KernelSU/scripts/ksubot.py {} + + # find . -type f -name "*.gz" -exec python3 "$GITHUB_WORKSPACE"/KernelSU/scripts/ksubot.py {} + } for dir in Image*; do diff --git a/.github/scripts/build_a13.sh b/.github/scripts/build_a13.sh index d76e045a4374..929e1be077e2 100644 --- a/.github/scripts/build_a13.sh +++ b/.github/scripts/build_a13.sh @@ -30,7 +30,7 @@ build_from_image() { echo '[+] Images to upload' find . -type f -name "*.gz" - find . -type f -name "*.gz" -exec python3 "$GITHUB_WORKSPACE"/KernelSU/scripts/ksubot.py {} + + # find . -type f -name "*.gz" -exec python3 "$GITHUB_WORKSPACE"/KernelSU/scripts/ksubot.py {} + } for dir in Image*; do diff --git a/.github/workflows/add-device.yml b/.github/workflows/add-device.yml index 3d30dd446ea4..c98c2ac4f9b9 100644 --- a/.github/workflows/add-device.yml +++ b/.github/workflows/add-device.yml @@ -26,7 +26,7 @@ jobs: - name: Make pull request if: steps.handle-add-device.outputs.success == 'true' id: cpr - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v7 with: token: ${{ secrets.GITHUB_TOKEN }} commit-message: "[add device]: ${{ steps.handle-add-device.outputs.device }}" @@ -37,6 +37,7 @@ jobs: branch: "add-device-${{ github.event.issue.number }}" labels: add-device delete-branch: true + sign-commits: true - name: Check outputs if: ${{ steps.cpr.outputs.pull-request-number }} run: | @@ -53,7 +54,7 @@ jobs: message: "Cannot create pull request. Please check the issue content. Or you can create a pull request manually." GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: close issue - uses: peter-evans/close-issue@v1 + uses: peter-evans/close-issue@v3 with: issue-number: ${{ github.event.issue.number }} token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/avd-kernel.yml b/.github/workflows/avd-kernel.yml new file mode 100644 index 000000000000..a8ef728885bd --- /dev/null +++ b/.github/workflows/avd-kernel.yml @@ -0,0 +1,137 @@ +name: GKI Kernel Build + +on: + workflow_call: + inputs: + version_name: + required: true + type: string + description: > + With SUBLEVEL of kernel, + for example: android12-5.10.66 + arch: + required: true + type: string + description: > + Build arch: aarch64/x86_64 + debug: + required: false + type: boolean + default: true + manifest_name: + required: false + type: string + description: > + Local repo manifest xml path, + typically for AVD kernel build. + secrets: + BOOT_SIGN_KEY: + required: false + CHAT_ID: + required: false + BOT_TOKEN: + required: false + MESSAGE_THREAD_ID: + required: false + +jobs: + build: + name: Build ${{ inputs.version_name }} + runs-on: ubuntu-22.04 + steps: + - name: Maximize build space + uses: easimon/maximize-build-space@master + with: + root-reserve-mb: 8192 + temp-reserve-mb: 2048 + remove-dotnet: 'true' + remove-android: 'true' + remove-haskell: 'true' + remove-codeql: 'true' + + - uses: actions/checkout@v4 + with: + path: KernelSU + fetch-depth: 0 + + - name: Setup need_upload + id: need_upload + run: | + if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then + echo "UPLOAD=true" >> $GITHUB_OUTPUT + else + echo "UPLOAD=false" >> $GITHUB_OUTPUT + fi + + - name: Setup kernel source + run: | + echo "Free space:" + df -h + cd $GITHUB_WORKSPACE + sudo apt-get install repo -y + mkdir android-kernel && cd android-kernel + repo init --depth=1 -u https://android.googlesource.com/kernel/manifest -m "$GITHUB_WORKSPACE/KernelSU/.github/manifests/${{ inputs.manifest_name }}" --repo-rev=v2.16 + repo --version + repo --trace sync -c -j$(nproc --all) --no-tags + df -h + + - name: Setup KernelSU + env: + PATCH_PATH: ${{ inputs.patch_path }} + IS_DEBUG_KERNEL: ${{ inputs.debug }} + run: | + cd $GITHUB_WORKSPACE/android-kernel + echo "[+] KernelSU setup" + GKI_ROOT=$(pwd) + echo "[+] GKI_ROOT: $GKI_ROOT" + echo "[+] Copy KernelSU driver to $GKI_ROOT/common/drivers" + ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $GKI_ROOT/common/drivers/kernelsu + echo "[+] Add KernelSU driver to Makefile" + DRIVER_MAKEFILE=$GKI_ROOT/common/drivers/Makefile + DRIVER_KCONFIG=$GKI_ROOT/common/drivers/Kconfig + grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" + grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" + echo "[+] Apply KernelSU patches" + cd $GKI_ROOT/common/ && git apply $GITHUB_WORKSPACE/KernelSU/.github/patches/$PATCH_PATH/*.patch || echo "[-] No patch found" + + if [ "$IS_DEBUG_KERNEL" = "true" ]; then + echo "[+] Enable debug features for kernel" + printf "\nccflags-y += -DCONFIG_KSU_DEBUG\n" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile + fi + repo status + echo "[+] KernelSU setup done." + cd $GITHUB_WORKSPACE/KernelSU + VERSION=$(($(git rev-list --count HEAD) + 10200)) + echo "VERSION: $VERSION" + echo "kernelsu_version=$VERSION" >> $GITHUB_ENV + + - name: Make working directory clean to avoid dirty + working-directory: android-kernel + run: | + rm common/android/abi_gki_protected_exports_* || echo "No protected exports!" + git config --global user.email "bot@kernelsu.org" + git config --global user.name "KernelSUBot" + cd common/ && git add -A && git commit -a -m "Add KernelSU" + repo status + + - name: Build kernel + working-directory: android-kernel + run: | + if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then + export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }} + export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }} + fi + tools/bazel run --config=fast --config=stamp --lto=thin //common-modules/virtual-device:virtual_device_${{ inputs.arch }}_dist -- --dist_dir=dist + NAME=kernel-${{ inputs.arch }}-avd-${{ inputs.version_name }}-${{ env.kernelsu_version }} + TARGET_IMAGE=dist/bzImage + if [ ! -e $TARGET_IMAGE ]; then + TARGET_IMAGE=dist/Image + fi + mv $TARGET_IMAGE $NAME + echo "file_path=android-kernel/$NAME" >> $GITHUB_ENV + + - name: Upload Kernel + uses: actions/upload-artifact@v4 + with: + name: kernel-${{ inputs.arch }}-avd-${{ inputs.version_name }}-${{ env.kernelsu_version }} + path: "${{ env.file_path }}" diff --git a/.github/workflows/build-debug-kernel.yml b/.github/workflows/build-debug-kernel.yml index 5e0630e7c137..fd9b3d9f31fb 100644 --- a/.github/workflows/build-debug-kernel.yml +++ b/.github/workflows/build-debug-kernel.yml @@ -7,9 +7,9 @@ jobs: uses: ./.github/workflows/gki-kernel.yml with: version: android12-5.10 - version_name: android12-5.10.185 - tag: android12-5.10-2023-09 - os_patch_level: 2023-09 + version_name: android12-5.10.226 + tag: android12-5.10-2024-11 + os_patch_level: 2024-11 patch_path: "5.10" debug: true build-debug-kernel-a13: @@ -17,11 +17,11 @@ jobs: matrix: include: - version: "5.10" - sub_level: 187 - os_patch_level: 2023-08 + sub_level: 223 + os_patch_level: 2024-11 - version: "5.15" - sub_level: 119 - os_patch_level: 2023-09 + sub_level: 167 + os_patch_level: 2024-11 uses: ./.github/workflows/gki-kernel.yml with: version: android13-${{ matrix.version }} @@ -29,3 +29,34 @@ jobs: tag: android13-${{ matrix.version }}-${{ matrix.os_patch_level }} patch_path: ${{ matrix.version }} debug: true + build-debug-kernel-a14: + strategy: + matrix: + include: + - version: "5.15" + sub_level: 167 + os_patch_level: 2024-11 + - version: "6.1" + sub_level: 115 + os_patch_level: 2024-12 + uses: ./.github/workflows/gki-kernel.yml + with: + version: android14-${{ matrix.version }} + version_name: android14-${{ matrix.version }}.${{ matrix.sub_level }} + tag: android14-${{ matrix.version }}-${{ matrix.os_patch_level }} + patch_path: ${{ matrix.version }} + debug: true + build-debug-kernel-a15: + strategy: + matrix: + include: + - version: "6.6" + sub_level: 57 + os_patch_level: 2024-12 + uses: ./.github/workflows/gki-kernel.yml + with: + version: android15-${{ matrix.version }} + version_name: android15-${{ matrix.version }}.${{ matrix.sub_level }} + tag: android15-${{ matrix.version }}-${{ matrix.os_patch_level }} + patch_path: ${{ matrix.version }} + debug: true \ No newline at end of file diff --git a/.github/workflows/build-kernel-a12.yml b/.github/workflows/build-kernel-a12.yml index 357383a6a3aa..4e1ea3972ccf 100644 --- a/.github/workflows/build-kernel-a12.yml +++ b/.github/workflows/build-kernel-a12.yml @@ -1,7 +1,7 @@ name: Build Kernel - Android 12 on: push: - branches: ["main", "ci"] + branches: ["main", "ci", "checkci"] paths: - ".github/workflows/build-kernel-a12.yml" - ".github/workflows/gki-kernel.yml" @@ -17,32 +17,16 @@ on: workflow_call: jobs: build-kernel: - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && github.ref != 'refs/heads/checkci' strategy: matrix: include: - - sub_level: 66 - os_patch_level: 2022-01 - - sub_level: 81 - os_patch_level: 2022-03 - - sub_level: 101 - os_patch_level: 2022-05 - - sub_level: 110 - os_patch_level: 2022-07 - - sub_level: 117 - os_patch_level: 2022-09 - - sub_level: 136 - os_patch_level: 2022-11 - - sub_level: 149 - os_patch_level: 2023-01 - - sub_level: 160 - os_patch_level: 2023-03 - - sub_level: 168 - os_patch_level: 2023-05 - - sub_level: 177 - os_patch_level: 2023-07 - - sub_level: 185 - os_patch_level: 2023-09 + - sub_level: 209 + os_patch_level: 2024-05 + - sub_level: 218 + os_patch_level: 2024-08 + - sub_level: 226 + os_patch_level: 2024-11 uses: ./.github/workflows/gki-kernel.yml secrets: inherit with: @@ -51,13 +35,13 @@ jobs: tag: android12-5.10-${{ matrix.os_patch_level }} os_patch_level: ${{ matrix.os_patch_level }} patch_path: "5.10" + upload-artifacts: needs: build-kernel runs-on: ubuntu-latest if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' || github.ref == 'refs/heads/ci' }} env: CHAT_ID: ${{ secrets.CHAT_ID }} - CACHE_CHAT_ID: ${{ secrets.CACHE_CHAT_ID }} BOT_TOKEN: ${{ secrets.BOT_TOKEN }} MESSAGE_THREAD_ID: ${{ secrets.MESSAGE_THREAD_ID }} COMMIT_MESSAGE: ${{ github.event.head_commit.message }} @@ -65,7 +49,7 @@ jobs: RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - uses: actions/checkout@v4 with: @@ -79,11 +63,11 @@ jobs: - name: Download prebuilt toolchain run: | AOSP_MIRROR=https://android.googlesource.com - BRANCH=main-kernel-build-2023 + BRANCH=main-kernel-build-2024 git clone $AOSP_MIRROR/platform/prebuilts/build-tools -b $BRANCH --depth 1 build-tools git clone $AOSP_MIRROR/kernel/prebuilts/build-tools -b $BRANCH --depth 1 kernel-build-tools git clone $AOSP_MIRROR/platform/system/tools/mkbootimg -b $BRANCH --depth 1 - pip3 install python-telegram-bot + pip3 install telethon - name: Set boot sign key env: @@ -93,8 +77,13 @@ jobs: echo "$BOOT_SIGN_KEY" > ./kernel-build-tools/linux-x86/share/avb/testkey_rsa2048.pem fi - - name: Setup mutex for uploading - uses: ben-z/gh-action-mutex@v1.0-alpha-7 + - name: Bot session cache + id: bot_session_cache + uses: actions/cache@v4 + if: false + with: + path: scripts/ksubot.session + key: ${{ runner.os }}-bot-session - name: Build boot images run: | @@ -113,17 +102,17 @@ jobs: run: ls -R - name: Upload images artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: boot-images-android12 path: Image-android12*/*.img.gz check-build-kernel: - if: github.event_name == 'pull_request' + if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || github.ref == 'refs/heads/checkci' uses: ./.github/workflows/gki-kernel.yml with: version: android12-5.10 - version_name: android12-5.10.177 - tag: android12-5.10-2023-06 - os_patch_level: 2023-06 - patch_path: "5.10" + version_name: android12-5.10.223 + tag: android12-5.10-2024-11 + os_patch_level: 2024-11 + patch_path: "5.10" \ No newline at end of file diff --git a/.github/workflows/build-kernel-a13.yml b/.github/workflows/build-kernel-a13.yml index bb44885c8d1d..d9e1c379b89b 100644 --- a/.github/workflows/build-kernel-a13.yml +++ b/.github/workflows/build-kernel-a13.yml @@ -1,7 +1,7 @@ name: Build Kernel - Android 13 on: push: - branches: ["main", "ci"] + branches: ["main", "ci", "checkci"] paths: - ".github/workflows/build-kernel-a13.yml" - ".github/workflows/gki-kernel.yml" @@ -17,49 +17,40 @@ on: workflow_call: jobs: build-kernel: - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && github.ref != 'refs/heads/checkci' strategy: matrix: include: - version: "5.10" - sub_level: 107 - os_patch_level: 2022-11 + sub_level: 209 + os_patch_level: 2024-05 - version: "5.10" - sub_level: 149 - os_patch_level: 2023-01 - - version: "5.10" - sub_level: 157 - os_patch_level: 2023-03 + sub_level: 210 + os_patch_level: 2024-06 - version: "5.10" - sub_level: 168 - os_patch_level: 2023-05 + sub_level: 214 + os_patch_level: 2024-07 - version: "5.10" - sub_level: 177 - os_patch_level: 2023-06 + sub_level: 218 + os_patch_level: 2024-08 - version: "5.10" - sub_level: 186 - os_patch_level: 2023-08 - - version: "5.10" - sub_level: 186 - os_patch_level: 2023-09 - - version: "5.15" - sub_level: 41 - os_patch_level: 2022-11 + sub_level: 223 + os_patch_level: 2024-11 - version: "5.15" - sub_level: 74 - os_patch_level: 2023-01 + sub_level: 148 + os_patch_level: 2024-05 - version: "5.15" - sub_level: 78 - os_patch_level: 2023-03 + sub_level: 149 + os_patch_level: 2024-07 - version: "5.15" - sub_level: 94 - os_patch_level: 2023-05 + sub_level: 151 + os_patch_level: 2024-08 - version: "5.15" - sub_level: 104 - os_patch_level: 2023-07 + sub_level: 153 + os_patch_level: 2024-09 - version: "5.15" - sub_level: 119 - os_patch_level: 2023-09 + sub_level: 167 + os_patch_level: 2024-11 uses: ./.github/workflows/gki-kernel.yml secrets: inherit with: @@ -68,13 +59,13 @@ jobs: tag: android13-${{ matrix.version }}-${{ matrix.os_patch_level }} os_patch_level: ${{ matrix.os_patch_level }} patch_path: ${{ matrix.version }} + upload-artifacts: needs: build-kernel runs-on: ubuntu-latest if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' || github.ref == 'refs/heads/ci' }} env: CHAT_ID: ${{ secrets.CHAT_ID }} - CACHE_CHAT_ID: ${{ secrets.CACHE_CHAT_ID }} BOT_TOKEN: ${{ secrets.BOT_TOKEN }} MESSAGE_THREAD_ID: ${{ secrets.MESSAGE_THREAD_ID }} COMMIT_MESSAGE: ${{ github.event.head_commit.message }} @@ -82,7 +73,7 @@ jobs: RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - uses: actions/checkout@v4 with: @@ -96,11 +87,11 @@ jobs: - name: Download prebuilt toolchain run: | AOSP_MIRROR=https://android.googlesource.com - BRANCH=main-kernel-build-2023 + BRANCH=main-kernel-build-2024 git clone $AOSP_MIRROR/platform/prebuilts/build-tools -b $BRANCH --depth 1 build-tools git clone $AOSP_MIRROR/kernel/prebuilts/build-tools -b $BRANCH --depth 1 kernel-build-tools git clone $AOSP_MIRROR/platform/system/tools/mkbootimg -b $BRANCH --depth 1 - pip3 install python-telegram-bot + pip3 install telethon - name: Set boot sign key env: @@ -110,8 +101,13 @@ jobs: echo "$BOOT_SIGN_KEY" > ./kernel-build-tools/linux-x86/share/avb/testkey_rsa2048.pem fi - - name: Setup mutex for uploading - uses: ben-z/gh-action-mutex@v1.0-alpha-7 + - name: Bot session cache + id: bot_session_cache + uses: actions/cache@v4 + if: false + with: + path: scripts/ksubot.session + key: ${{ runner.os }}-bot-session - name: Build boot images run: | @@ -125,31 +121,31 @@ jobs: echo "VERSION: $VERSION" cd - bash $GITHUB_WORKSPACE/KernelSU/.github/scripts/build_a13.sh - + - name: Display structure of boot files run: ls -R - name: Upload images artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: boot-images-android13 path: Image-android13*/*.img.gz check-build-kernel: - if: github.event_name == 'pull_request' + if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || github.ref == 'refs/heads/checkci' strategy: matrix: include: - version: "5.10" - sub_level: 187 - os_patch_level: 2023-08 + sub_level: 223 + os_patch_level: 2024-11 - version: "5.15" - sub_level: 119 - os_patch_level: 2023-09 + sub_level: 167 + os_patch_level: 2024-11 uses: ./.github/workflows/gki-kernel.yml with: version: android13-${{ matrix.version }} version_name: android13-${{ matrix.version }}.${{ matrix.sub_level }} tag: android13-${{ matrix.version }}-${{ matrix.os_patch_level }} os_patch_level: ${{ matrix.os_patch_level }} - patch_path: ${{ matrix.version }} + patch_path: ${{ matrix.version }} \ No newline at end of file diff --git a/.github/workflows/build-kernel-a14.yml b/.github/workflows/build-kernel-a14.yml index a9c4be8075c6..a65873e2ec00 100644 --- a/.github/workflows/build-kernel-a14.yml +++ b/.github/workflows/build-kernel-a14.yml @@ -1,7 +1,7 @@ name: Build Kernel - Android 14 on: push: - branches: ["main", "ci"] + branches: ["main", "ci", "checkci"] paths: - ".github/workflows/build-kernel-a14.yml" - ".github/workflows/gki-kernel.yml" @@ -17,13 +17,52 @@ on: workflow_call: jobs: build-kernel: - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && github.ref != 'refs/heads/checkci' strategy: matrix: include: - version: "5.15" - sub_level: 110 - os_patch_level: 2023-09 + sub_level: 148 + os_patch_level: 2024-05 + - version: "5.15" + sub_level: 149 + os_patch_level: 2024-06 + - version: "5.15" + sub_level: 153 + os_patch_level: 2024-07 + - version: "5.15" + sub_level: 158 + os_patch_level: 2024-08 + - version: "5.15" + sub_level: 164 + os_patch_level: 2024-09 + - version: "5.15" + sub_level: 167 + os_patch_level: 2024-11 + - version: "6.1" + sub_level: 75 + os_patch_level: 2024-05 + - version: "6.1" + sub_level: 78 + os_patch_level: 2024-06 + - version: "6.1" + sub_level: 84 + os_patch_level: 2024-07 + - version: "6.1" + sub_level: 90 + os_patch_level: 2024-08 + - version: "6.1" + sub_level: 93 + os_patch_level: 2024-09 + - version: "6.1" + sub_level: 99 + os_patch_level: 2024-10 + - version: "6.1" + sub_level: 112 + os_patch_level: 2024-11 + - version: "6.1" + sub_level: 115 + os_patch_level: 2024-12 uses: ./.github/workflows/gki-kernel.yml secrets: inherit with: @@ -32,13 +71,13 @@ jobs: tag: android14-${{ matrix.version }}-${{ matrix.os_patch_level }} os_patch_level: ${{ matrix.os_patch_level }} patch_path: ${{ matrix.version }} + upload-artifacts: needs: build-kernel runs-on: ubuntu-latest if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' || github.ref == 'refs/heads/ci' }} env: CHAT_ID: ${{ secrets.CHAT_ID }} - CACHE_CHAT_ID: ${{ secrets.CACHE_CHAT_ID }} BOT_TOKEN: ${{ secrets.BOT_TOKEN }} MESSAGE_THREAD_ID: ${{ secrets.MESSAGE_THREAD_ID }} COMMIT_MESSAGE: ${{ github.event.head_commit.message }} @@ -46,7 +85,7 @@ jobs: RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} steps: - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 - uses: actions/checkout@v4 with: @@ -60,11 +99,11 @@ jobs: - name: Download prebuilt toolchain run: | AOSP_MIRROR=https://android.googlesource.com - BRANCH=main-kernel-build-2023 + BRANCH=main-kernel-build-2024 git clone $AOSP_MIRROR/platform/prebuilts/build-tools -b $BRANCH --depth 1 build-tools git clone $AOSP_MIRROR/kernel/prebuilts/build-tools -b $BRANCH --depth 1 kernel-build-tools git clone $AOSP_MIRROR/platform/system/tools/mkbootimg -b $BRANCH --depth 1 - pip3 install python-telegram-bot + pip3 install telethon - name: Set boot sign key env: @@ -74,8 +113,13 @@ jobs: echo "$BOOT_SIGN_KEY" > ./kernel-build-tools/linux-x86/share/avb/testkey_rsa2048.pem fi - - name: Setup mutex for uploading - uses: ben-z/gh-action-mutex@v1.0-alpha-7 + - name: Bot session cache + id: bot_session_cache + uses: actions/cache@v4 + if: false + with: + path: scripts/ksubot.session + key: ${{ runner.os }}-bot-session - name: Build boot images run: | @@ -89,28 +133,31 @@ jobs: echo "VERSION: $VERSION" cd - bash $GITHUB_WORKSPACE/KernelSU/.github/scripts/build_a13.sh - + - name: Display structure of boot files run: ls -R - name: Upload images artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: - name: boot-images-android13 - path: Image-android13*/*.img.gz + name: boot-images-android14 + path: Image-android14*/*.img.gz check-build-kernel: - if: github.event_name == 'pull_request' + if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || github.ref == 'refs/heads/checkci' strategy: matrix: include: - version: "5.15" - sub_level: 110 - os_patch_level: 2023-09 + sub_level: 167 + os_patch_level: 2024-11 + - version: "6.1" + sub_level: 115 + os_patch_level: 2024-12 uses: ./.github/workflows/gki-kernel.yml with: version: android14-${{ matrix.version }} version_name: android14-${{ matrix.version }}.${{ matrix.sub_level }} tag: android14-${{ matrix.version }}-${{ matrix.os_patch_level }} os_patch_level: ${{ matrix.os_patch_level }} - patch_path: ${{ matrix.version }} + patch_path: ${{ matrix.version }} \ No newline at end of file diff --git a/.github/workflows/build-kernel-a15.yml b/.github/workflows/build-kernel-a15.yml new file mode 100644 index 000000000000..6c325395f445 --- /dev/null +++ b/.github/workflows/build-kernel-a15.yml @@ -0,0 +1,133 @@ +name: Build Kernel - Android 15 +on: + push: + branches: ["main", "ci", "checkci"] + paths: + - ".github/workflows/build-kernel-a15.yml" + - ".github/workflows/gki-kernel.yml" + - ".github/scripts/build_a13.sh" + - "kernel/**" + pull_request: + branches: ["main"] + paths: + - ".github/workflows/build-kernel-a15.yml" + - ".github/workflows/gki-kernel.yml" + - ".github/scripts/build-a13.sh" + - "kernel/**" + workflow_call: +jobs: + build-kernel: + if: github.event_name != 'pull_request' && github.ref != 'refs/heads/checkci' + strategy: + matrix: + include: + - version: "6.6" + sub_level: 30 + os_patch_level: 2024-08 + - version: "6.6" + sub_level: 46 + os_patch_level: 2024-09 + - version: "6.6" + sub_level: 50 + os_patch_level: 2024-10 + - version: "6.6" + sub_level: 56 + os_patch_level: 2024-11 + - version: "6.6" + sub_level: 57 + os_patch_level: 2024-12 + uses: ./.github/workflows/gki-kernel.yml + secrets: inherit + with: + version: android15-${{ matrix.version }} + version_name: android15-${{ matrix.version }}.${{ matrix.sub_level }} + tag: android15-${{ matrix.version }}-${{ matrix.os_patch_level }} + os_patch_level: ${{ matrix.os_patch_level }} + patch_path: ${{ matrix.version }} + + upload-artifacts: + needs: build-kernel + runs-on: ubuntu-latest + if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' || github.ref == 'refs/heads/ci' }} + env: + CHAT_ID: ${{ secrets.CHAT_ID }} + BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + MESSAGE_THREAD_ID: ${{ secrets.MESSAGE_THREAD_ID }} + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + COMMIT_URL: ${{ github.event.head_commit.url }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + + - uses: actions/checkout@v4 + with: + path: KernelSU + fetch-depth: 0 + + - name: List artifacts + run: | + tree + + - name: Download prebuilt toolchain + run: | + AOSP_MIRROR=https://android.googlesource.com + BRANCH=main-kernel-build-2024 + git clone $AOSP_MIRROR/platform/prebuilts/build-tools -b $BRANCH --depth 1 build-tools + git clone $AOSP_MIRROR/kernel/prebuilts/build-tools -b $BRANCH --depth 1 kernel-build-tools + git clone $AOSP_MIRROR/platform/system/tools/mkbootimg -b $BRANCH --depth 1 + pip3 install telethon + + - name: Set boot sign key + env: + BOOT_SIGN_KEY: ${{ secrets.BOOT_SIGN_KEY }} + run: | + if [ ! -z "$BOOT_SIGN_KEY" ]; then + echo "$BOOT_SIGN_KEY" > ./kernel-build-tools/linux-x86/share/avb/testkey_rsa2048.pem + fi + + - name: Bot session cache + id: bot_session_cache + uses: actions/cache@v4 + if: false + with: + path: scripts/ksubot.session + key: ${{ runner.os }}-bot-session + + - name: Build boot images + run: | + export AVBTOOL=$GITHUB_WORKSPACE/kernel-build-tools/linux-x86/bin/avbtool + export GZIP=$GITHUB_WORKSPACE/build-tools/path/linux-x86/gzip + export LZ4=$GITHUB_WORKSPACE/build-tools/path/linux-x86/lz4 + export MKBOOTIMG=$GITHUB_WORKSPACE/mkbootimg/mkbootimg.py + export UNPACK_BOOTIMG=$GITHUB_WORKSPACE/mkbootimg/unpack_bootimg.py + cd $GITHUB_WORKSPACE/KernelSU + export VERSION=$(($(git rev-list --count HEAD) + 10200)) + echo "VERSION: $VERSION" + cd - + bash $GITHUB_WORKSPACE/KernelSU/.github/scripts/build_a13.sh + + - name: Display structure of boot files + run: ls -R + + - name: Upload images artifact + uses: actions/upload-artifact@v4 + with: + name: boot-images-android15 + path: Image-android15*/*.img.gz + + check-build-kernel: + if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || github.ref == 'refs/heads/checkci' + strategy: + matrix: + include: + - version: "6.6" + sub_level: 57 + os_patch_level: 2024-12 + uses: ./.github/workflows/gki-kernel.yml + with: + version: android15-${{ matrix.version }} + version_name: android15-${{ matrix.version }}.${{ matrix.sub_level }} + tag: android15-${{ matrix.version }}-${{ matrix.os_patch_level }} + os_patch_level: ${{ matrix.os_patch_level }} + patch_path: ${{ matrix.version }} \ No newline at end of file diff --git a/.github/workflows/build-kernel-arcvm.yml b/.github/workflows/build-kernel-arcvm.yml index 859b31a9114a..9dbe8de1a0f6 100644 --- a/.github/workflows/build-kernel-arcvm.yml +++ b/.github/workflows/build-kernel-arcvm.yml @@ -1,7 +1,7 @@ name: Build Kernel - ChromeOS ARCVM on: push: - branches: ["main"] + branches: ["main", "ci", "checkci"] paths: - ".github/workflows/build-kernel-arcvm.yml" - "kernel/**" @@ -13,16 +13,23 @@ on: workflow_call: workflow_dispatch: +env: + git_tag: chromeos-5.10-arcvm + jobs: build: + if: github.event_name != 'pull_request' || (github.event_name == 'pull_request' && !github.event.pull_request.draft) strategy: matrix: - arch: [x86_64] - version: ["5.10.178"] include: - arch: x86_64 - git_tag: chromeos-5.10-arcvm - file_name: "bzImage" + kernel_image_name: bzImage + build_config: build.config.gki.x86_64 + defconfig: x86_64_arcvm_defconfig + - arch: arm64 + kernel_image_name: Image + build_config: build.config.gki.aarch64 + defconfig: arm64_arcvm_defconfig name: Build ChromeOS ARCVM kernel runs-on: ubuntu-20.04 @@ -65,7 +72,16 @@ jobs: fetch-depth: 0 - name: Setup kernel source - run: git clone https://chromium.googlesource.com/chromiumos/third_party/kernel.git -b ${{ matrix.git_tag }} --depth=1 + run: git clone https://chromium.googlesource.com/chromiumos/third_party/kernel.git -b ${{ env.git_tag }} --depth=1 + + - name: Extract version from Makefile + working-directory: kernel + run: | + VERSION=$(grep -E '^VERSION = ' Makefile | awk '{print $3}') + PATCHLEVEL=$(grep -E '^PATCHLEVEL = ' Makefile | awk '{print $3}') + SUBLEVEL=$(grep -E '^SUBLEVEL = ' Makefile | awk '{print $3}') + echo "ChromeOS ARCVM Linux kernel version: $VERSION.$PATCHLEVEL.$SUBLEVEL" + echo "version=$VERSION.$PATCHLEVEL.$SUBLEVEL" >> $GITHUB_ENV - name: Setup KernelSU working-directory: kernel @@ -78,25 +94,30 @@ jobs: echo "[+] Add KernelSU driver to Makefile" DRIVER_MAKEFILE=$KERNEL_ROOT/drivers/Makefile - grep -q "kernelsu" $DRIVER_MAKEFILE || echo "obj-y += kernelsu/" >> $DRIVER_MAKEFILE + DRIVER_KCONFIG=$KERNEL_ROOT/drivers/Kconfig + grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" + grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" echo "[+] Apply KernelSU patches" - cd $KERNEL_ROOT && git apply $GITHUB_WORKSPACE/KernelSU/.github/patches/5.10/*.patch + cd $KERNEL_ROOT && git apply $GITHUB_WORKSPACE/KernelSU/.github/patches/5.10/*.patch || echo "[-] No patch found" echo "[+] Patch script/setlocalversion" sed -i 's/-dirty//g' $KERNEL_ROOT/scripts/setlocalversion echo "[+] KernelSU setup done." cd $GITHUB_WORKSPACE/KernelSU - VERSION=$(($(git rev-list --count HEAD) + 10200)) - echo "VERSION: $VERSION" - echo "kernelsu_version=$VERSION" >> $GITHUB_ENV + KSU_VERSION=$(($(git rev-list --count HEAD) + 10200)) + echo "KernelSU version: $KSU_VERSION" + echo "kernelsu_version=$KSU_VERSION" >> $GITHUB_ENV - name: Build Kernel working-directory: kernel + env: + KERNEL_IMAGE_NAME: ${{ matrix.kernel_image_name }} + ARCH: ${{ matrix.arch }} run: | - set -a && . build.config.gki.x86_64; set +a - export DEFCONFIG=x86_64_arcvm_defconfig + set -a && . ${{ matrix.build_config }}; set +a + export DEFCONFIG=${{ matrix.defconfig }} if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }} export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }} @@ -105,36 +126,12 @@ jobs: make LLVM=1 LLVM_IAS=1 DEPMOD=depmod DTC=dtc O=${PWD} mrproper make LLVM=1 LLVM_IAS=1 DEPMOD=depmod DTC=dtc O=${PWD} ${DEFCONFIG} < /dev/null scripts/config --file .config -e LTO_CLANG -d LTO_NONE -e LTO_CLANG_THIN -d LTO_CLANG_FULL -e THINLTO - make LLVM=1 LLVM_IAS=1 DEPMOD=depmod DTC=dtc O=${PWD} -j$(nproc) bzImage modules prepare-objtool + make LLVM=1 LLVM_IAS=1 DEPMOD=depmod DTC=dtc O=${PWD} -j$(nproc) ${KERNEL_IMAGE_NAME} modules prepare-objtool + ls -l -h ${PWD}/arch/${ARCH}/boot + echo "file_path=${PWD}/arch/${ARCH}/boot/${KERNEL_IMAGE_NAME}" >> $GITHUB_ENV - echo "file_path=${PWD}/arch/x86/boot/bzImage" >> $GITHUB_ENV - - - name: Upload kernel-ARCVM-${{ matrix.arch }}-${{ matrix.version }} - uses: actions/upload-artifact@v3 + - name: Upload kernel-ARCVM-${{ matrix.arch }}-${{ env.version }} + uses: actions/upload-artifact@v4 with: - name: kernel-ARCVM-${{ matrix.arch }}-${{ matrix.version }} + name: kernel-ARCVM-${{ matrix.arch }}-${{ env.version }} path: "${{ env.file_path }}" - - - name: Post to Telegram - if: ${{ ( github.event_name == 'push' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }} - env: - CHAT_ID: ${{ secrets.CHAT_ID }} - CACHE_CHAT_ID: ${{ secrets.CACHE_CHAT_ID }} - BOT_TOKEN: ${{ secrets.BOT_TOKEN }} - MESSAGE_THREAD_ID: ${{ secrets.MESSAGE_THREAD_ID }} - COMMIT_MESSAGE: ${{ github.event.head_commit.message }} - COMMIT_URL: ${{ github.event.head_commit.url }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - TITLE=kernel-ARCVM-${{ matrix.arch }}-${{ matrix.version }} - echo "[+] title: $TITLE" - export TITLE - export VERSION="${{ env.kernelsu_version }}" - echo "[+] Compress images" - gzip -n -f -9 "${{ env.file_path }}" - echo "[+] Image to upload" - ls -l "${{ env.file_path }}.gz" - if [ -n "${{ secrets.BOT_TOKEN }}" ]; then - pip3 install python-telegram-bot - python3 "$GITHUB_WORKSPACE/KernelSU/scripts/ksubot.py" "${{ env.file_path }}.gz" - fi diff --git a/.github/workflows/build-kernel-avd.yml b/.github/workflows/build-kernel-avd.yml new file mode 100644 index 000000000000..504a5207ee10 --- /dev/null +++ b/.github/workflows/build-kernel-avd.yml @@ -0,0 +1,40 @@ +name: Build Kernel - AVD +on: + push: + branches: ["main", "ci", "checkci"] + paths: + - ".github/workflows/build-kernel-avd.yml" + - ".github/workflows/avd-kernel.yml" + - ".github/workflows/manifests/*xml" + - "kernel/**" + pull_request: + branches: ["main"] + paths: + - ".github/workflows/build-kernel-avd.yml" + - ".github/workflows/avd-kernel.yml" + - ".github/workflows/manifests/*.xml" + - "kernel/**" + workflow_call: + workflow_dispatch: +jobs: + build-kernel: + if: github.event_name != 'pull_request' && github.ref != 'refs/heads/checkci' + uses: ./.github/workflows/avd-kernel.yml + secrets: inherit + strategy: + matrix: + include: + - version: "android-14-avd_x86_64" + manifest: "android-14-avd_x86_64.xml" + arch: "x86_64" + - version: "android-15-avd_aarch64" + manifest: "android-15-avd_aarch64.xml" + arch: "aarch64" + - version: "android-15-avd_x86_64" + manifest: "android-15-avd_x86_64.xml" + arch: "x86_64" + with: + version_name: ${{ matrix.version }} + manifest_name: ${{ matrix.manifest }} + arch: ${{ matrix.arch }} + debug: true diff --git a/.github/workflows/build-kernel-wsa.yml b/.github/workflows/build-kernel-wsa.yml index d3990c20bf06..9a313de9a1db 100644 --- a/.github/workflows/build-kernel-wsa.yml +++ b/.github/workflows/build-kernel-wsa.yml @@ -1,134 +1,38 @@ name: Build Kernel - WSA on: push: - branches: ["main"] + branches: ["main", "ci", "checkci"] paths: - ".github/workflows/build-kernel-wsa.yml" + - ".github/workflows/wsa-kernel.yml" - "kernel/**" pull_request: branches: ["main"] paths: - ".github/workflows/build-kernel-wsa.yml" + - ".github/workflows/wsa-kernel.yml" - "kernel/**" workflow_call: workflow_dispatch: jobs: build: + if: github.event_name != 'pull_request' && github.ref != 'refs/heads/checkci' strategy: matrix: arch: [x86_64, arm64] version: ["5.15.94.2", "5.15.104.1", "5.15.104.2", "5.15.104.3", "5.15.104.4"] - - name: Build WSA-Kernel-${{ matrix.version }}-${{ matrix.arch }} - runs-on: ubuntu-20.04 - env: - CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" - CCACHE_NOHASHDIR: "true" - CCACHE_HARDLINK: "true" - - steps: - - name: Install Build Tools - uses: awalsh128/cache-apt-pkgs-action@v1 - with: - packages: bc bison build-essential flex libelf-dev binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu gzip ccache - version: 1.0 - - - name: Cache LLVM - id: cache-llvm - uses: actions/cache@v3 - with: - path: ./llvm - key: llvm-12.0.1 - - - name: Setup LLVM - uses: KyleMayes/install-llvm-action@v1 - with: - version: "12.0.1" - force-version: true - ubuntu-version: "16.04" - cached: ${{ steps.cache-llvm.outputs.cache-hit }} - - - name: Checkout KernelSU - uses: actions/checkout@v4 - with: - path: KernelSU - fetch-depth: 0 - - - name: Setup kernel source - uses: actions/checkout@v4 - with: - repository: microsoft/WSA-Linux-Kernel - ref: android-lts/latte-2/${{ matrix.version }} - path: WSA-Linux-Kernel - - - name: Setup Ccache - uses: hendrikmuhs/ccache-action@v1.2 - with: - key: WSA-Kernel-${{ matrix.version }}-${{ matrix.arch }} - save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - max-size: 2G - - - name: Setup KernelSU - working-directory: WSA-Linux-Kernel - run: | - echo "[+] KernelSU setup" - KERNEL_ROOT=$GITHUB_WORKSPACE/WSA-Linux-Kernel - echo "[+] KERNEL_ROOT: $KERNEL_ROOT" - echo "[+] Copy KernelSU driver to $KERNEL_ROOT/drivers" - ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $KERNEL_ROOT/drivers/kernelsu - echo "[+] Add KernelSU driver to Makefile" - DRIVER_MAKEFILE=$KERNEL_ROOT/drivers/Makefile - grep -q "kernelsu" $DRIVER_MAKEFILE || echo "obj-y += kernelsu/" >> $DRIVER_MAKEFILE - echo "[+] Apply KernelSU patches" - cd $KERNEL_ROOT && git apply $GITHUB_WORKSPACE/KernelSU/.github/patches/5.15/*.patch - echo "[+] KernelSU setup done." - cd $GITHUB_WORKSPACE/KernelSU - VERSION=$(($(git rev-list --count HEAD) + 10200)) - echo "VERSION: $VERSION" - echo "kernelsu_version=$VERSION" >> $GITHUB_ENV - - - name: Build Kernel - working-directory: WSA-Linux-Kernel - run: | - if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then - export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }} - export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }} - fi - declare -A ARCH_MAP=(["x86_64"]="x64" ["arm64"]="arm64") - cp configs/wsa/config-wsa-${ARCH_MAP[${{ matrix.arch }}]} .config - make olddefconfig - declare -A FILE_NAME=(["x86_64"]="bzImage" ["arm64"]="Image") - make -j`nproc` LLVM=1 ARCH=${{ matrix.arch }} $(if [ "${{ matrix.arch }}" == "arm64" ]; then echo CROSS_COMPILE=aarch64-linux-gnu; fi) ${FILE_NAME[${{ matrix.arch }}]} CCACHE="/usr/bin/ccache" - declare -A ARCH_MAP_FILE=(["x86_64"]="x86" ["arm64"]="arm64") - echo "file_path=WSA-Linux-Kernel/arch/${ARCH_MAP_FILE[${{ matrix.arch }}]}/boot/${FILE_NAME[${{ matrix.arch }}]}" >> $GITHUB_ENV - - - name: Upload kernel-${{ matrix.arch }}-${{ matrix.version }} - uses: actions/upload-artifact@v3 - with: - name: kernel-WSA-${{ matrix.arch }}-${{ matrix.version }} - path: "${{ env.file_path }}" - - - name: Post to Telegram - if: github.event_name == 'push' && github.ref == 'refs/heads/main' || github.ref_type == 'tag' - env: - CHAT_ID: ${{ secrets.CHAT_ID }} - CACHE_CHAT_ID: ${{ secrets.CACHE_CHAT_ID }} - BOT_TOKEN: ${{ secrets.BOT_TOKEN }} - MESSAGE_THREAD_ID: ${{ secrets.MESSAGE_THREAD_ID }} - COMMIT_MESSAGE: ${{ github.event.head_commit.message }} - COMMIT_URL: ${{ github.event.head_commit.url }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - run: | - TITLE=kernel-${{ matrix.arch }}-WSA-${{ matrix.version }} - echo "[+] title: $TITLE" - export TITLE - export VERSION="${{ env.kernelsu_version }}" - echo "[+] Compress images" - gzip -n -f -9 "${{ env.file_path }}" - echo "[+] Image to upload" - ls -l "${{ env.file_path }}.gz" - if [ -n "${{ secrets.BOT_TOKEN }}" ]; then - pip3 install python-telegram-bot - python3 "$GITHUB_WORKSPACE/KernelSU/scripts/ksubot.py" "${{ env.file_path }}.gz" - fi + uses: ./.github/workflows/wsa-kernel.yml + with: + arch: ${{ matrix.arch }} + version: ${{ matrix.version }} + + check_build: + if: (github.event_name == 'pull_request' && !github.event.pull_request.draft) || github.ref == 'refs/heads/checkci' + uses: ./.github/workflows/wsa-kernel.yml + strategy: + matrix: + arch: [x86_64, arm64] + with: + arch: ${{ matrix.arch }} + version: "5.15.104.4" \ No newline at end of file diff --git a/.github/workflows/build-ksud.yml b/.github/workflows/build-ksud.yml deleted file mode 100644 index 7dbbd683af12..000000000000 --- a/.github/workflows/build-ksud.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Build KSUD -on: - push: - branches: [ "main", "ci" ] - paths: - - '.github/workflows/build-ksud.yml' - - '.github/workflows/ksud.yml' - - 'userspace/ksud/**' - pull_request: - branches: [ "main" ] - paths: - - '.github/workflows/build-ksud.yml' - - '.github/workflows/ksud.yml' - - 'userspace/ksud/**' -jobs: - build: - strategy: - matrix: - include: - - target: aarch64-linux-android - - target: x86_64-linux-android - - target: x86_64-pc-windows-gnu # only for build - uses: ./.github/workflows/ksud.yml - with: - target: ${{ matrix.target }} diff --git a/.github/workflows/build-lkm.yml b/.github/workflows/build-lkm.yml new file mode 100644 index 000000000000..ef9282dbed31 --- /dev/null +++ b/.github/workflows/build-lkm.yml @@ -0,0 +1,41 @@ +name: Build LKM for KernelSU +on: + push: + branches: ["main", "ci", "checkci"] + paths: + - ".github/workflows/build-lkm.yml" + pull_request: + branches: ["main"] + paths: + - ".github/workflows/build-lkm.yml" + workflow_call: +jobs: + build-lkm: + strategy: + matrix: + include: + - version: "android12-5.10" + sub_level: 226 + os_patch_level: 2024-11 + - version: "android13-5.10" + sub_level: 223 + os_patch_level: 2024-11 + - version: "android13-5.15" + sub_level: 167 + os_patch_level: 2024-11 + - version: "android14-5.15" + sub_level: 167 + os_patch_level: 2024-11 + - version: "android14-6.1" + sub_level: 115 + os_patch_level: 2024-12 + - version: "android15-6.6" + sub_level: 57 + os_patch_level: 2024-12 + uses: ./.github/workflows/gki-kernel.yml + with: + version: ${{ matrix.version }} + version_name: ${{ matrix.version }}.${{ matrix.sub_level }} + tag: ${{ matrix.version }}-${{ matrix.os_patch_level }} + os_patch_level: ${{ matrix.os_patch_level }} + build_lkm: true \ No newline at end of file diff --git a/.github/workflows/build-manager.yml b/.github/workflows/build-manager.yml index 44410f0010a3..89360617be92 100644 --- a/.github/workflows/build-manager.yml +++ b/.github/workflows/build-manager.yml @@ -2,10 +2,11 @@ name: Build Manager on: push: - branches: [ "main" ] + branches: [ "main", "ci" ] paths: - '.github/workflows/build-manager.yml' - 'manager/**' + - 'kernel/**' - 'userspace/ksud/**' pull_request: branches: [ "main" ] @@ -14,15 +15,33 @@ on: workflow_call: jobs: + build-lkm: + uses: ./.github/workflows/build-lkm.yml + secrets: inherit + build-ksud: + needs: build-lkm strategy: matrix: include: - target: aarch64-linux-android + os: ubuntu-latest - target: x86_64-linux-android + os: ubuntu-latest + - target: x86_64-pc-windows-gnu # windows pc + os: ubuntu-latest + - target: x86_64-apple-darwin # Intel mac + os: macos-latest + - target: aarch64-apple-darwin # M chip mac + os: macos-latest + - target: aarch64-unknown-linux-musl # arm64 Linux + os: ubuntu-latest + - target: x86_64-unknown-linux-musl # x86 Linux + os: ubuntu-latest uses: ./.github/workflows/ksud.yml with: target: ${{ matrix.target }} + os: ${{ matrix.os }} build-manager: needs: build-ksud @@ -50,32 +69,35 @@ jobs: if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }} run: | if [ ! -z "${{ secrets.KEYSTORE }}" ]; then - echo KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' >> gradle.properties - echo KEY_ALIAS='${{ secrets.KEY_ALIAS }}' >> gradle.properties - echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' >> gradle.properties - echo KEYSTORE_FILE='../key.jks' >> gradle.properties - echo ${{ secrets.KEYSTORE }} | base64 --decode > key.jks + { + echo KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' + echo KEY_ALIAS='${{ secrets.KEY_ALIAS }}' + echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' + echo KEYSTORE_FILE='key.jks' + } >> gradle.properties + echo ${{ secrets.KEYSTORE }} | base64 -d > key.jks fi - name: Setup Java - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: - distribution: "temurin" - java-version: "17" + distribution: temurin + java-version: 21 - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - with: - gradle-home-cache-cleanup: true + uses: gradle/actions/setup-gradle@v4 + + - name: Setup Android SDK + uses: android-actions/setup-android@v3 - name: Download arm64 ksud - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ksud-aarch64-linux-android path: . - name: Download x86_64 ksud - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: ksud-x86_64-linux-android path: . @@ -89,28 +111,41 @@ jobs: - name: Build with Gradle run: | - echo 'org.gradle.parallel=true' >> gradle.properties - echo 'org.gradle.vfs.watch=true' >> gradle.properties - echo 'org.gradle.jvmargs=-Xmx2048m' >> gradle.properties - echo 'android.native.buildOutput=verbose' >> gradle.properties + { + echo 'org.gradle.parallel=true' + echo 'org.gradle.vfs.watch=true' + echo 'org.gradle.jvmargs=-Xmx2048m' + echo 'android.native.buildOutput=verbose' + } >> gradle.properties sed -i 's/org.gradle.configuration-cache=true//g' gradle.properties ./gradlew clean assembleRelease - name: Upload build artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 + if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }} with: name: manager path: manager/app/build/outputs/apk/release/*.apk - - name: Setup mutex for uploading - uses: ben-z/gh-action-mutex@v1.0-alpha-7 + - name: Upload mappings + uses: actions/upload-artifact@v4 + if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref_type == 'tag' }} + with: + name: "mappings" + path: "manager/app/build/outputs/mapping/release/" + + - name: Bot session cache if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true' + id: bot_session_cache + uses: actions/cache@v4 + with: + path: scripts/ksubot.session + key: ${{ runner.os }}-bot-session - name: Upload to telegram if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true' env: CHAT_ID: ${{ secrets.CHAT_ID }} - CACHE_CHAT_ID: ${{ secrets.CACHE_CHAT_ID }} BOT_TOKEN: ${{ secrets.BOT_TOKEN }} MESSAGE_THREAD_ID: ${{ secrets.MESSAGE_THREAD_ID }} COMMIT_MESSAGE: ${{ github.event.head_commit.message }} @@ -121,6 +156,6 @@ jobs: if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then export VERSION=$(git rev-list --count HEAD) APK=$(find ./app/build/outputs/apk/release -name "*.apk") - pip3 install python-telegram-bot + pip3 install telethon python3 $GITHUB_WORKSPACE/scripts/ksubot.py $APK fi diff --git a/.github/workflows/build-su.yml b/.github/workflows/build-su.yml index 93a4a628369a..22fd58af4508 100644 --- a/.github/workflows/build-su.yml +++ b/.github/workflows/build-su.yml @@ -1,7 +1,7 @@ name: Build SU on: push: - branches: [ "main" ] + branches: [ "main", "ci" ] paths: - '.github/workflows/build-su.yml' - 'userspace/su/**' @@ -26,37 +26,11 @@ jobs: else echo "UPLOAD=false" >> $GITHUB_OUTPUT fi - - uses: nttld/setup-ndk@v1 - with: - ndk-version: r25b - local-cache: true - name: Build su working-directory: ./userspace/su - run: ndk-build + run: $ANDROID_NDK/ndk-build - name: Upload a Build Artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: su path: ./userspace/su/libs - - name: Setup mutex for uploading - uses: ben-z/gh-action-mutex@v1.0-alpha-7 - if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true' - - name: Upload to telegram - if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true' - env: - CHAT_ID: ${{ secrets.CHAT_ID }} - CACHE_CHAT_ID: ${{ secrets.CACHE_CHAT_ID }} - BOT_TOKEN: ${{ secrets.BOT_TOKEN }} - MESSAGE_THREAD_ID: ${{ secrets.MESSAGE_THREAD_ID }} - COMMIT_MESSAGE: ${{ github.event.head_commit.message }} - COMMIT_URL: ${{ github.event.head_commit.url }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - TITLE: SU - run: | - if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then - export VERSION=$(git rev-list --count HEAD) - pip3 install python-telegram-bot - mv ./userspace/su/libs/arm64-v8a/su su-arm64 - mv ./userspace/su/libs/x86_64/su su-x86_64 - python3 scripts/ksubot.py su-arm64 su-x86_64 - fi diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml index e27f2321dc28..145475e450fa 100644 --- a/.github/workflows/clippy.yml +++ b/.github/workflows/clippy.yml @@ -22,14 +22,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - # cross build failed after Rust 1.68, see https://github.com/cross-rs/cross/issues/1222 - - run: rustup default 1.67.0 + - run: rustup update stable - uses: Swatinem/rust-cache@v2 with: workspaces: userspace/ksud - name: Install cross - run: cargo install cross + run: | + cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1 - name: Run clippy run: | diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml index d6597c25eaaa..b2b2b763556f 100644 --- a/.github/workflows/deploy-website.yml +++ b/.github/workflows/deploy-website.yml @@ -8,30 +8,60 @@ on: paths: - '.github/workflows/deploy-website.yml' - 'website/**' + workflow_dispatch: + +# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages +permissions: + contents: read + pages: write + id-token: write + +# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. +# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. +concurrency: + group: pages + cancel-in-progress: false jobs: - deploy: + # Build job + build: runs-on: ubuntu-latest defaults: run: working-directory: ./website steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4 with: - fetch-depth: 0 - - uses: actions/setup-node@v3 + fetch-depth: 0 # Not needed if lastUpdated is not enabled + - name: Setup Node + uses: actions/setup-node@v4 with: - node-version: 16 - cache: yarn + node-version: 20 + cache: yarn # or pnpm / yarn cache-dependency-path: website/yarn.lock - - run: yarn install --frozen-lockfile - - - name: Build - run: yarn docs:build - - - name: Deploy - uses: peaceiris/actions-gh-pages@v3 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Install dependencies + run: yarn install --frozen-lockfile + - name: Build with VitePress + run: | + yarn docs:build + touch docs/.vitepress/dist/.nojekyll + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: website/docs/.vitepress/dist - cname: kernelsu.org # if wanna deploy to custom domain + path: website/docs/.vitepress/dist + + # Deployment job + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + needs: build + runs-on: ubuntu-latest + name: Deploy + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/gki-kernel.yml b/.github/workflows/gki-kernel.yml index feb55d73e1ec..88082738e91b 100644 --- a/.github/workflows/gki-kernel.yml +++ b/.github/workflows/gki-kernel.yml @@ -29,7 +29,7 @@ on: for example: 2021-11 default: 2022-05 patch_path: - required: true + required: false type: string description: > Directory name of .github/patches/ @@ -49,13 +49,15 @@ on: required: false type: boolean default: false + build_lkm: + required: false + type: boolean + default: false secrets: BOOT_SIGN_KEY: required: false CHAT_ID: required: false - CACHE_CHAT_ID: - required: false BOT_TOKEN: required: false MESSAGE_THREAD_ID: @@ -126,16 +128,24 @@ jobs: ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $GKI_ROOT/common/drivers/kernelsu echo "[+] Add KernelSU driver to Makefile" DRIVER_MAKEFILE=$GKI_ROOT/common/drivers/Makefile - grep -q "kernelsu" $DRIVER_MAKEFILE || echo "obj-y += kernelsu/" >> $DRIVER_MAKEFILE - echo "[+] Apply KernelSU patches" - cd $GKI_ROOT/common/ && git apply $GITHUB_WORKSPACE/KernelSU/.github/patches/$PATCH_PATH/*.patch - echo "[+] Patch script/setlocalversion" - sed -i 's/-dirty//g' $GKI_ROOT/common/scripts/setlocalversion + DRIVER_KCONFIG=$GKI_ROOT/common/drivers/Kconfig + grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" + grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" + echo "[+] Apply Compilation Patches" + if [ ! -e build/build.sh ]; then + GLIBC_VERSION=$(ldd --version 2>/dev/null | head -n 1 | awk '{print $NF}') + echo "GLIBC_VERSION: $GLIBC_VERSION" + if [ "$(printf '%s\n' "2.38" "$GLIBC_VERSION" | sort -V | head -n1)" = "2.38" ]; then + echo "Patching resolve_btfids/Makefile" + cd $GKI_ROOT/common/ && sed -i '/\$(Q)\$(MAKE) -C \$(SUBCMD_SRC) OUTPUT=\$(abspath \$(dir \$@))\/ \$(abspath \$@)/s//$(Q)$(MAKE) -C $(SUBCMD_SRC) EXTRA_CFLAGS="$(CFLAGS)" OUTPUT=$(abspath $(dir $@))\/ $(abspath $@)/' tools/bpf/resolve_btfids/Makefile || echo "No patch needed." + fi + fi if [ "$IS_DEBUG_KERNEL" = "true" ]; then echo "[+] Enable debug features for kernel" - echo "ccflags-y += -DCONFIG_KSU_DEBUG" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile + printf "\nccflags-y += -DCONFIG_KSU_DEBUG\n" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile fi + repo status echo "[+] KernelSU setup done." - name: Symbol magic @@ -151,13 +161,50 @@ jobs: - name: Setup ccache if: inputs.use_cache == true - uses: hendrikmuhs/ccache-action@v1.2 + uses: hendrikmuhs/ccache-action@v1 with: key: gki-kernel-aarch64-${{ inputs.version_name }} max-size: 2G save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - - name: Build boot.img + - name: Setup for LKM + if: ${{ inputs.build_lkm == true }} + working-directory: android-kernel + run: | + pip install ast-grep-cli + sudo apt-get install llvm-15 -y + ast-grep -U -p '$$$ check_exports($$$) {$$$}' -r '' common/scripts/mod/modpost.c + ast-grep -U -p 'check_exports($$$);' -r '' common/scripts/mod/modpost.c + sed -i '/config KSU/,/help/{s/default y/default m/}' common/drivers/kernelsu/Kconfig + echo "drivers/kernelsu/kernelsu.ko" >> common/android/gki_aarch64_modules + + # bazel build, android14-5.15, android14-6.1 use bazel + if [ ! -e build/build.sh ]; then + sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found" + if [ -e common/modules.bzl ]; then + sed -i 's/_COMMON_GKI_MODULES_LIST = \[/_COMMON_GKI_MODULES_LIST = \[ "drivers\/kernelsu\/kernelsu.ko",/g' common/modules.bzl + fi + else + TARGET_FILE="build/kernel/build.sh" + if [ ! -e "$TARGET_FILE" ]; then + TARGET_FILE="build/build.sh" + fi + sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' $TARGET_FILE || echo "No unknown symbol in $TARGET_FILE" + sed -i 's/if ! diff -u "\${KERNEL_DIR}\/\${MODULES_ORDER}" "\${OUT_DIR}\/modules\.order"; then/if false; then/g' $TARGET_FILE + sed -i 's@${ROOT_DIR}/build/abi/compare_to_symbol_list@echo@g' $TARGET_FILE + sed -i 's/needs unknown symbol/Dont abort when unknown symbol/g' build/kernel/*.sh || echo "No unknown symbol scripts found" + fi + + - name: Make working directory clean to avoid dirty + working-directory: android-kernel + run: | + rm common/android/abi_gki_protected_exports_* || echo "No protected exports!" + git config --global user.email "bot@kernelsu.org" + git config --global user.name "KernelSUBot" + cd common/ && git add -A && git commit -a -m "Add KernelSU" + repo status + + - name: Build Kernel/LKM working-directory: android-kernel run: | if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then @@ -165,9 +212,9 @@ jobs: export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }} fi if [ -e build/build.sh ]; then - CCACHE="/usr/bin/ccache" LTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh - else - tools/bazel run --lto=thin //common:kernel_aarch64_dist -- --dist_dir=dist + LTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh CC="/usr/bin/ccache clang" + else + tools/bazel run --disk_cache=/home/runner/.cache/bazel --config=fast --config=stamp --lto=thin //common:kernel_aarch64_dist -- --dist_dir=dist fi - name: Prepare artifacts @@ -178,20 +225,34 @@ jobs: OUTDIR=android-kernel/dist fi mkdir output - cp $OUTDIR/Image ./output/ - cp $OUTDIR/Image.lz4 ./output/ - git clone https://github.com/Kernel-SU/AnyKernel3 - rm -rf ./AnyKernel3/.git - cp $OUTDIR/Image ./AnyKernel3/ + if [ "${{ inputs.build_lkm}}" = "true" ]; then + llvm-strip-15 -d $OUTDIR/kernelsu.ko + mv $OUTDIR/kernelsu.ko ./output/${{ inputs.version }}_kernelsu.ko + else + cp $OUTDIR/Image ./output/ + cp $OUTDIR/Image.lz4 ./output/ + git clone https://github.com/Kernel-SU/AnyKernel3 + rm -rf ./AnyKernel3/.git + cp $OUTDIR/Image ./AnyKernel3/ + fi - name: Upload Image and Image.gz - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 + if: ${{ inputs.build_lkm == false }} with: name: Image-${{ inputs.version_name }}_${{ inputs.os_patch_level }} path: ./output/* - name: Upload AnyKernel3 - uses: actions/upload-artifact@v3 + if: ${{ inputs.build_lkm == false }} + uses: actions/upload-artifact@v4 with: name: AnyKernel3-${{ inputs.version_name }}_${{ inputs.os_patch_level }} path: ./AnyKernel3/* + + - name: Upload LKM + uses: actions/upload-artifact@v4 + if: ${{ inputs.build_lkm == true }} + with: + name: ${{ inputs.version }}-lkm + path: ./output/*_kernelsu.ko \ No newline at end of file diff --git a/.github/workflows/ksud.yml b/.github/workflows/ksud.yml index 4b6440ae0ee8..e0fe80a1a6d3 100644 --- a/.github/workflows/ksud.yml +++ b/.github/workflows/ksud.yml @@ -5,32 +5,53 @@ on: target: required: true type: string + os: + required: false + type: string + default: ubuntu-latest + pack_lkm: + required: false + type: boolean + default: true use_cache: required: false type: boolean default: true jobs: build: - runs-on: ubuntu-latest + runs-on: ${{ inputs.os }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - # cross build failed after Rust 1.68, see https://github.com/cross-rs/cross/issues/1222 - - run: rustup default 1.67.0 + + - name: Download artifacts + uses: actions/download-artifact@v4 + + - name: Prepare LKM fies + if: ${{ inputs.pack_lkm }} + run: | + cp android*-lkm/*_kernelsu.ko ./userspace/ksud/bin/aarch64/ + + - name: Setup rustup + run: | + rustup update stable + rustup target add x86_64-apple-darwin + rustup target add aarch64-apple-darwin - uses: Swatinem/rust-cache@v2 with: workspaces: userspace/ksud cache-targets: false - name: Install cross - run: cargo install cross + run: | + cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1 - name: Build ksud - run: cross build --target ${{ inputs.target }} --release --manifest-path ./userspace/ksud/Cargo.toml + run: CROSS_NO_WARNINGS=0 cross build --target ${{ inputs.target }} --release --manifest-path ./userspace/ksud/Cargo.toml - name: Upload ksud artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ksud-${{ inputs.target }} - path: userspace/ksud/target/**/release/ksud + path: userspace/ksud/target/**/release/ksud* diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 38e2d044bc92..a79128e7c53d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,23 +11,48 @@ jobs: secrets: inherit build-a12-kernel: uses: ./.github/workflows/build-kernel-a12.yml + secrets: inherit build-a13-kernel: uses: ./.github/workflows/build-kernel-a13.yml + secrets: inherit + build-a14-kernel: + uses: ./.github/workflows/build-kernel-a14.yml + secrets: inherit + build-a15-kernel: + uses: ./.github/workflows/build-kernel-a15.yml + secrets: inherit build-wsa-kernel: uses: ./.github/workflows/build-kernel-wsa.yml + secrets: inherit build-arcvm-kernel: uses: ./.github/workflows/build-kernel-arcvm.yml + secrets: inherit release: needs: - build-manager - build-a12-kernel - build-a13-kernel + - build-a14-kernel - build-wsa-kernel - build-arcvm-kernel runs-on: ubuntu-latest steps: - name: Download artifacts - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 + - name: Rename ksud + run: | + mkdir -p ksud + for dir in ./ksud-*; do + if [ -d "$dir" ]; then + echo "----- Rename $dir -----" + ksud_platform_name=$(basename "$dir") + find "$dir" -type f -name "ksud" -path "*/release/*" | while read -r ksud_file; do + if [ -f "$ksud_file" ]; then + mv "$ksud_file" "ksud/$ksud_platform_name" + fi + done + fi + done - name: Zip AnyKernel3 run: | for dir in AnyKernel3-*; do @@ -59,11 +84,13 @@ jobs: run: ls -R - name: release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: | manager/*.apk + android*-lkm/*_kernelsu.ko AnyKernel3-*.zip boot-images-*/Image-*/*.img.gz kernel-WSA*.zip kernel-ARCVM*.zip + ksud/ksud-* diff --git a/.github/workflows/wsa-kernel.yml b/.github/workflows/wsa-kernel.yml new file mode 100644 index 000000000000..1f87e4975411 --- /dev/null +++ b/.github/workflows/wsa-kernel.yml @@ -0,0 +1,106 @@ +name: Build Kernel - WSA +on: + workflow_call: + inputs: + arch: + required: true + type: string + description: > + Build arch: x86_64 / arm64 + version: + required: true + type: string + description: > + Build version +jobs: + build: + name: Build WSA-Kernel-${{ inputs.version }}-${{ inputs.arch }} + runs-on: ubuntu-22.04 + env: + CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" + CCACHE_NOHASHDIR: "true" + CCACHE_HARDLINK: "true" + + steps: + - name: Install Build Tools + uses: awalsh128/cache-apt-pkgs-action@v1 + with: + packages: bc bison build-essential flex libelf-dev binutils-aarch64-linux-gnu gcc-aarch64-linux-gnu gzip ccache + version: 1.0 + + - name: Cache LLVM + id: cache-llvm + uses: actions/cache@v4 + with: + path: ./llvm + key: llvm-12.0.1 + + - name: Setup LLVM + uses: KyleMayes/install-llvm-action@v1 + with: + version: "12.0.1" + force-version: true + ubuntu-version: "16.04" + cached: ${{ steps.cache-llvm.outputs.cache-hit }} + + - name: Checkout KernelSU + uses: actions/checkout@v4 + with: + path: KernelSU + fetch-depth: 0 + + - name: Setup kernel source + uses: actions/checkout@v4 + with: + repository: microsoft/WSA-Linux-Kernel + ref: android-lts/latte-2/${{ inputs.version }} + path: WSA-Linux-Kernel + + - name: Setup Ccache + uses: hendrikmuhs/ccache-action@v1 + with: + key: WSA-Kernel-${{ inputs.version }}-${{ inputs.arch }} + save: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + max-size: 2G + + - name: Setup KernelSU + working-directory: WSA-Linux-Kernel + run: | + echo "[+] KernelSU setup" + KERNEL_ROOT=$GITHUB_WORKSPACE/WSA-Linux-Kernel + echo "[+] KERNEL_ROOT: $KERNEL_ROOT" + echo "[+] Copy KernelSU driver to $KERNEL_ROOT/drivers" + ln -sf $GITHUB_WORKSPACE/KernelSU/kernel $KERNEL_ROOT/drivers/kernelsu + echo "[+] Add KernelSU driver to Makefile" + DRIVER_MAKEFILE=$KERNEL_ROOT/drivers/Makefile + DRIVER_KCONFIG=$KERNEL_ROOT/drivers/Kconfig + grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" + grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" + echo "[+] Apply KernelSU patches" + cd $KERNEL_ROOT && git apply $GITHUB_WORKSPACE/KernelSU/.github/patches/5.15/*.patch || echo "[-] No patch found" + echo "[+] KernelSU setup done." + cd $GITHUB_WORKSPACE/KernelSU + VERSION=$(($(git rev-list --count HEAD) + 10200)) + echo "VERSION: $VERSION" + echo "kernelsu_version=$VERSION" >> $GITHUB_ENV + + - name: Build Kernel + working-directory: WSA-Linux-Kernel + run: | + if [ ! -z ${{ vars.EXPECTED_SIZE }} ] && [ ! -z ${{ vars.EXPECTED_HASH }} ]; then + export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }} + export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }} + fi + declare -A ARCH_MAP=(["x86_64"]="x64" ["arm64"]="arm64") + cp configs/wsa/config-wsa-${ARCH_MAP[${{ inputs.arch }}]} .config + make olddefconfig + declare -A FILE_NAME=(["x86_64"]="bzImage" ["arm64"]="Image") + make -j`nproc` LLVM=1 ARCH=${{ inputs.arch }} $(if [ "${{ inputs.arch }}" == "arm64" ]; then echo CROSS_COMPILE=aarch64-linux-gnu; fi) ${FILE_NAME[${{ inputs.arch }}]} CCACHE="/usr/bin/ccache" + declare -A ARCH_MAP_FILE=(["x86_64"]="x86" ["arm64"]="arm64") + echo "file_path=WSA-Linux-Kernel/arch/${ARCH_MAP_FILE[${{ inputs.arch }}]}/boot/${FILE_NAME[${{ inputs.arch }}]}" >> $GITHUB_ENV + + - name: Upload kernel-${{ inputs.arch }}-${{ inputs.version }} + uses: actions/upload-artifact@v4 + with: + name: kernel-WSA-${{ inputs.arch }}-${{ inputs.version }} + path: "${{ env.file_path }}" \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 8ee21dbf946c..3e99c1e8cd33 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,14 +1,21 @@ - -**English** | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [Polski](README_PL.md) | [Portuguese-Brazil](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_iw.md) | [हिंदी](README_IN.md) +**English** | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU +logo + A Kernel-based root solution for Android devices. +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + ## Features 1. Kernel-based `su` and root access management. -2. Module system based on overlayfs. +2. Module system based on [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS). 3. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock up the root power in a cage. ## Compatibility State @@ -27,16 +34,20 @@ Currently, only `arm64-v8a` and `x86_64` are supported. ## Translation -To help translate KernelSU or improve existing translations, please use [Weblate](https://hosted.weblate.org/engage/kernelsu/). +To help translate KernelSU or improve existing translations, please use [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR of Manager's translation is no longer accepted, because it will conflict with Weblate. ## Discussion - Telegram: [@KernelSU](https://t.me/KernelSU) +## Security + +For information on reporting security vulnerabilities in KernelSU, see [SECURITY.md](/SECURITY.md). + ## License -- Files under the `kernel` directory are [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -- All other parts except the `kernel` directory are [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- Files under the `kernel` directory are [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). +- All other parts except the `kernel` directory are [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Credits diff --git a/docs/README_CN.md b/docs/README_CN.md index ebd8252ef788..17e44e942763 100644 --- a/docs/README_CN.md +++ b/docs/README_CN.md @@ -1,14 +1,22 @@ -[English](README.md) | [Español](README_ES.md) | **简体中文** | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [Polski](README_PL.md) | [Portuguese-Brazil](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_iw.md) | [हिंदी](README_IN.md) +[English](README.md) | [Español](README_ES.md) | **简体中文** | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU +logo + 一个 Android 上基于内核的 root 方案。 +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + ## 特性 -- 基于内核的 su 和权限管理。 -- 基于 overlayfs 的模块系统。 -- [App Profile](https://kernelsu.org/guide/app-profile.html): 把 Root 权限关进笼子里。 +- 基于内核的 `su` 和权限管理。 +- 基于 [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) 的模块系统。 +- [App Profile](https://kernelsu.org/zh_CN/guide/app-profile.html): 把 Root 权限关进笼子里。 ## 兼容状态 @@ -16,25 +24,30 @@ KernelSU 官方支持 GKI 2.0 的设备(内核版本5.10以上);旧内核 WSA, ChromeOS 和运行在容器上的 Android 也可以与 KernelSU 一起工作。 -目前支持架构 : `arm64-v8a` 和 `x86_64` +目前支持架构 : `arm64-v8a` 和 `x86_64`。 ## 使用方法 - [安装教程](https://kernelsu.org/zh_CN/guide/installation.html) - [如何构建?](https://kernelsu.org/zh_CN/guide/how-to-build.html) +- [官方网站](https://kernelsu.org/zh_CN/) ## 参与翻译 -要将 KernelSU 翻译成您的语言,或完善现有的翻译,请使用 [Weblate](https://hosted.weblate.org/engage/kernelsu/)。 +要将 KernelSU 翻译成您的语言,或完善现有的翻译,请使用 [Weblate](https://hosted.weblate.org/engage/kernelsu/)。现已不再接受有关管理器翻译的PR,因为这会与Weblate冲突。 ## 讨论 - Telegram: [@KernelSU](https://t.me/KernelSU) +## 安全性 + +有关报告 KernelSU 安全漏洞的信息,请参阅 [SECURITY.md](/SECURITY.md)。 + ## 许可证 -- 目录 `kernel` 下所有文件为 [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -- 除 `kernel` 目录的其他部分均为 [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- 目录 `kernel` 下所有文件为 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。 +- 除 `kernel` 目录的其他部分均为 [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)。 ## 鸣谢 diff --git a/docs/README_ES.md b/docs/README_ES.md index 01dd3d62cff6..42a4828fa0fa 100644 --- a/docs/README_ES.md +++ b/docs/README_ES.md @@ -1,47 +1,56 @@ -[ 🇬🇧 English](README.md) | 🇪🇸 **Español** | [🇨🇳 简体中文](README_CN.md) | [🇹🇼 繁體中文](README_TW.md) | [ 🇯🇵 日本語](README_JP.md) | [🇵🇱 Polski](README_PL.md) | [🇧🇷 Portuguese-Brazil](README_PT-BR.md) | [🇹🇷 Türkçe](README_TR.md) | [🇷🇺Русский](README_RU.md) | [🇻🇳Tiếng Việt](README_VI.md) | [ɪᴅ indonesia](README_ID.md) | [עברית](README_iw.md) | [🇮🇳हिंदी](README_IN.md) +[English](README.md) | **Español** | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) -
- -
- KernelSU -
- Una solución root basada en el kernel para dispositivos Android. -
-
+# KernelSU -## 🚀 Características +logo -**1.** Binario `su` basado en el kernel y gestión de acceso root.
-**2.** Sistema de módulos basado en **OverlayFS**. +Una solución root basada en el kernel para dispositivos Android. -## ✅ Estado de compatibilidad +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localización-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Seguir-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/Licencia-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + +## Características + +1. Binario `su` basado en el kernel y gestión de acceso root. +2. Sistema de módulos basado en [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS). + +## Estado de compatibilidad **KernelSU** soporta de forma oficial dispositivos Android con **GKI 2.0** (a partir de la versión **5.10** del kernel). Los kernels antiguos (a partir de la versión **4.14**) también son compatibles, pero necesitas compilarlos por tu cuenta. -El **Subsistema de Windows para Android (WSA)** e implementaciones de Android basadas en contenedores, como **Waydroid**, también deberían funcionar con **KernelSU** integrado. +Con esto, WSA, ChromeOS y Android basado en contenedores están todos compatibles. -Actualmente se soportan las siguientes **ABIs**: `arm64-v8a`; `x86_64`. +Actualmente, solo se admiten las arquitecturas `arm64-v8a` y `x86_64`. -## 📖 Uso +## Uso -[¿Cómo instalarlo?](https://kernelsu.org/guide/installation.html) +- [¿Cómo instalarlo?](https://kernelsu.org/guide/installation.html) +- [¿Cómo compilarlo?](https://kernelsu.org/guide/how-to-build.html) +- [Site oficial](https://kernelsu.org/) -## 🔨 Compilación +## Traducción -[¿Cómo compilarlo?](https://kernelsu.org/guide/how-to-build.html) +Para ayudar a traducir KernelSU o mejorar las traducciones existentes, utilice [Weblate](https://hosted.weblate.org/engage/kernelsu/). Ya no se aceptan PR de la traducción de Manager porque entrará en conflicto con Weblate. -## 💬 Discusión +## Discusión - Telegram: [@KernelSU](https://t.me/KernelSU) -## ⚖️ Licencia +## Seguridad + +Para obtener información sobre cómo informar vulnerabilidades de seguridad en KernelSU, consulte [SECURITY.md](/SECURITY.md). + +## Licencia -- Los archivos bajo el directorio `kernel` están licenciados bajo [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). -- Todas las demás partes, a excepción del directorio `kernel`, están licenciados bajo [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html). +- Los archivos bajo el directorio `kernel` están licenciados bajo [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). +- Todas las demás partes, a excepción del directorio `kernel`, están licenciados bajo [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html). -## 👥 Créditos +## Créditos -- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): la idea de **KernelSU**. -- [genuine](https://github.com/brevent/genuine/): la validación del **esquema de firmas APK v2**. +- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): la idea de KernelSU. +- [Magisk](https://github.com/topjohnwu/Magisk): la poderosa herramienta root. +- [genuine](https://github.com/brevent/genuine/): validación de firma apk v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): algunas habilidades de rootkit. -- [Magisk](https://github.com/topjohnwu/Magisk): la implementación de la **política de SELinux (SEPolicy)**. diff --git a/docs/README_ID.md b/docs/README_ID.md index a8aa00375838..33bfa733fefb 100644 --- a/docs/README_ID.md +++ b/docs/README_ID.md @@ -1,13 +1,21 @@ -[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [Polski](README_PL.md) | [Portugis-Brasil](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | **Indonesia** | [עברית](README_iw.md) | [हिंदी](README_IN.md) +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | **Indonesia** | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU +logo + Solusi root berbasis Kernel untuk perangkat Android. +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + ## Fitur 1. Manajemen akses root dan `su` berbasis kernel. -2. Sistem modul berdasarkan overlayfs. +2. Sistem modul berdasarkan [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS). 3. [Profil Aplikasi](https://kernelsu.org/guide/app-profile.html): Kunci daya root di dalam sangkar. ## Status Kompatibilitas @@ -20,9 +28,9 @@ Dan ABI yang didukung saat ini adalah: `arm64-v8a` dan `x86_64` ## Penggunaan -- [Petunjuk Instalasi](https://kernelsu.org/guide/installation.html) -- [Bagaimana cara membuat?](https://kernelsu.org/guide/how-to-build.html) -- [Situs Web Resmi](https://kernelsu.org/) +- [Petunjuk Instalasi](https://kernelsu.org/id_ID/guide/installation.html) +- [Bagaimana cara membuat?](https://kernelsu.org/id_ID/guide/how-to-build.html) +- [Situs Web Resmi](https://kernelsu.org/id_ID/) ## Terjemahan @@ -34,8 +42,8 @@ Untuk menerjemahkan KernelSU ke dalam bahasa Anda atau menyempurnakan terjemahan ## Lisensi -- File di bawah direktori `kernel` adalah [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -- Semua bagian lain kecuali direktori `kernel` adalah [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- File di bawah direktori `kernel` adalah [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). +- Semua bagian lain kecuali direktori `kernel` adalah [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Kredit diff --git a/docs/README_IN.md b/docs/README_IN.md index 150eceef7d26..392a99041613 100644 --- a/docs/README_IN.md +++ b/docs/README_IN.md @@ -1,19 +1,21 @@ +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | **हिंदी** | [Italiano](README_IT.md) -[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [Polski](README_PL.md) | [Portuguese-Brazil](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_iw.md) | **हिंदी** +# KernelSU -
- -
- KernelSU -
- Android उपकरणों के लिए कर्नेल-आधारित रूट समाधान। -
-
+logo + +Android उपकरणों के लिए कर्नेल-आधारित रूट समाधान। + +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## विशेषताएँ 1. कर्नेल-आधारित `su` और रूट एक्सेस प्रबंधन। -2. Overlayfs पर आधारित मॉड्यूल प्रणाली। +2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) पर आधारित मॉड्यूल प्रणाली। 3. [App Profile](https://kernelsu.org/guide/app-profile.html): Root शक्ति को पिंजरे में बंद कर दो। ## अनुकूलता अवस्था @@ -40,8 +42,8 @@ KernelSU का अनुवाद करने या मौजूदा अन ## लाइसेंस -- `Kernel` निर्देशिका के अंतर्गत फ़ाइलें हैं [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -- `Kernel` निर्देशिका को छोड़कर अन्य सभी भाग हैं [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- `Kernel` निर्देशिका के अंतर्गत फ़ाइलें हैं [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +- `Kernel` निर्देशिका को छोड़कर अन्य सभी भाग हैं [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html) ## आभार सूची diff --git a/docs/README_IT.md b/docs/README_IT.md new file mode 100644 index 000000000000..54216b65ff62 --- /dev/null +++ b/docs/README_IT.md @@ -0,0 +1,58 @@ +[English](REAME.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | **Italiano** + +# KernelSU + +logo + +Una soluzione per il root basata sul kernel per i dispositivi Android. + +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Canale Telegraml](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![Licenza componenti kernel: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![Licenza elementi non kern](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + +## Funzionalità + +1. `su` e accesso root basato sul kernel. +2. Sistema di moduli per la modifica del sistema basato su [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS). +3. [App profile](https://kernelsu.org/guide/app-profile.html): Limita i poteri dell'accesso root a permessi specifici. + +## Compatibilità + +KernelSU supporta ufficialmente i dispositivi Android GKI 2.0 (kernel 5.10 o superiore). I kernel precedenti (kernel 4.14+) sono anche compatibili, ma il kernel deve essere compilato manualmente. + +Questo implica che WSA, ChromeOS e tutti le varianti di Android basate su container e virtualizzazione sono supportate. + +Allo stato attuale solo le architetture a 64-bit ARM (arm64-v8a) e x86 (x86_64) sono supportate. + +## Utilizzo + +- [Istruzioni per l'installazione](https://kernelsu.org/guide/installation.html) +- [Come compilare manualmente?](https://kernelsu.org/guide/how-to-build.html) +- [Sito web ufficiale](https://kernelsu.org/) + +## Traduzioni + +Per aiutare a tradurre KernelSU o migliorare le traduzioni esistenti, si è pregati di utilizzare +To help translate KernelSU or improve existing translations, please use [Weblate](https://hosted.weblate.org/engage/kernelsu/). Le richieste di pull delle traduzioni del manager non saranno più accettate perché sarebbero in conflitto con Weblate. + +## Discussione + +- Telegram: [@KernelSU](https://t.me/KernelSU) + +## Securezza + +Per informazioni riguardo la segnalazione di vulnerabilità di sicurezza per KernelSU, leggi [SECURITY.md](/SECURITY.md). + +## Licenza + +- I file nella cartella `kernel` sono forniti secondo la licenza [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). +- Tutte le altre parti, ad eccezione della certella `kernel`, seguono la licenza [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html). + +## Riconoscimenti e attribuzioni + +- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): l'idea alla base di KernelSU. +- [Magisk](https://github.com/topjohnwu/Magisk): la potente utilità per il root. +- [genuine](https://github.com/brevent/genuine/): verifica della firma apk v2. +- [Diamorphine](https://github.com/m0nad/Diamorphine): alcune capacità di rootkit. diff --git a/docs/README_iw.md b/docs/README_IW.md similarity index 57% rename from docs/README_iw.md rename to docs/README_IW.md index 7884cf9a1db1..233c3b61ce11 100644 --- a/docs/README_iw.md +++ b/docs/README_IW.md @@ -1,13 +1,21 @@ -[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [Polski](README_PL.md) | [Portuguese-Brazil](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | **עברית** | [हिंदी](README_IN.md) +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | **עברית** | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU +logo + פתרון לניהול root מבוסס על Kernel עבור מכשירי Android. +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + ## תכונות 1. ניהול root ו־`su` מבוססים על Kernel. -2. מערכת מודולים מבוססת overlayfs. +2. מערכת מודולים מבוססת [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS). 3. [פרופיל אפליקציה](https://kernelsu.org/guide/app-profile.html): נעילת גישת root בכלוב. ## מצב תאימות @@ -34,12 +42,12 @@ KernelSU תומך במכשירי Android GKI 2.0 (kernel 5.10+) באופן רש ## רשיון -- קבצים תחת הספרייה `kernel` מוגנים על פי [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -- כל החלקים האחרים, למעט הספרייה `kernel`, מוגנים על פי [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- קבצים תחת הספרייה `kernel` מוגנים על פי [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). +- כל החלקים האחרים, למעט הספרייה `kernel`, מוגנים על פי [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## קרדיטים - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): הרעיון של KernelSU. - [Magisk](https://github.com/topjohnwu/Magisk): הכלי הסופר חזק לניהול root. - [genuine](https://github.com/brevent/genuine/): אימות חתימת apk v2. -- [Diamorphine](https://github.com/m0nad/Diamorphine): כמה יכולות רוט. \ No newline at end of file +- [Diamorphine](https://github.com/m0nad/Diamorphine): כמה יכולות רוט. diff --git a/docs/README_JP.md b/docs/README_JP.md index 98437adff9a9..04ec0cfc3873 100644 --- a/docs/README_JP.md +++ b/docs/README_JP.md @@ -1,16 +1,23 @@ -[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | **日本語** | [Polski](README_PL.md) | [Portuguese-Brazil](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_iw.md) | [हिंदी](README_IN.md) +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | **日本語** | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU +logo + Android におけるカーネルベースの root ソリューションです。 +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + ## 特徴 -1. カーネルベースの `su` と権限管理 -2. OverlayFS に基づくモジュールシステム +1. カーネルベースの `su` と権限管理。 +2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) に基づくモジュールシステム。 3. [アプリのプロファイル](https://kernelsu.org/guide/app-profile.html): root の権限をケージ内に閉じ込めます。 - ## 対応状況 KernelSU は GKI 2.0 デバイス(カーネルバージョン 5.10 以上)を公式にサポートしています。古いカーネル(4.14以上)とも互換性がありますが、自分でカーネルをビルドする必要があります。 @@ -23,11 +30,11 @@ WSA 、ChromeOS とコンテナ上で動作する Android でも KernelSU を統 - [インストール方法はこちら](https://kernelsu.org/ja_JP/guide/installation.html) - [ビルド方法はこちら](https://kernelsu.org/guide/how-to-build.html) -- [公式サイト](https://kernelsu.org) +- [公式サイト](https://kernelsu.org/ja_JP/) ## 翻訳 -KernelSU をあなたの言語に翻訳するか、既存の翻訳を改善するには、[Weblate](https://hosted.weblate.org/engage/kernelsu/) を使用してください。 +KernelSU をあなたの言語に翻訳するか、既存の翻訳を改善するには、[Weblate](https://hosted.weblate.org/engage/kernelsu/) を使用してください。Manager翻訳した PR は、Weblate と競合するため受け入れられなくなりました。 ## ディスカッション @@ -35,13 +42,12 @@ KernelSU をあなたの言語に翻訳するか、既存の翻訳を改善す ## ライセンス -- `kernel` ディレクトリの下にあるすべてのファイル: [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -- `kernel` ディレクトリ以外のすべてのファイル: [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- `kernel` ディレクトリの下にあるすべてのファイル: [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。 +- `kernel` ディレクトリ以外のすべてのファイル: [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)。 ## クレジット -- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU のアイデア元 -- [Magisk](https://github.com/topjohnwu/Magisk):強力な root ツール -- [genuine](https://github.com/brevent/genuine/):apk v2 の署名検証 -- [Diamorphine](https://github.com/m0nad/Diamorphine): rootkit のスキル - +- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU のアイデア元。 +- [Magisk](https://github.com/topjohnwu/Magisk):強力な root ツール。 +- [genuine](https://github.com/brevent/genuine/):apk v2 の署名検証。 +- [Diamorphine](https://github.com/m0nad/Diamorphine): rootkit のスキル。 diff --git a/docs/README_KR.md b/docs/README_KR.md new file mode 100644 index 000000000000..ccb8f8ac4dc2 --- /dev/null +++ b/docs/README_KR.md @@ -0,0 +1,57 @@ +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | **한국어** | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) + +# KernelSU + +logo + +안드로이드 기기에서 사용되는 커널 기반 루팅 솔루션입니다. + +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + +## 기능들 + +1. 커널 기반 `su` 및 루트 액세스 관리. +2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) 기반 모듈 시스템. +3. [App Profile](https://kernelsu.org/guide/app-profile.html): 루트 권한을 케이지에 가둡니다. + +## 호환 상태 + +KernelSU는 공식적으로 안드로이드 GKI 2.0 디바이스(커널 5.10 이상)를 지원합니다. 오래된 커널(4.14 이상)도 사용할 수 있지만, 커널을 수동으로 빌드해야 합니다. + +KernelSU는 WSA, ChromeOS, 컨테이너 기반 안드로이드 모두를 지원합니다. + +현재는 `arm64-v8a`와 `x86_64`만 지원됩니다. + +## 사용 방법 + +- [설치 방법](https://kernelsu.org/guide/installation.html) +- [어떻게 빌드하나요?](https://kernelsu.org/guide/how-to-build.html) +- [공식 웹사이트](https://kernelsu.org/) + +## 번역 + +KernelSU 번역을 돕거나 기존 번역을 개선하려면 [Weblate](https://hosted.weblate.org/engage/kernelsu/)를 이용해 주세요. 매니저의 번역은 Weblate와 충돌할 수 있으므로 더 이상 허용되지 않습니다. + +## 토론 + +- 텔레그램: [@KernelSU](https://t.me/KernelSU) + +## 보안 + +KernelSU의 보안 취약점 보고에 대한 자세한 내용은 [SECURITY.md](/SECURITY.md)를 참조하세요. + +## 저작권 + +- `kernel` 디렉터리 아래의 파일은 [GPL-2.0 전용](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)입니다. +- `kernel` 디렉토리를 제외한 다른 모든 부분은 [GPL-3.0-이상](https://www.gnu.org/licenses/gpl-3.0.html)입니다. + +## 크래딧 + +- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU의 아이디어. +- [Magisk](https://github.com/topjohnwu/Magisk): 강력한 루팅 도구. +- [genuine](https://github.com/brevent/genuine/): apk v2 서명 유효성 검사. +- [Diamorphine](https://github.com/m0nad/Diamorphine): 일부 rootkit 스킬. diff --git a/docs/README_PL.md b/docs/README_PL.md index 50812c9a8b16..f5cfc8defc7a 100644 --- a/docs/README_PL.md +++ b/docs/README_PL.md @@ -1,13 +1,21 @@ -[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | **Polski** | [Portuguese-Brazil](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_iw.md) | [हिंदी](README_IN.md) +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | **Polski** | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU +logo + Rozwiązanie root oparte na jądrze dla urządzeń z systemem Android. +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + ## Cechy 1. Oparte na jądrze `su` i zarządzanie dostępem roota. -2. System modułów oparty na overlayfs. +2. System modułów oparty na [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS). ## Kompatybilność @@ -19,24 +27,29 @@ Aktualnie obsługiwane ABI to : `arm64-v8a` i `x86_64`. ## Użycie -[Instalacja](https://kernelsu.org/guide/installation.html) +- [Instalacja](https://kernelsu.org/guide/installation.html) +- [Jak skompilować?](https://kernelsu.org/guide/how-to-build.html) -## Kompilacja +## Tłumaczenie -[Jak skompilować?](https://kernelsu.org/guide/how-to-build.html) +Aby pomóc w tłumaczeniu KernelSU lub ulepszyć istniejące tłumaczenia, użyj [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR tłumaczenia Managera nie jest już akceptowany, ponieważ będzie kolidował z Weblate. ## Dyskusja - Telegram: [@KernelSU](https://t.me/KernelSU) +## Bezpieczeństwo + +Informacje na temat zgłaszania luk w zabezpieczeniach w KernelSU można znaleźć w pliku [SECURITY.md](/SECURITY.md). + ## Licencja -- Pliki w katalogu `kernel` są na licencji [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -- Wszystkie inne części poza katalogiem `kernel` są na licencji [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- Pliki w katalogu `kernel` są na licencji [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). +- Wszystkie inne części poza katalogiem `kernel` są na licencji [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Podziękowania - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): pomysłodawca KernelSU. +- [Magisk](https://github.com/topjohnwu/Magisk): implementacja sepolicy. - [genuine](https://github.com/brevent/genuine/): walidacja podpisu apk v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): cenna znajomość rootkitów. -- [Magisk](https://github.com/topjohnwu/Magisk): implementacja sepolicy. diff --git a/docs/README_PT-BR.md b/docs/README_PT-BR.md index d0c8ee4f7dc2..c91737c864ae 100644 --- a/docs/README_PT-BR.md +++ b/docs/README_PT-BR.md @@ -1,18 +1,24 @@ -[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [Polski](README_PL.md) | **Português (Brasil)** | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_iw.md) | [हिंदी](README_IN.md) +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | **Português (Brasil)** | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU +logo + Uma solução root baseada em kernel para dispositivos Android. +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localização-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Seguir-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/Licença-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + ## Características 1. `su` e gerenciamento de acesso root baseado em kernel. - -2. Sistema modular baseado em overlayfs. - +2. Sistema modular baseado em [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS). 3. [Perfil do Aplicativo](https://kernelsu.org/pt_BR/guide/app-profile.html): Tranque o poder root em uma gaiola. -## Estado de Compatibilidade +## Estado de compatibilidade O KernelSU oferece suporte oficial a dispositivos Android GKI 2.0 (kernel 5.10+). Kernels mais antigos (4.14+) também são compatíveis, mas o kernel terá que ser construído manualmente. @@ -21,22 +27,27 @@ Com isso, WSA, ChromeOS e Android baseado em contêiner são todos suportados. Atualmente, apenas `arm64-v8a` e `x86_64` são suportados. ## Uso + - [Instalação](https://kernelsu.org/pt_BR/guide/installation.html) - - [Como construir o KernelSU?](https://kernelsu.org/pt_BR/guide/how-to-build.html) + - [Como compilar o KernelSU?](https://kernelsu.org/pt_BR/guide/how-to-build.html) - [Site oficial](https://kernelsu.org/pt_BR/) ## Tradução -Para ajudar a traduzir o KernelSU ou melhorar as traduções existentes, use o [Weblate](https://hosted.weblate.org/engage/kernelsu/), por favor. + +Para contribuir com a tradução do KernelSU ou aprimorar traduções existentes, por favor, utilize o [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR para a tradução do Gerenciador não são mais aceitas, pois podem entrar em conflito com o Weblate. ## Discussão - Telegram: [@KernelSU](https://t.me/KernelSU) -## Licença +## Segurança -- Os arquivos no diretório `kernel` são [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +Para obter informações sobre como relatar vulnerabilidades de segurança do KernelSU, consulte [SECURITY.md](/SECURITY.md). + +## Licença -- Todas as outras partes, exceto o diretório `kernel` são [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- Os arquivos no diretório `kernel` são [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). +- Todas as outras partes, exceto o diretório `kernel` são [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Créditos diff --git a/docs/README_RU.md b/docs/README_RU.md index 5538497f61da..f3b69643ad17 100644 --- a/docs/README_RU.md +++ b/docs/README_RU.md @@ -1,13 +1,22 @@ -[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [Polski](README_PL.md) | [Portuguese-Brazil](README_PT-BR.md) | [Türkçe](README_TR.md) | **Русский** | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_iw.md) | [हिंदी](README_IN.md) +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | **Русский** | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU +logo + Решение на основе ядра root для Android-устройств. +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + ## Особенности 1. Управление `su` и root-доступом на основе ядра. -2. Система модулей на основе overlayfs. +2. Система модулей на основе [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS). +3. [Профиль приложений](https://kernelsu.org/ru_RU/guide/app-profile.html): Запри корневую силу в клетке. ## Совместимость @@ -19,11 +28,9 @@ WSA и Android на основе контейнеров также должны ## Использование -[Установка](https://kernelsu.org/ru_RU/guide/installation.html) - -## Сборка - -[Как собрать?](https://kernelsu.org/ru_RU/guide/how-to-build.html) +- [Установка](https://kernelsu.org/ru_RU/guide/installation.html) +- [Как собрать?](https://kernelsu.org/ru_RU/guide/how-to-build.html) +- [официальный сайт](https://kernelsu.org/ru_RU/) ## Обсуждение @@ -31,12 +38,12 @@ WSA и Android на основе контейнеров также должны ## Лицензия -- Файлы в директории `kernel` - [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -- Все остальные части, кроме директории `kernel` - [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- Файлы в директории `kernel` [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). +- Все остальные части, кроме директории `kernel` [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Благодарности - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): идея KernelSU. +- [Magisk](https://github.com/topjohnwu/Magisk): реализация sepolicy. - [genuine](https://github.com/brevent/genuine/): проверка подписи apk v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): некоторые навыки руткита. -- [Magisk](https://github.com/topjohnwu/Magisk): реализация sepolicy. diff --git a/docs/README_TR.md b/docs/README_TR.md index 944163d0f8a4..223fcadf978a 100644 --- a/docs/README_TR.md +++ b/docs/README_TR.md @@ -1,45 +1,57 @@ -[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [Polski](README_PL.md) | [Portuguese-Brazil](README_PT-BR.md) | **Türkçe** | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_iw.md) | [हिंदी](README_IN.md) +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | **Türkçe** | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU -Android cihazlar için kernel tabanlı bir root çözümü. +logo + +Android cihazlar için kernel tabanlı root çözümü. + +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## Özellikler 1. Kernel-tabanlı `su` ve root erişimi yönetimi. -2. Overlayfs'ye dayalı modül sistemi. +2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS)'ye dayalı modül sistemi. 3. [Uygulama profili](https://kernelsu.org/guide/app-profile.html): Root gücünü bir kafese kapatın. ## Uyumluluk Durumu -KernelSU resmi olarak Android GKI 2.0 cihazlarını ( 5.10+ kernelli) destekler, eski kernellerle de (4.14+) uyumludur, ancak kerneli kendinizin inşaa etmesi gerekir. +KernelSU resmi olarak Android GKI 2.0 cihazlarını (5.10+ kernelli) destekler, eski kernellerle de (4.14+) uyumludur, ancak kerneli kendinizin derlemeniz gerekir. -WSA ve konteyner tabanlı Android'in de, KernelSU ile entegre olarak da çalışması gerekmektedir. +Bununla birlikte; WSA, ChromeOS ve konteyner tabanlı Android'in tamamı desteklenmektedir. -Ve desteklenen mevcut ABI'ler : `arm64-v8a` ve `x86_64` +Şimdilik sadece `arm64-v8a` ve `x86_64` desteklenmektedir. ## Kullanım -- [Yükleme](https://kernelsu.org/guide/installation.html) -- [Nasıl inşa edilir?](https://kernelsu.org/guide/how-to-build.html) +- [Yükleme yönergeleri](https://kernelsu.org/guide/installation.html) +- [Nasıl derlenir?](https://kernelsu.org/guide/how-to-build.html) - [Resmi WEB sitesi](https://kernelsu.org/) ## Çeviri -KernelSU'yu kendi dilinize çevirmek veya varolan bir çeviriyi geliştirmek istiyorsanız, lütfen [Weblate](https://hosted.weblate.org/engage/kernelsu/)'i kullanın. +KernelSU'nun başka dillere çevrilmesine veya mevcut çevirilerin iyileştirilmesine yardımcı olmak için lütfen [Weblate](https://hosted.weblate.org/engage/kernelsu/) kullanın. Yönetici uygulamasının PR ile çevirisi, Weblate ile çakışacağından artık kabul edilmeyecektir. ## Tartışma - Telegram: [@KernelSU](https://t.me/KernelSU) +## Güvenlik + +KernelSU'daki güvenlik açıklarını bildirme hakkında bilgi için, bkz [SECURITY.md](/SECURITY.md). + ## Lisans -- `kernel` klasöründeki dosyalar [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) lisansı altındadır. -- `kernel` klasörü dışındaki bütün diğer bölümler [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) lisansı altındadır. +- `kernel` klasöründeki dosyalar [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) lisansı altındadır. +- `kernel` klasörü dışındaki bütün diğer bölümler [GPL-3-veya-sonraki](https://www.gnu.org/licenses/gpl-3.0.html) lisansı altındadır. ## Krediler - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU fikri. - [Magisk](https://github.com/topjohnwu/Magisk): güçlü root aracı. -- [genuine](https://github.com/brevent/genuine/): apk v2 imza doğrulama. +- [genuine](https://github.com/brevent/genuine/): apk v2 imza doğrulaması. - [Diamorphine](https://github.com/m0nad/Diamorphine): bazı rootkit becerileri. diff --git a/docs/README_TW.md b/docs/README_TW.md index 0d19eb888ddf..df335a5aec61 100644 --- a/docs/README_TW.md +++ b/docs/README_TW.md @@ -1,42 +1,57 @@ -[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | **繁體中文** | [日本語](README_JP.md) | [Polski](README_PL.md) | [Portuguese-Brazil](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_iw.md) | [हिंदी](README_IN.md) +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | **繁體中文** | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU -一個基於核心的 Android 裝置 Root 解決方案 +標誌 -## 功能 +一套基於 Android 裝置核心的 Root 解決方案。 -- 基於核心的 Su 和 Root 存取權管理。 -- 基於 Overlayfs 的模組系統。 +[![最新版本](https://img.shields.io/github/v/release/tiann/KernelSU?label=%e7%99%bc%e8%a1%8c%e7%89%88%e6%9c%ac&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/%e6%9c%ac%e5%9c%9f%e5%8c%96%e7%bf%bb%e8%ad%af-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![頻道](https://img.shields.io/badge/%e8%bf%bd%e8%b9%a4-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![授權條款:《GPL v2》](https://img.shields.io/badge/%e6%8e%88%e6%ac%8a%e6%a2%9d%e6%ac%be-%E3%80%8AGPL%20v2%E3%80%8B-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub 授權條款](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) -## 相容性狀態 +## 特色功能 -KernelSU 官方支援 Android GKI 2.0 的裝置 (核心版本 5.10+);舊版核心同樣相容 (最低 4.14+),但需要自行編譯核心。 +1. 以核心內 `su` 管理 Root 存取。 +2. 以 [OverlayFS](https://zh.wikipedia.org/zh-tw/OverlayFS) 運作模組系統。 +3. [App Profile](https://kernelsu.org/zh_TW/guide/app-profile.html):使 Root 掌握的生殺大權受制於此。 -WSA 和執行在容器中的 Android 也可以與 KernelSU 一同運作。 +## 相容事態 -目前支援架構:`arm64-v8a` 和 `x86_64` +理論上採以 Android GKI 2.0 的裝置(核心版本 5.10+),皆受 KernelSU 支援;採以老舊核心版本(4.14+)的裝置在手動建置核心後,亦受支援。 -## 使用方法 +另可在 WSA、ChromeOS 一類的容器式 Android 中運作。 -[安裝教學](https://kernelsu.org/zh_TW/guide/installation.html) +目前僅適用 `arm64-v8a` 以及 `x86_64` 架構。 -## 建置 +## 使用手冊 -[如何建置?](https://kernelsu.org/zh_TW/guide/how-to-build.html) +- [安裝教學](https://kernelsu.org/zh_TW/guide/installation.html) +- [如何建置 KernelSU?](https://kernelsu.org/zh_TW/guide/how-to-build.html) +- [官方網站](https://kernelsu.org/zh_TW/) -### 討論 +## 多語翻譯 + +欲要協助 KernelSU 邁向多語化,抑或改進翻譯品質,請前往 [Weblate](https://hosted.weblate.org/engage/kernelsu/) 進行翻譯。為避免與 Weblate 上的翻譯發生衝突,現已不再受理翻譯相關的管理工具 PR。 + +## 綜合討論 - Telegram:[@KernelSU](https://t.me/KernelSU) -## 授權 +## 安全政策 + +欲要得知、回報 KernelSU 的安全性漏洞,請參閱 [SECURITY.md](/SECURITY.md)。 + +## 授權條款 -- 目錄 `kernel` 下所有檔案為 [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -- 除 `kernel` 目錄的其他部分均為 [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- 位於 `kernel` 資料夾的檔案以[《GPL-2.0-only》](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)規範。 +- 非位於 `kernel` 資料夾的其他檔案以[《GPL-3.0-or-later》](https://www.gnu.org/licenses/gpl-3.0.html)規範。 -## 致謝 +## 致謝名單 -- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU 的靈感。 -- [genuine](https://github.com/brevent/genuine/):apk v2 簽章驗證。 -- [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技巧。 -- [Magisk](https://github.com/topjohnwu/Magisk):sepolicy 實作。 +- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU 的靈感來源。 +- [Magisk](https://github.com/topjohnwu/Magisk):強而有力的 Root 工具。 +- [genuine](https://github.com/brevent/genuine/):用於確效 Apk v2 簽章。 +- [Diamorphine](https://github.com/m0nad/Diamorphine): 用於增進 Rootkit 技巧。 diff --git a/docs/README_VI.md b/docs/README_VI.md index efa7f18f3a23..665e242b00ce 100644 --- a/docs/README_VI.md +++ b/docs/README_VI.md @@ -1,13 +1,21 @@ -[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [Polski](README_PL.md) | [Portuguese-Brazil](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | **Tiếng Việt** | [Indonesia](README_ID.md) | [עברית](README_iw.md) | [हिंदी](README_IN.md) +[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | **Tiếng Việt** | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU +logo + Giải pháp root thông qua thay đổi trên Kernel hệ điều hành cho các thiết bị Android. +[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) +[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) +[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) +[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) +[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) + ## Tính năng 1. Hỗ trợ gói thực thi `su` và quản lý quyền root. -2. Hệ thống mô-đun thông qua overlayfs. +2. Hệ thống mô-đun thông qua [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS). 3. [App Profile](https://kernelsu.org/guide/app-profile.html): Hạn chế quyền root của ứng dụng. ## Tình trạng tương thích @@ -16,7 +24,7 @@ KernelSU chính thức hỗ trợ các thiết bị Android với kernel GKI 2.0 WSA, ChromeOS và Android dựa trên container(container-based) cũng được hỗ trợ bởi KernelSU. -Hiên tại Giao diện nhị phân của ứng dụng (ABI) được hỗ trợ bao gồm `arm64-v8a` và `x86_64` +Hiên tại Giao diện nhị phân của ứng dụng (ABI) được hỗ trợ bao gồm `arm64-v8a` và `x86_64`. ## Sử dụng @@ -34,12 +42,12 @@ Nếu bạn muốn hỗ trợ dịch KernelSU sang một ngôn ngữ khác hoặ ## Giấy phép -- Tất cả các file trong thư mục `kernel` dùng giấy phép [GPL-2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) -- Tất cả các thành phần khác ngoại trừ thư mục `kernel` dùng giấy phép [GPL-3](https://www.gnu.org/licenses/gpl-3.0.html) +- Tất cả các file trong thư mục `kernel` dùng giấy phép [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). +- Tất cả các thành phần khác ngoại trừ thư mục `kernel` dùng giấy phép [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Lời cảm ơn - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): ý tưởng cho KernelSU. - [Magisk](https://github.com/topjohnwu/Magisk): công cụ root mạnh mẽ. - [genuine](https://github.com/brevent/genuine/): phương pháp xác thực apk v2. -- [Diamorphine](https://github.com/m0nad/Diamorphine): các phương pháp ẩn của rootkit . +- [Diamorphine](https://github.com/m0nad/Diamorphine): các phương pháp ẩn của rootkit. diff --git a/js/README.md b/js/README.md new file mode 100644 index 000000000000..91de2b45b9c6 --- /dev/null +++ b/js/README.md @@ -0,0 +1,120 @@ +# Library for KernelSU's module WebUI + +## Install + +```sh +yarn add kernelsu +``` + +## API + +### exec + +Spawns a **root** shell and runs a command within that shell, passing the `stdout` and `stderr` to a Promise when complete. + +- `command` `` The command to run, with space-separated arguments. +- `options` `` + - `cwd` - Current working directory of the child process + - `env` - Environment key-value pairs + +```javascript +import { exec } from 'kernelsu'; + +const { errno, stdout, stderr } = await exec('ls -l', { cwd: '/tmp' }); +if (errno === 0) { + // success + console.log(stdout); +} +``` + +### spawn + +Spawns a new process using the given `command` in **root** shell, with command-line arguments in `args`. If omitted, `args` defaults to an empty array. + +Returns a `ChildProcess`, Instances of the ChildProcess represent spawned child processes. + +- `command` `` The command to run. +- `args` `` List of string arguments. +- `options` ``: + - `cwd` `` - Current working directory of the child process + - `env` `` - Environment key-value pairs + +Example of running `ls -lh /data`, capturing `stdout`, `stderr`, and the exit code: + +```javascript +import { spawn } from 'kernelsu'; + +const ls = spawn('ls', ['-lh', '/data']); + +ls.stdout.on('data', (data) => { + console.log(`stdout: ${data}`); +}); + +ls.stderr.on('data', (data) => { + console.log(`stderr: ${data}`); +}); + +ls.on('exit', (code) => { + console.log(`child process exited with code ${code}`); +}); +``` + +#### ChildProcess + +##### Event 'exit' + +- `code` `` The exit code if the child exited on its own. + +The `'exit'` event is emitted after the child process ends. If the process exited, `code` is the final exit code of the process, otherwise null + +##### Event 'error' + +- `err` `` The error. + +The `'error'` event is emitted whenever: + +- The process could not be spawned. +- The process could not be killed. + +##### `stdout` + +A `Readable Stream` that represents the child process's `stdout`. + +```javascript +const subprocess = spawn('ls'); + +subprocess.stdout.on('data', (data) => { + console.log(`Received chunk ${data}`); +}); +``` + +#### `stderr` + +A `Readable Stream` that represents the child process's `stderr`. + +### fullScreen + +Request the WebView enter/exit full screen. + +```javascript +import { fullScreen } from 'kernelsu'; +fullScreen(true); +``` + +### toast + +Show a toast message. + +```javascript +import { toast } from 'kernelsu'; +toast('Hello, world!'); +``` + +### moduleInfo + +Get Module info. +```javascript +import { moduleInfo } from 'kernelsu'; +// print moduleId in console +console.log(moduleInfo()); +``` \ No newline at end of file diff --git a/js/index.d.ts b/js/index.d.ts new file mode 100644 index 000000000000..c9278175c891 --- /dev/null +++ b/js/index.d.ts @@ -0,0 +1,48 @@ +interface ExecOptions { + cwd?: string, + env?: { [key: string]: string } +} + +interface ExecResults { + errno: number, + stdout: string, + stderr: string +} + +declare function exec(command: string): Promise; +declare function exec(command: string, options: ExecOptions): Promise; + +interface SpawnOptions { + cwd?: string, + env?: { [key: string]: string } +} + +interface Stdio { + on(event: 'data', callback: (data: string) => void) +} + +interface ChildProcess { + stdout: Stdio, + stderr: Stdio, + on(event: 'exit', callback: (code: number) => void) + on(event: 'error', callback: (err: any) => void) +} + +declare function spawn(command: string): ChildProcess; +declare function spawn(command: string, args: string[]): ChildProcess; +declare function spawn(command: string, options: SpawnOptions): ChildProcess; +declare function spawn(command: string, args: string[], options: SpawnOptions): ChildProcess; + +declare function fullScreen(isFullScreen: boolean); + +declare function toast(message: string); + +declare function moduleInfo(): string; + +export { + exec, + spawn, + fullScreen, + toast, + moduleInfo +} diff --git a/js/index.js b/js/index.js new file mode 100644 index 000000000000..29b928acd6f1 --- /dev/null +++ b/js/index.js @@ -0,0 +1,119 @@ +let callbackCounter = 0; +function getUniqueCallbackName(prefix) { + return `${prefix}_callback_${Date.now()}_${callbackCounter++}`; +} + +export function exec(command, options) { + if (typeof options === "undefined") { + options = {}; + } + + return new Promise((resolve, reject) => { + // Generate a unique callback function name + const callbackFuncName = getUniqueCallbackName("exec"); + + // Define the success callback function + window[callbackFuncName] = (errno, stdout, stderr) => { + resolve({ errno, stdout, stderr }); + cleanup(callbackFuncName); + }; + + function cleanup(successName) { + delete window[successName]; + } + + try { + ksu.exec(command, JSON.stringify(options), callbackFuncName); + } catch (error) { + reject(error); + cleanup(callbackFuncName); + } + }); +} + +function Stdio() { + this.listeners = {}; + } + + Stdio.prototype.on = function (event, listener) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(listener); + }; + + Stdio.prototype.emit = function (event, ...args) { + if (this.listeners[event]) { + this.listeners[event].forEach((listener) => listener(...args)); + } + }; + + function ChildProcess() { + this.listeners = {}; + this.stdin = new Stdio(); + this.stdout = new Stdio(); + this.stderr = new Stdio(); + } + + ChildProcess.prototype.on = function (event, listener) { + if (!this.listeners[event]) { + this.listeners[event] = []; + } + this.listeners[event].push(listener); + }; + + ChildProcess.prototype.emit = function (event, ...args) { + if (this.listeners[event]) { + this.listeners[event].forEach((listener) => listener(...args)); + } + }; + + export function spawn(command, args, options) { + if (typeof args === "undefined") { + args = []; + } else if (!(args instanceof Array)) { + // allow for (command, options) signature + options = args; + } + + if (typeof options === "undefined") { + options = {}; + } + + const child = new ChildProcess(); + const childCallbackName = getUniqueCallbackName("spawn"); + window[childCallbackName] = child; + + function cleanup(name) { + delete window[name]; + } + + child.on("exit", code => { + cleanup(childCallbackName); + }); + + try { + ksu.spawn( + command, + JSON.stringify(args), + JSON.stringify(options), + childCallbackName + ); + } catch (error) { + child.emit("error", error); + cleanup(childCallbackName); + } + return child; + } + +export function fullScreen(isFullScreen) { + ksu.fullScreen(isFullScreen); +} + +export function toast(message) { + ksu.toast(message); +} + +export function moduleInfo() { + return ksu.moduleInfo(); +} diff --git a/js/package.json b/js/package.json new file mode 100644 index 000000000000..12002a05328e --- /dev/null +++ b/js/package.json @@ -0,0 +1,26 @@ +{ + "name": "kernelsu", + "version": "1.0.7", + "description": "Library for KernelSU's module WebUI", + "main": "index.js", + "types": "index.d.ts", + "scripts": { + "test": "npm run test" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tiann/KernelSU.git" + }, + "keywords": [ + "su", + "kernelsu", + "module", + "webui" + ], + "author": "weishu", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/tiann/KernelSU/issues" + }, + "homepage": "https://github.com/tiann/KernelSU#readme" +} diff --git a/justfile b/justfile new file mode 100644 index 000000000000..51bef767d4a9 --- /dev/null +++ b/justfile @@ -0,0 +1,14 @@ +alias bk := build_ksud +alias bm := build_manager + +build_ksud: + cross build --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml + +build_manager: build_ksud + cp userspace/ksud/target/aarch64-linux-android/release/ksud manager/app/src/main/jniLibs/arm64-v8a/libksud.so + cd manager && ./gradlew aDebug + +clippy: + cargo fmt --manifest-path ./userspace/ksud/Cargo.toml + cross clippy --target x86_64-pc-windows-gnu --release --manifest-path ./userspace/ksud/Cargo.toml + cross clippy --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml diff --git a/kernel/Kconfig b/kernel/Kconfig index 641e7123fecc..67f177f4708a 100644 --- a/kernel/Kconfig +++ b/kernel/Kconfig @@ -5,13 +5,15 @@ config KSU depends on OVERLAY_FS default y help - Enable kernel-level root privileges on Android System. + 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 + Enable KernelSU debug mode. endmenu diff --git a/kernel/Makefile b/kernel/Makefile index b58786e08d98..9fec3ef387b7 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -1,19 +1,24 @@ -obj-y += ksu.o -obj-y += allowlist.o -kernelsu-objs := apk_sign.o -obj-y += kernelsu.o -obj-y += module_api.o -obj-y += sucompat.o -obj-y += uid_observer.o -obj-y += manager.o -obj-y += core_hook.o -obj-y += ksud.o -obj-y += embed_ksud.o -obj-y += kernel_compat.o - -obj-y += selinux/ +kernelsu-objs := ksu.o +kernelsu-objs += allowlist.o +kernelsu-objs += apk_sign.o +kernelsu-objs += sucompat.o +kernelsu-objs += throne_tracker.o +kernelsu-objs += core_hook.o +kernelsu-objs += ksud.o +kernelsu-objs += embed_ksud.o +kernelsu-objs += kernel_compat.o + +kernelsu-objs += selinux/selinux.o +kernelsu-objs += selinux/sepolicy.o +kernelsu-objs += selinux/rules.o +ccflags-y += -I$(srctree)/security/selinux -I$(srctree)/security/selinux/include +ccflags-y += -I$(objtree)/security/selinux -include $(srctree)/include/uapi/asm-generic/errno.h + +obj-$(CONFIG_KSU) += kernelsu.o + # .git is a text file while the module is imported by 'git submodule add'. ifeq ($(shell test -e $(srctree)/$(src)/../.git; echo $$?),0) +$(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin [ -f ../.git/shallow ] && git fetch --unshallow) KSU_GIT_VERSION := $(shell cd $(srctree)/$(src); /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git rev-list --count HEAD) # ksu_version: major * 10000 + git version + 200 for historical reasons $(eval KSU_VERSION=$(shell expr 10000 + $(KSU_GIT_VERSION) + 200)) @@ -32,10 +37,18 @@ ifndef KSU_EXPECTED_HASH KSU_EXPECTED_HASH := c371061b19d8c7d7d6133c6a9bafe198fa944e50c1b31c9d8daa8d7f1fc2d2d6 endif +ifdef KSU_MANAGER_PACKAGE +ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\" +$(info -- KernelSU Manager package name: $(KSU_MANAGER_PACKAGE)) +endif + $(info -- KernelSU Manager signature size: $(KSU_EXPECTED_SIZE)) $(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH)) ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE) ccflags-y += -DEXPECTED_HASH=\"$(KSU_EXPECTED_HASH)\" + ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat -ccflags-y += -Wno-declaration-after-statement +ccflags-y += -Wno-declaration-after-statement -Wno-unused-function + +# Keep a new line here!! Because someone may append config diff --git a/kernel/allowlist.c b/kernel/allowlist.c index f1115eb531bf..4c8abe196b64 100644 --- a/kernel/allowlist.c +++ b/kernel/allowlist.c @@ -1,21 +1,21 @@ -#include "ksu.h" -#include "linux/compiler.h" -#include "linux/fs.h" -#include "linux/gfp.h" -#include "linux/kernel.h" -#include "linux/list.h" -#include "linux/printk.h" -#include "linux/slab.h" -#include "linux/types.h" -#include "linux/version.h" -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0) -#include "linux/compiler_types.h" -#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "ksu.h" #include "klog.h" // IWYU pragma: keep #include "selinux/selinux.h" #include "kernel_compat.h" #include "allowlist.h" +#include "manager.h" #define FILE_MAGIC 0x7f4b5355 // ' KSU', u32 #define FILE_FORMAT_VERSION 3 // u32 @@ -63,12 +63,14 @@ static void remove_uid_from_arr(uid_t uid) static void init_default_profiles() { + kernel_cap_t full_cap = CAP_FULL_SET; + default_root_profile.uid = 0; default_root_profile.gid = 0; default_root_profile.groups_count = 1; default_root_profile.groups[0] = 0; - memset(&default_root_profile.capabilities, 0xff, - sizeof(default_root_profile.capabilities)); + 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); @@ -274,6 +276,11 @@ bool __ksu_is_allow_uid(uid_t uid) return false; } + 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 { @@ -289,6 +296,10 @@ bool __ksu_is_allow_uid(uid_t 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 @@ -351,7 +362,7 @@ void do_save_allow_list(struct work_struct *work) loff_t off = 0; struct file *fp = - ksu_filp_open_compat(KERNEL_SU_ALLOWLIST, O_WRONLY | O_CREAT, 0644); + 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; @@ -458,7 +469,9 @@ void ksu_prune_allowlist(bool (*is_uid_valid)(uid_t, char *, void *), void *data modified = true; pr_info("prune uid: %d, package: %s\n", uid, package); list_del(&np->list); - allow_list_bitmap[uid / BITS_PER_BYTE] &= ~(1 << (uid % BITS_PER_BYTE)); + 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); diff --git a/kernel/allowlist.h b/kernel/allowlist.h index 298624bca4ee..e89bf71fa10c 100644 --- a/kernel/allowlist.h +++ b/kernel/allowlist.h @@ -1,7 +1,7 @@ #ifndef __KSU_H_ALLOWLIST #define __KSU_H_ALLOWLIST -#include "linux/types.h" +#include #include "ksu.h" void ksu_allowlist_init(void); diff --git a/kernel/apk_sign.c b/kernel/apk_sign.c index 5f7175c8d1db..ba8b73f2eb2b 100644 --- a/kernel/apk_sign.c +++ b/kernel/apk_sign.c @@ -1,21 +1,23 @@ -#include "linux/err.h" -#include "linux/fs.h" -#include "linux/gfp.h" -#include "linux/kernel.h" -#include "linux/moduleparam.h" +#include +#include +#include +#include +#include +#include +#ifdef CONFIG_KSU_DEBUG +#include +#endif +#include +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) +#include +#else +#include +#endif #include "apk_sign.h" #include "klog.h" // IWYU pragma: keep #include "kernel_compat.h" -#include "crypto/hash.h" -#include "linux/slab.h" -#include "linux/version.h" -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) -#include "crypto/sha2.h" -#else -#include "crypto/sha.h" -#endif struct sdesc { struct shash_desc shash; @@ -53,7 +55,7 @@ static int calc_hash(struct crypto_shash *alg, const unsigned char *data, } 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"; @@ -70,7 +72,7 @@ static int ksu_sha256(const unsigned char *data, unsigned int datalen, } static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, - unsigned expected_size, const char* expected_sha256) + unsigned expected_size, const char *expected_sha256) { ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer-sequence length ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer length @@ -90,7 +92,7 @@ static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, if (*size4 == expected_size) { *offset += *size4; - #define CERT_MAX_LENGTH 1024 +#define CERT_MAX_LENGTH 1024 char cert[CERT_MAX_LENGTH]; if (*size4 > CERT_MAX_LENGTH) { pr_info("cert length overlimit\n"); @@ -107,7 +109,8 @@ static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, hash_str[SHA256_DIGEST_SIZE * 2] = '\0'; bin2hex(hash_str, digest, SHA256_DIGEST_SIZE); - pr_info("sha256: %s, expected: %s\n", hash_str, expected_sha256); + pr_info("sha256: %s, expected: %s\n", hash_str, + expected_sha256); if (strcmp(expected_sha256, hash_str) == 0) { return true; } @@ -115,8 +118,62 @@ static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, return false; } -static __always_inline bool -check_v2_signature(char *path, unsigned expected_size, const char *expected_sha256) +struct zip_entry_header { + uint32_t signature; + uint16_t version; + uint16_t flags; + uint16_t compression; + uint16_t mod_time; + uint16_t mod_date; + uint32_t crc32; + uint32_t compressed_size; + uint32_t uncompressed_size; + uint16_t file_name_length; + uint16_t extra_field_length; +} __attribute__((packed)); + +// This is a necessary but not sufficient condition, but it is enough for us +static bool has_v1_signature_file(struct file *fp) +{ + struct zip_entry_header header; + const char MANIFEST[] = "META-INF/MANIFEST.MF"; + + loff_t pos = 0; + + while (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'; + + // Check if the entry matches META-INF/MANIFEST.MF + if (strncmp(MANIFEST, fileName, sizeof(MANIFEST) - 1) == + 0) { + return true; + } + } else { + // Skip the entry file name + pos += header.file_name_length; + } + + // Skip to the next entry + pos += header.extra_field_length + header.compressed_size; + } + + return false; +} + +static __always_inline bool check_v2_signature(char *path, + unsigned expected_size, + const char *expected_sha256) { unsigned char buffer[0x11] = { 0 }; u32 size4; @@ -125,6 +182,7 @@ check_v2_signature(char *path, unsigned expected_size, const char *expected_sha2 loff_t pos; bool v2_signing_valid = false; + int v2_signing_blocks = 0; bool v3_signing_exist = false; bool v3_1_signing_exist = false; @@ -132,7 +190,7 @@ check_v2_signature(char *path, unsigned expected_size, const char *expected_sha2 struct file *fp = ksu_filp_open_compat(path, O_RDONLY, 0); if (IS_ERR(fp)) { pr_err("open %s error.\n", path); - return PTR_ERR(fp); + return false; } // disable inotify for this file @@ -173,7 +231,8 @@ check_v2_signature(char *path, unsigned expected_size, const char *expected_sha2 goto clean; } - for (;;) { + int loop_count = 0; + while (loop_count++ < 10) { uint32_t id; uint32_t offset; ksu_kernel_read_compat(fp, &size8, 0x8, @@ -183,25 +242,48 @@ check_v2_signature(char *path, unsigned expected_size, const char *expected_sha2 } ksu_kernel_read_compat(fp, &id, 0x4, &pos); // id offset = 4; - pr_info("id: 0x%08x\n", id); if (id == 0x7109871au) { - v2_signing_valid = check_block(fp, &size4, &pos, &offset, - expected_size, expected_sha256); + v2_signing_blocks++; + v2_signing_valid = + check_block(fp, &size4, &pos, &offset, + expected_size, expected_sha256); } else if (id == 0xf05368c0u) { // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73 v3_signing_exist = true; } else if (id == 0x1b93ad61u) { // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#74 v3_1_signing_exist = true; + } else { +#ifdef CONFIG_KSU_DEBUG + pr_info("Unknown id: 0x%08x\n", id); +#endif } pos += (size8 - offset); } + if (v2_signing_blocks != 1) { +#ifdef CONFIG_KSU_DEBUG + pr_err("Unexpected v2 signature count: %d\n", + v2_signing_blocks); +#endif + v2_signing_valid = false; + } + + if (v2_signing_valid) { + int has_v1_signing = has_v1_signature_file(fp); + if (has_v1_signing) { + pr_err("Unexpected v1 signature scheme found!\n"); + filp_close(fp, 0); + return false; + } + } clean: filp_close(fp, 0); if (v3_signing_exist || v3_1_signing_exist) { +#ifdef CONFIG_KSU_DEBUG pr_err("Unexpected v3 signature scheme found!\n"); +#endif return false; } @@ -210,25 +292,15 @@ check_v2_signature(char *path, unsigned expected_size, const char *expected_sha2 #ifdef CONFIG_KSU_DEBUG -unsigned ksu_expected_size = EXPECTED_SIZE; -const char *ksu_expected_hash = EXPECTED_HASH; +int ksu_debug_manager_uid = -1; #include "manager.h" static int set_expected_size(const char *val, const struct kernel_param *kp) { int rv = param_set_uint(val, kp); - ksu_invalidate_manager_uid(); - pr_info("ksu_expected_size set to %x\n", ksu_expected_size); - return rv; -} - -static int set_expected_hash(const char *val, const struct kernel_param *kp) -{ - pr_info("set_expected_hash: %s\n", val); - int rv = param_set_charp(val, kp); - ksu_invalidate_manager_uid(); - pr_info("ksu_expected_hash set to %s\n", ksu_expected_hash); + ksu_set_manager_uid(ksu_debug_manager_uid); + pr_info("ksu_manager_uid set to %d\n", ksu_debug_manager_uid); return rv; } @@ -237,27 +309,12 @@ static struct kernel_param_ops expected_size_ops = { .get = param_get_uint, }; -static struct kernel_param_ops expected_hash_ops = { - .set = set_expected_hash, - .get = param_get_charp, - .free = param_free_charp, -}; - -module_param_cb(ksu_expected_size, &expected_size_ops, &ksu_expected_size, - S_IRUSR | S_IWUSR); -module_param_cb(ksu_expected_hash, &expected_hash_ops, &ksu_expected_hash, - S_IRUSR | S_IWUSR); - -bool is_manager_apk(char *path) -{ - return check_v2_signature(path, ksu_expected_size, ksu_expected_hash); -} +module_param_cb(ksu_debug_manager_uid, &expected_size_ops, + &ksu_debug_manager_uid, S_IRUSR | S_IWUSR); -#else +#endif bool is_manager_apk(char *path) { return check_v2_signature(path, EXPECTED_SIZE, EXPECTED_HASH); -} - -#endif +} \ No newline at end of file diff --git a/kernel/apk_sign.h b/kernel/apk_sign.h index ebd78a8b0b23..bed501c49264 100644 --- a/kernel/apk_sign.h +++ b/kernel/apk_sign.h @@ -1,7 +1,7 @@ #ifndef __KSU_H_APK_V2_SIGN #define __KSU_H_APK_V2_SIGN -#include "linux/types.h" +#include bool is_manager_apk(char *path); diff --git a/kernel/arch.h b/kernel/arch.h index 1678fd1b0ca8..eec38c289e71 100644 --- a/kernel/arch.h +++ b/kernel/arch.h @@ -1,7 +1,7 @@ #ifndef __KSU_H_ARCH #define __KSU_H_ARCH -#include "linux/version.h" +#include #if defined(__aarch64__) @@ -18,11 +18,11 @@ #define __PT_SP_REG sp #define __PT_IP_REG pc -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 16, 0) #define PRCTL_SYMBOL "__arm64_sys_prctl" -#else -#define PRCTL_SYMBOL "sys_prctl" -#endif +#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,11 +39,11 @@ #define __PT_RC_REG ax #define __PT_SP_REG sp #define __PT_IP_REG ip -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 16, 0) #define PRCTL_SYMBOL "__x64_sys_prctl" -#else -#define PRCTL_SYMBOL "sys_prctl" -#endif +#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 #error "Unsupported arch" @@ -67,4 +67,6 @@ #define PT_REGS_SP(x) (__PT_REGS_CAST(x)->__PT_SP_REG) #define PT_REGS_IP(x) (__PT_REGS_CAST(x)->__PT_IP_REG) +#define PT_REAL_REGS(regs) ((struct pt_regs *)PT_REGS_PARM1(regs)) + #endif diff --git a/kernel/core_hook.c b/kernel/core_hook.c index 5a8ebc8dc4d9..af3225cda4ef 100644 --- a/kernel/core_hook.c +++ b/kernel/core_hook.c @@ -1,23 +1,37 @@ -#include "linux/capability.h" -#include "linux/cred.h" -#include "linux/dcache.h" -#include "linux/err.h" -#include "linux/init.h" -#include "linux/init_task.h" -#include "linux/kernel.h" -#include "linux/kprobes.h" -#include "linux/lsm_hooks.h" -#include "linux/nsproxy.h" -#include "linux/path.h" -#include "linux/printk.h" -#include "linux/uaccess.h" -#include "linux/uidgid.h" -#include "linux/version.h" -#include "linux/mount.h" - -#include "linux/fs.h" -#include "linux/namei.h" -#include "linux/rcupdate.h" +#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" @@ -27,9 +41,12 @@ #include "ksud.h" #include "manager.h" #include "selinux/selinux.h" -#include "uid_observer.h" +#include "throne_tracker.h" +#include "throne_tracker.h" #include "kernel_compat.h" +static bool ksu_module_mounted = false; + extern int handle_sepolicy(unsigned long arg3, void __user *arg4); static inline bool is_allow_su() @@ -41,16 +58,11 @@ static inline bool is_allow_su() return ksu_is_allow_uid(current_uid().val); } -static inline bool is_isolated_uid(uid_t uid) +static inline bool is_unsupported_uid(uid_t uid) { -#define FIRST_ISOLATED_UID 99000 -#define LAST_ISOLATED_UID 99999 -#define FIRST_APP_ZYGOTE_ISOLATED_UID 90000 -#define LAST_APP_ZYGOTE_ISOLATED_UID 98999 +#define LAST_APPLICATION_UID 19999 uid_t appid = uid % 100000; - return (appid >= FIRST_ISOLATED_UID && appid <= LAST_ISOLATED_UID) || - (appid >= FIRST_APP_ZYGOTE_ISOLATED_UID && - appid <= LAST_APP_ZYGOTE_ISOLATED_UID); + return appid > LAST_APPLICATION_UID; } static struct group_info root_groups = { .usage = ATOMIC_INIT(2) }; @@ -87,25 +99,45 @@ static void setup_groups(struct root_profile *profile, struct cred *cred) put_group_info(group_info); return; } -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 9, 0) group_info->gid[i] = kgid; -#else - GROUP_AT(group_info, i) = kgid; -#endif } groups_sort(group_info); set_groups(cred, 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 = (struct cred *)__task_cred(current); + rcu_read_lock(); + + do { + cred = (struct cred *)__task_cred((current)); + BUG_ON(!cred); + } while (!get_cred_rcu(cred)); if (cred->euid.val == 0) { pr_warn("Already root, don't escape!\n"); + rcu_read_unlock(); return; } struct root_profile *profile = ksu_get_root_profile(cred->uid.val); @@ -119,37 +151,32 @@ void escape_to_root(void) 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)); - // capabilities - memcpy(&cred->cap_effective, &profile->capabilities.effective, + // 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_inheritable, &profile->capabilities.effective, - sizeof(cred->cap_inheritable)); memcpy(&cred->cap_permitted, &profile->capabilities.effective, sizeof(cred->cap_permitted)); memcpy(&cred->cap_bset, &profile->capabilities.effective, sizeof(cred->cap_bset)); - memcpy(&cred->cap_ambient, &profile->capabilities.effective, - sizeof(cred->cap_ambient)); - // 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 + setup_groups(profile, cred); -#ifdef CONFIG_SECCOMP - current->seccomp.mode = 0; - current->seccomp.filter = NULL; -#else -#endif + rcu_read_unlock(); - setup_groups(profile, 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); } @@ -182,13 +209,13 @@ int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry) return 0; } - if (strcmp(buf, "/system/packages.list")) { + 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); - update_uid(); + track_throne(); return 0; } @@ -204,89 +231,39 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, return 0; } - // always ignore isolated app uid - if (is_isolated_uid(current_uid().val)) { - 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); } - static uid_t last_failed_uid = -1; - if (last_failed_uid == current_uid().val) { + bool from_root = 0 == current_uid().val; + bool from_manager = is_manager(); + + if (!from_root && !from_manager) { + // only root or manager can access this interface return 0; } - // pr_info("option: 0x%x, cmd: %ld\n", option, arg2); - - if (arg2 == CMD_BECOME_MANAGER) { - // quick check - if (is_manager()) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("become_manager: prctl reply error\n"); - } - return 0; - } - if (ksu_is_manager_uid_valid()) { - pr_info("manager already exist: %d\n", - ksu_get_manager_uid()); - return 0; - } - - // someone wants to be root manager, just check it! - // arg3 should be `/data/user//` - char param[128]; - if (ksu_strncpy_from_user_nofault(param, arg3, sizeof(param)) == - -EFAULT) { #ifdef CONFIG_KSU_DEBUG - pr_err("become_manager: copy param err\n"); + pr_info("option: 0x%x, cmd: %ld\n", option, arg2); #endif - return 0; - } - - // for user 0, it is /data/data - // for user 999, it is /data/user/999 - const char *prefix; - char prefixTmp[64]; - int userId = current_uid().val / 100000; - if (userId == 0) { - prefix = "/data/data"; - } else { - snprintf(prefixTmp, sizeof(prefixTmp), "/data/user/%d", - userId); - prefix = prefixTmp; - } - if (startswith(param, (char *)prefix) != 0) { - pr_info("become_manager: invalid param: %s\n", param); - return 0; - } - - // stat the param, app must have permission to do this - // otherwise it may fake the path! - struct path path; - if (kern_path(param, LOOKUP_DIRECTORY, &path)) { - pr_err("become_manager: kern_path err\n"); - return 0; - } - if (path.dentry->d_inode->i_uid.val != current_uid().val) { - pr_err("become_manager: path uid != current uid\n"); - path_put(&path); - return 0; - } - char *pkg = param + strlen(prefix); - pr_info("become_manager: param pkg: %s\n", pkg); - - bool success = become_manager(pkg); - if (success) { + 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; } - path_put(&path); return 0; } if (arg2 == CMD_GRANT_ROOT) { if (is_allow_su()) { - pr_info("allow root for: %d\n", current_uid()); + 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"); @@ -297,17 +274,23 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, // Both root manager and root processes should be allowed to get version if (arg2 == CMD_GET_VERSION) { - if (is_manager() || 0 == current_uid().val) { - u32 version = KERNEL_SU_VERSION; - if (copy_to_user(arg3, &version, sizeof(version))) { - pr_err("prctl reply error, cmd: %d\n", arg2); - } + u32 version = KERNEL_SU_VERSION; + if (copy_to_user(arg3, &version, sizeof(version))) { + pr_err("prctl reply error, cmd: %lu\n", arg2); + } +#ifdef MODULE + u32 is_lkm = 0x1; +#else + u32 is_lkm = 0x0; +#endif + if (arg4 && copy_to_user(arg4, &is_lkm, sizeof(is_lkm))) { + pr_err("prctl reply error, cmd: %lu\n", arg2); } return 0; } if (arg2 == CMD_REPORT_EVENT) { - if (0 != current_uid().val) { + if (!from_root) { return 0; } switch (arg3) { @@ -328,6 +311,11 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, } break; } + case EVENT_MODULE_MOUNTED: { + ksu_module_mounted = true; + pr_info("module mounted!\n"); + break; + } default: break; } @@ -335,7 +323,7 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, } if (arg2 == CMD_SET_SEPOLICY) { - if (0 != current_uid().val) { + if (!from_root) { return 0; } if (!handle_sepolicy(arg3, arg4)) { @@ -348,9 +336,6 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, } if (arg2 == CMD_CHECK_SAFEMODE) { - if (!is_manager() && 0 != current_uid().val) { - return 0; - } if (ksu_is_safe_mode()) { pr_warn("safemode enabled!\n"); if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { @@ -361,57 +346,49 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, } if (arg2 == CMD_GET_ALLOW_LIST || arg2 == CMD_GET_DENY_LIST) { - if (is_manager() || 0 == current_uid().val) { - 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: %d\n", - arg2); - } - } else { - pr_err("prctl copy allowlist error\n"); + 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) { - if (is_manager() || 0 == current_uid().val) { - 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: %d\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: %d\n", - arg2); - } - } else { - pr_err("prctl copy err, cmd: %d\n", arg2); + 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; } // all other cmds are for 'root manager' - if (!is_manager()) { - last_failed_uid = current_uid().val; + if (!from_manager) { return 0; } @@ -430,7 +407,7 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, return 0; } if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %d\n", arg2); + pr_err("prctl reply error, cmd: %lu\n", arg2); } } return 0; @@ -446,7 +423,7 @@ int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, // 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: %d\n", arg2); + pr_err("prctl reply error, cmd: %lu\n", arg2); } } return 0; @@ -486,14 +463,10 @@ static bool should_umount(struct path *path) static void ksu_umount_mnt(struct path *path, int flags) { -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0) int err = path_umount(path, flags); if (err) { pr_info("umount %s failed: %d\n", path->dentry->d_iname, err); } -#else - // TODO: umount for non GKI kernel -#endif } static void try_umount(const char *mnt, bool check_mnt, int flags) @@ -519,6 +492,11 @@ static void try_umount(const char *mnt, bool check_mnt, int 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; } @@ -531,7 +509,7 @@ int ksu_handle_setuid(struct cred *new, const struct cred *old) return 0; } - if (!is_appuid(new_uid) || is_isolated_uid(new_uid.val)) { + if (!is_appuid(new_uid) || is_unsupported_uid(new_uid.val)) { // pr_info("handle setuid ignore non application or isolated uid: %d\n", new_uid.val); return 0; } @@ -554,19 +532,27 @@ int ksu_handle_setuid(struct cred *new, const struct cred *old) // when we umount for such process, that is a disaster! bool is_zygote_child = is_zygote(old->security); if (!is_zygote_child) { - pr_info("handle umount ignore non zygote child: %d\n", current->pid); + 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); + 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; } @@ -574,22 +560,12 @@ int ksu_handle_setuid(struct cred *new, const struct cred *old) static int handler_pre(struct kprobe *p, struct pt_regs *regs) { -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 16, 0) - struct pt_regs *real_regs = (struct pt_regs *)PT_REGS_PARM1(regs); -#else - struct pt_regs *real_regs = regs; -#endif + 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); -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 16, 0) // 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); -#else - // PRCTL_SYMBOL is the common one, called by C convention in do_syscall_64 - // https://elixir.bootlin.com/linux/v4.15.18/source/arch/x86/entry/common.c#L287 - unsigned long arg4 = (unsigned long)PT_REGS_CCALL_PARM4(real_regs); -#endif unsigned long arg5 = (unsigned long)PT_REGS_PARM5(real_regs); return ksu_handle_prctl(option, arg2, arg3, arg4, arg5); @@ -649,23 +625,7 @@ static int ksu_task_prctl(int option, unsigned long arg2, unsigned long arg3, ksu_handle_prctl(option, arg2, arg3, arg4, arg5); return -ENOSYS; } -// kernel 4.4 and 4.9 -#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) -static int ksu_key_permission(key_ref_t key_ref, const struct cred *cred, - unsigned perm) -{ - if (init_session_keyring != NULL) { - return 0; - } - if (strcmp(current->comm, "init")) { - // we are only interested in `init` process - return 0; - } - init_session_keyring = cred->session_keyring; - pr_info("kernel_compat: got init_session_keyring\n"); - return 0; -} -#endif + static int ksu_inode_rename(struct inode *old_inode, struct dentry *old_dentry, struct inode *new_inode, struct dentry *new_dentry) { @@ -678,40 +638,196 @@ static int ksu_task_fix_setuid(struct cred *new, const struct cred *old, 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), -#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) - LSM_HOOK_INIT(key_permission, ksu_key_permission) -#endif }; void __init ksu_lsm_hook_init(void) { -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0) security_add_hooks(ksu_hooks, ARRAY_SIZE(ksu_hooks), "ksu"); +} + #else - // https://elixir.bootlin.com/linux/v4.10.17/source/include/linux/lsm_hooks.h#L1892 - security_add_hooks(ksu_hooks, ARRAY_SIZE(ksu_hooks)); -#endif +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) { -#ifndef MODULE - pr_info("ksu_lsm_hook_init\n"); ksu_lsm_hook_init(); -#else - pr_info("ksu_kprobe_init\n"); - ksu_kprobe_init(); -#endif } void ksu_core_exit(void) { -#ifndef MODULE - pr_info("ksu_kprobe_exit\n"); - ksu_kprobe_exit(); +#ifdef CONFIG_KPROBES + 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 index 8e8bfc2caef4..616951e8db35 100644 --- a/kernel/core_hook.h +++ b/kernel/core_hook.h @@ -1,7 +1,7 @@ #ifndef __KSU_H_KSU_CORE #define __KSU_H_KSU_CORE -#include "linux/init.h" +#include void __init ksu_core_init(void); void ksu_core_exit(void); diff --git a/kernel/include/ksu_hook.h b/kernel/include/ksu_hook.h index d4a2cddb06e2..ea0b04d3e538 100644 --- a/kernel/include/ksu_hook.h +++ b/kernel/include/ksu_hook.h @@ -1,8 +1,8 @@ #ifndef __KSU_H_KSHOOK #define __KSU_H_KSHOOK -#include "linux/fs.h" -#include "linux/types.h" +#include +#include // For sucompat diff --git a/kernel/kernel_compat.c b/kernel/kernel_compat.c index 3e216657e637..d4ee546d6ef1 100644 --- a/kernel/kernel_compat.c +++ b/kernel/kernel_compat.c @@ -1,41 +1,10 @@ -#include "linux/version.h" -#include "linux/fs.h" -#include "linux/nsproxy.h" -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 10, 0) -#include "linux/sched/task.h" -#include "linux/uaccess.h" -#elif LINUX_VERSION_CODE >= KERNEL_VERSION(4, 4, 0) -#include "linux/uaccess.h" -#include "linux/sched.h" -#else -#include "linux/sched.h" -#endif +#include +#include +#include +#include +#include #include "klog.h" // IWYU pragma: keep - -#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) -#include "linux/key.h" -#include "linux/errno.h" -#include "linux/cred.h" -struct key *init_session_keyring = NULL; - -static inline int install_session_keyring(struct key *keyring) -{ - struct cred *new; - int ret; - - new = prepare_creds(); - if (!new) - return -ENOMEM; - - ret = install_session_keyring_to_cred(new, keyring); - if (ret < 0) { - abort_creds(new); - return ret; - } - - return commit_creds(new); -} -#endif +#include "kernel_compat.h" extern struct task_struct init_task; @@ -81,13 +50,6 @@ void ksu_android_ns_fs_check() struct file *ksu_filp_open_compat(const char *filename, int flags, umode_t mode) { -#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) - if (init_session_keyring != NULL && !current_cred()->session_keyring && - (current->flags & PF_WQ_WORKER)) { - pr_info("installing init session keyring for older kernel\n"); - install_session_keyring(init_session_keyring); - } -#endif // 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) { @@ -110,69 +72,17 @@ struct file *ksu_filp_open_compat(const char *filename, int flags, umode_t mode) ssize_t ksu_kernel_read_compat(struct file *p, void *buf, size_t count, loff_t *pos) { -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0) return kernel_read(p, buf, count, pos); -#else - loff_t offset = pos ? *pos : 0; - ssize_t result = kernel_read(p, offset, (char *)buf, count); - if (pos && result > 0) { - *pos = offset + result; - } - return result; -#endif } ssize_t ksu_kernel_write_compat(struct file *p, const void *buf, size_t count, loff_t *pos) { -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0) return kernel_write(p, buf, count, pos); -#else - loff_t offset = pos ? *pos : 0; - ssize_t result = kernel_write(p, buf, count, offset); - if (pos && result > 0) { - *pos = offset + result; - } - return result; -#endif } -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 8, 0) long ksu_strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr, long count) { return strncpy_from_user_nofault(dst, unsafe_addr, count); } -#elif LINUX_VERSION_CODE >= KERNEL_VERSION(5, 3, 0) -long ksu_strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr, - long count) -{ - return strncpy_from_unsafe_user(dst, unsafe_addr, count); -} -#else -// Copied from: https://elixir.bootlin.com/linux/v4.9.337/source/mm/maccess.c#L201 -long ksu_strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr, - long count) -{ - mm_segment_t old_fs = get_fs(); - long ret; - - if (unlikely(count <= 0)) - return 0; - - set_fs(USER_DS); - pagefault_disable(); - ret = strncpy_from_user(dst, unsafe_addr, count); - pagefault_enable(); - set_fs(old_fs); - - if (ret >= count) { - ret = count; - dst[ret - 1] = '\0'; - } else if (ret > 0) { - ret++; - } - - return ret; -} -#endif diff --git a/kernel/kernel_compat.h b/kernel/kernel_compat.h index f97080d41eae..4bcfbf389318 100644 --- a/kernel/kernel_compat.h +++ b/kernel/kernel_compat.h @@ -1,18 +1,29 @@ #ifndef __KSU_H_KERNEL_COMPAT #define __KSU_H_KERNEL_COMPAT -#include "linux/fs.h" +#include +#include +#include "ss/policydb.h" #include "linux/key.h" -#include "linux/version.h" + +/* + * 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); -#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0) -extern struct key *init_session_keyring; -#endif - extern void ksu_android_ns_fs_check(); extern struct file *ksu_filp_open_compat(const char *filename, int flags, umode_t mode); diff --git a/kernel/ksu.c b/kernel/ksu.c index 28b8c0078b1e..d517c3b54396 100644 --- a/kernel/ksu.c +++ b/kernel/ksu.c @@ -1,13 +1,15 @@ -#include "linux/fs.h" -#include "linux/module.h" -#include "linux/workqueue.h" +#include +#include +#include +#include +#include #include "allowlist.h" #include "arch.h" #include "core_hook.h" #include "klog.h" // IWYU pragma: keep #include "ksu.h" -#include "uid_observer.h" +#include "throne_tracker.h" static struct workqueue_struct *ksu_workqueue; @@ -30,8 +32,10 @@ int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv, flags); } -extern void ksu_enable_sucompat(); -extern void ksu_enable_ksud(); +extern void ksu_sucompat_init(); +extern void ksu_sucompat_exit(); +extern void ksu_ksud_init(); +extern void ksu_ksud_exit(); int __init kernelsu_init(void) { @@ -51,15 +55,20 @@ int __init kernelsu_init(void) ksu_allowlist_init(); - ksu_uid_observer_init(); + ksu_throne_tracker_init(); #ifdef CONFIG_KPROBES - ksu_enable_sucompat(); - ksu_enable_ksud(); + ksu_sucompat_init(); + 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 MODULE +#ifndef CONFIG_KSU_DEBUG + kobject_del(&THIS_MODULE->mkobj.kobj); +#endif +#endif return 0; } @@ -67,10 +76,15 @@ void kernelsu_exit(void) { ksu_allowlist_exit(); - ksu_uid_observer_exit(); + ksu_throne_tracker_exit(); destroy_workqueue(ksu_workqueue); +#ifdef CONFIG_KPROBES + ksu_ksud_exit(); + ksu_sucompat_exit(); +#endif + ksu_core_exit(); } @@ -80,7 +94,4 @@ module_exit(kernelsu_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("weishu"); MODULE_DESCRIPTION("Android KernelSU"); - -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 0, 0) 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 cdffb5aece87..35d1b14dbb7c 100644 --- a/kernel/ksu.h +++ b/kernel/ksu.h @@ -1,8 +1,8 @@ #ifndef __KSU_H_KSU #define __KSU_H_KSU -#include "linux/types.h" -#include "linux/workqueue.h" +#include +#include #define KERNEL_SU_VERSION KSU_VERSION #define KERNEL_SU_OPTION 0xDEADBEEF @@ -24,6 +24,7 @@ #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 diff --git a/kernel/ksud.c b/kernel/ksud.c index db50eee427ad..98fee107bf5e 100644 --- a/kernel/ksud.c +++ b/kernel/ksud.c @@ -1,15 +1,17 @@ -#include "asm/current.h" -#include "linux/compat.h" -#include "linux/dcache.h" -#include "linux/err.h" -#include "linux/fs.h" -#include "linux/input-event-codes.h" -#include "linux/kprobes.h" -#include "linux/printk.h" -#include "linux/types.h" -#include "linux/uaccess.h" -#include "linux/version.h" -#include "linux/workqueue.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include "allowlist.h" #include "arch.h" @@ -55,6 +57,8 @@ bool ksu_execveat_hook __read_mostly = true; bool ksu_input_hook __read_mostly = true; #endif +u32 ksu_devpts_sid; + void on_post_fs_data(void) { static bool done = false; @@ -67,6 +71,9 @@ void on_post_fs_data(void) ksu_load_allow_list(); // 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); } #define MAX_ARG_STRINGS 0x7FFFFFFF @@ -107,7 +114,7 @@ static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr) * 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. */ @@ -138,9 +145,10 @@ static int __maybe_unused count(struct user_arg_ptr argv, int max) return i; } -// 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 +// 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 *__never_use_flags) + struct user_arg_ptr *argv, + struct user_arg_ptr *envp, int *flags) { #ifndef CONFIG_KPROBES if (!ksu_execveat_hook) { @@ -166,8 +174,9 @@ int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr, return 0; } - if (unlikely(!memcmp(filename->name, system_bin_init, - sizeof(system_bin_init) - 1))) { + 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); @@ -175,8 +184,10 @@ int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr, const char __user *p = get_user_arg_ptr(*argv, 1); if (p && !IS_ERR(p)) { char first_arg[16]; - ksu_strncpy_from_user_nofault(first_arg, p, sizeof(first_arg)); - pr_info("/system/bin/init first arg: %s\n", first_arg); + ksu_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(); @@ -188,7 +199,8 @@ int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr, } } } else if (unlikely(!memcmp(filename->name, old_system_init, - sizeof(old_system_init) - 1))) { + sizeof(old_system_init) - 1) && + argv)) { // /init executed int argc = count(*argv, MAX_ARG_STRINGS); pr_info("/init argc: %d\n", argc); @@ -197,7 +209,8 @@ int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr, const char __user *p = get_user_arg_ptr(*argv, 1); if (p && !IS_ERR(p)) { char first_arg[16]; - ksu_strncpy_from_user_nofault(first_arg, p, sizeof(first_arg)); + ksu_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"); @@ -208,19 +221,21 @@ int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr, } else { pr_err("/init parse args err!\n"); } - } else if (argc == 1 && !init_second_stage_executed) { + } 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); + 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 (ksu_strncpy_from_user_nofault(env, p, sizeof(env)) < 0) + if (ksu_strncpy_from_user_nofault( + env, p, sizeof(env)) < 0) continue; // Parsing environment variable names and values char *env_name = env; @@ -231,10 +246,14 @@ int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr, *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"))) { + 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; + init_second_stage_executed = + true; ksu_android_ns_fs_check(); } } @@ -242,10 +261,11 @@ int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr, } } - if (unlikely(first_app_process && - !memcmp(filename->name, app_process, sizeof(app_process) - 1))) { + 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); + pr_info("exec app_process, /data prepared, second_stage: %d\n", + init_second_stage_executed); on_post_fs_data(); // we keep this for old ksud stop_execve_hook(); } @@ -264,7 +284,8 @@ static ssize_t read_proxy(struct file *file, char __user *buf, size_t count, 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); + pr_info("read_proxy append %ld + %ld\n", ret, + read_count_append); ret += read_count_append; } return ret; @@ -340,17 +361,17 @@ int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr, size_t rc_count = strlen(KERNEL_SU_RC); - pr_info("vfs_read: %s, comm: %s, count: %d, rc_count: %d\n", dpath, + 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: %d < rc_count: %d\n", 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: %d\n", ret); + pr_err("copy ksud.rc failed: %zu\n", ret); return 0; } @@ -376,6 +397,18 @@ int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr, return 0; } +int ksu_handle_sys_read(unsigned int fd, char __user **buf_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; +} + static unsigned int volumedown_pressed_count = 0; static bool is_volumedown_enough(unsigned int count) @@ -430,35 +463,37 @@ bool ksu_is_safe_mode() #ifdef CONFIG_KPROBES -// https://elixir.bootlin.com/linux/v5.10.158/source/fs/exec.c#L1864 -static int execve_handler_pre(struct kprobe *p, struct pt_regs *regs) +static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs) { - int *fd = (int *)&PT_REGS_PARM1(regs); - struct filename **filename_ptr = - (struct filename **)&PT_REGS_PARM2(regs); - struct user_arg_ptr argv; -#ifdef CONFIG_COMPAT - argv.is_compat = PT_REGS_PARM3(regs); - if (unlikely(argv.is_compat)) { - argv.ptr.compat = PT_REGS_CCALL_PARM4(regs); - } else { - argv.ptr.native = PT_REGS_CCALL_PARM4(regs); - } -#else - argv.ptr.native = PT_REGS_PARM3(regs); -#endif + 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; - return ksu_handle_execveat_ksud(fd, filename_ptr, &argv, NULL, NULL); + memset(path, 0, sizeof(path)); + ksu_strncpy_from_user_nofault(path, *filename_user, 32); + filename_in.name = path; + + filename_p = &filename_in; + return ksu_handle_execveat_ksud(AT_FDCWD, &filename_p, &argv, NULL, + NULL); } -static int read_handler_pre(struct kprobe *p, struct pt_regs *regs) +static int sys_read_handler_pre(struct kprobe *p, struct pt_regs *regs) { - struct file **file_ptr = (struct file **)&PT_REGS_PARM1(regs); - char __user **buf_ptr = (char **)&PT_REGS_PARM2(regs); - size_t *count_ptr = (size_t *)&PT_REGS_PARM3(regs); - loff_t **pos_ptr = (loff_t **)&PT_REGS_CCALL_PARM4(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_vfs_read(file_ptr, buf_ptr, count_ptr, pos_ptr); + return ksu_handle_sys_read(fd, buf_ptr, count_ptr); } static int input_handle_event_handler_pre(struct kprobe *p, @@ -471,23 +506,18 @@ static int input_handle_event_handler_pre(struct kprobe *p, } static struct kprobe execve_kp = { -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0) - .symbol_name = "do_execveat_common", -#elif LINUX_VERSION_CODE >= KERNEL_VERSION(4, 19, 0) - .symbol_name = "__do_execve_file", -#elif LINUX_VERSION_CODE >= KERNEL_VERSION(3, 19, 0) - .symbol_name = "do_execveat_common", -#endif - .pre_handler = execve_handler_pre, + .symbol_name = SYS_EXECVE_SYMBOL, + .pre_handler = sys_execve_handler_pre, }; static struct kprobe vfs_read_kp = { - .symbol_name = "vfs_read", - .pre_handler = read_handler_pre, + .symbol_name = SYS_READ_SYMBOL, + .pre_handler = sys_read_handler_pre, }; -static struct kprobe input_handle_event_kp = { - .symbol_name = "input_handle_event", + +static struct kprobe input_event_kp = { + .symbol_name = "input_event", .pre_handler = input_handle_event_handler_pre, }; @@ -503,7 +533,7 @@ static void do_stop_execve_hook(struct work_struct *work) static void do_stop_input_hook(struct work_struct *work) { - unregister_kprobe(&input_handle_event_kp); + unregister_kprobe(&input_event_kp); } #endif @@ -546,7 +576,7 @@ static void stop_input_hook() } // ksud: module support -void ksu_enable_ksud() +void ksu_ksud_init() { #ifdef CONFIG_KPROBES int ret; @@ -557,11 +587,21 @@ void ksu_enable_ksud() ret = register_kprobe(&vfs_read_kp); pr_info("ksud: vfs_read_kp: %d\n", ret); - ret = register_kprobe(&input_handle_event_kp); - pr_info("ksud: input_handle_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); #endif } + +void ksu_ksud_exit() +{ +#ifdef CONFIG_KPROBES + unregister_kprobe(&execve_kp); + // this should be done before unregister vfs_read_kp + // unregister_kprobe(&vfs_read_kp); + unregister_kprobe(&input_event_kp); +#endif +} \ No newline at end of file diff --git a/kernel/ksud.h b/kernel/ksud.h index 5a32a527757f..cc2df243a8f0 100644 --- a/kernel/ksud.h +++ b/kernel/ksud.h @@ -1,10 +1,14 @@ #ifndef __KSU_H_KSUD #define __KSU_H_KSUD +#include + #define KSUD_PATH "/data/adb/ksud" void on_post_fs_data(void); bool ksu_is_safe_mode(void); +extern u32 ksu_devpts_sid; + #endif diff --git a/kernel/manager.c b/kernel/manager.c deleted file mode 100644 index 83a2226ed22c..000000000000 --- a/kernel/manager.c +++ /dev/null @@ -1,92 +0,0 @@ -#include "linux/cred.h" -#include "linux/gfp.h" -#include "linux/slab.h" -#include "linux/uidgid.h" -#include "linux/version.h" - -#include "linux/fdtable.h" -#include "linux/fs.h" -#include "linux/rcupdate.h" - -#include "apk_sign.h" -#include "klog.h" // IWYU pragma: keep -#include "ksu.h" -#include "manager.h" - -uid_t ksu_manager_uid = KSU_INVALID_UID; - -bool become_manager(char *pkg) -{ - struct fdtable *files_table; - int i = 0; - struct path files_path; - char *cwd; - char *buf; - bool result = false; - - // must be zygote's direct child, otherwise any app can fork a new process and - // open manager's apk - if (task_uid(current->real_parent).val != 0) { - pr_info("parent is not zygote!\n"); - return false; - } - - buf = (char *)kmalloc(PATH_MAX, GFP_ATOMIC); - if (!buf) { - pr_err("kalloc path failed.\n"); - return false; - } - - files_table = files_fdtable(current->files); - - int pkg_len = strlen(pkg); - // todo: use iterate_fd - for (i = 0; files_table->fd[i] != NULL; i++) { - files_path = files_table->fd[i]->f_path; - if (!d_is_reg(files_path.dentry)) { - continue; - } - cwd = d_path(&files_path, buf, PATH_MAX); - if (startswith(cwd, "/data/app/") != 0 || - endswith(cwd, "/base.apk") != 0) { - continue; - } - // we have found the apk! - pr_info("found apk: %s\n", cwd); - char *pkg_index = strstr(cwd, pkg); - if (!pkg_index) { - pr_info("apk path not match package name!\n"); - continue; - } - char *next_char = pkg_index + pkg_len; - // because we ensure the cwd must startswith `/data/app` and endswith `base.apk` - // we don't need to check if the pointer is out of bounds - if (*next_char != '-') { - // from android 8.1: http://aospxref.com/android-8.1.0_r81/xref/frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java#17612 - // to android 13: http://aospxref.com/android-13.0.0_r3/xref/frameworks/base/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java#1208 - // /data/app/~~[randomStringA]/[packageName]-[randomStringB] - // the previous char must be `/` and the next char must be `-` - // because we use strstr instead of equals, this is a strong verfication. - pr_info("invalid pkg: %s\n", pkg); - continue; - } - if (is_manager_apk(cwd)) { - // check passed - uid_t uid = current_uid().val; - pr_info("manager uid: %d\n", uid); - - ksu_set_manager_uid(uid); - - result = true; - goto clean; - } else { - pr_info("manager signature invalid!\n"); - } - - break; - } - -clean: - kfree(buf); - return result; -} diff --git a/kernel/manager.h b/kernel/manager.h index 9429d758f89d..be5bbced6f73 100644 --- a/kernel/manager.h +++ b/kernel/manager.h @@ -1,8 +1,8 @@ #ifndef __KSU_H_KSU_MANAGER #define __KSU_H_KSU_MANAGER -#include "linux/cred.h" -#include "linux/types.h" +#include +#include #define KSU_INVALID_UID -1 @@ -33,6 +33,4 @@ static inline void ksu_invalidate_manager_uid() ksu_manager_uid = KSU_INVALID_UID; } -bool become_manager(char *pkg); - #endif diff --git a/kernel/module_api.c b/kernel/module_api.c deleted file mode 100644 index 999d5102741f..000000000000 --- a/kernel/module_api.c +++ /dev/null @@ -1,33 +0,0 @@ -#include "linux/kallsyms.h" - -#define RE_EXPORT_SYMBOL1(ret, func, t1, v1) \ - ret ksu_##func(t1 v1) \ - { \ - return func(v1); \ - } \ - EXPORT_SYMBOL(ksu_##func); - -#define RE_EXPORT_SYMBOL2(ret, func, t1, v1, t2, v2) \ - ret ksu_##func(t1 v1, t2 v2) \ - { \ - return func(v1, v2); \ - } \ - EXPORT_SYMBOL(ksu_##func); - -RE_EXPORT_SYMBOL1(unsigned long, kallsyms_lookup_name, const char *, name) - -// RE_EXPORT_SYMBOL2(int, register_kprobe, struct kprobe *, p) -// RE_EXPORT_SYMBOL2(void, unregister_kprobe, struct kprobe *, p) - -// RE_EXPORT_SYMBOL2(int, register_kprobe, struct kprobe *, p) -// RE_EXPORT_SYMBOL2(void, unregister_kprobe, struct kprobe *, p) - -// int ksu_register_kprobe(struct kprobe *p); -// void ksu_unregister_kprobe(struct kprobe *p); -// int ksu_register_kprobes(struct kprobe **kps, int num); -// void ksu_unregister_kprobes(struct kprobe **kps, int num); - -// int ksu_register_kretprobe(struct kretprobe *rp); -// void unregister_kretprobe(struct kretprobe *rp); -// int register_kretprobes(struct kretprobe **rps, int num); -// void unregister_kretprobes(struct kretprobe **rps, int num); diff --git a/kernel/selinux/rules.c b/kernel/selinux/rules.c index 2504bd7da21a..b4e6eae0c1d7 100644 --- a/kernel/selinux/rules.c +++ b/kernel/selinux/rules.c @@ -1,6 +1,6 @@ -#include "linux/uaccess.h" -#include "linux/types.h" -#include "linux/version.h" +#include +#include +#include #include "../klog.h" // IWYU pragma: keep #include "selinux.h" @@ -9,9 +9,7 @@ #include "linux/lsm_audit.h" #include "xfrm.h" -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0) #define SELINUX_POLICY_INSTEAD_SELINUX_SS -#endif #define KERNEL_SU_DOMAIN "su" #define KERNEL_SU_FILE "ksu_file" @@ -21,18 +19,8 @@ static struct policydb *get_policydb(void) { struct policydb *db; -// selinux_state does not exists before 4.19 -#ifdef KSU_COMPAT_USE_SELINUX_STATE -#ifdef SELINUX_POLICY_INSTEAD_SELINUX_SS struct selinux_policy *policy = rcu_dereference(selinux_state.policy); db = &policy->policydb; -#else - struct selinux_ss *ss = rcu_dereference(selinux_state.ss); - db = &ss->policydb; -#endif -#else - db = &policydb; -#endif return db; } @@ -63,11 +51,17 @@ void apply_kernelsu_rules() 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 @@ -83,6 +77,7 @@ void apply_kernelsu_rules() 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"); @@ -123,11 +118,9 @@ void apply_kernelsu_rules() // Allow all binder transactions ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "binder", ALL); - // Allow system server devpts - ksu_allow(db, "system_server", "untrusted_app_all_devpts", "chr_file", - "read"); - ksu_allow(db, "system_server", "untrusted_app_all_devpts", "chr_file", - "write"); + // Allow system server kill su process + ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "getpgid"); + ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "sigkill"); rcu_read_unlock(); } @@ -176,7 +169,7 @@ static int get_object(char *buf, char __user *user_object, size_t buf_sz, // reset avc cache table, otherwise the new rules will not take effect if already denied static void reset_avc_cache() { -#ifndef KSU_COMPAT_USE_SELINUX_STATE +#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) avc_ss_reset(0); selnl_notify_policyload(0); selinux_status_update_policyload(0); diff --git a/kernel/selinux/selinux.c b/kernel/selinux/selinux.c index 0260fd570ed0..17a25dae5c70 100644 --- a/kernel/selinux/selinux.c +++ b/kernel/selinux/selinux.c @@ -2,14 +2,9 @@ #include "objsec.h" #include "linux/version.h" #include "../klog.h" // IWYU pragma: keep -#ifndef KSU_COMPAT_USE_SELINUX_STATE -#include "avc.h" -#endif #define KERNEL_SU_DOMAIN "u:r:su:s0" -static u32 ksu_sid; - static int transive_to_domain(const char *domain) { struct cred *cred; @@ -31,9 +26,6 @@ static int transive_to_domain(const char *domain) domain, sid, error); } if (!error) { - if (!ksu_sid) - ksu_sid = sid; - tsec->sid = sid; tsec->create_sid = 0; tsec->keycreate_sid = 0; @@ -60,32 +52,20 @@ if (!is_domain_permissive) { void setenforce(bool enforce) { #ifdef CONFIG_SECURITY_SELINUX_DEVELOP -#ifdef KSU_COMPAT_USE_SELINUX_STATE selinux_state.enforcing = enforce; -#else - selinux_enforcing = enforce; -#endif #endif } bool getenforce() { #ifdef CONFIG_SECURITY_SELINUX_DISABLE -#ifdef KSU_COMPAT_USE_SELINUX_STATE if (selinux_state.disabled) { -#else - if (selinux_disabled) { -#endif return false; } #endif #ifdef CONFIG_SECURITY_SELINUX_DEVELOP -#ifdef KSU_COMPAT_USE_SELINUX_STATE return selinux_state.enforcing; -#else - return selinux_enforcing; -#endif #else return true; #endif @@ -106,7 +86,16 @@ static inline u32 current_sid(void) bool is_ksu_domain() { - return ksu_sid && current_sid() == ksu_sid; + 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; } bool is_zygote(void *sec) @@ -117,9 +106,25 @@ bool is_zygote(void *sec) } char *domain; u32 seclen; + bool result; int err = security_secid_to_secctx(tsec->sid, &domain, &seclen); if (err) { return false; } - return strncmp("u:r:zygote:s0", domain, seclen) == 0; -} \ No newline at end of file + result = strncmp("u:r:zygote:s0", domain, seclen) == 0; + security_release_secctx(domain, seclen); + return result; +} + +#define DEVPTS_DOMAIN "u:object_r:ksu_file:s0" + +u32 ksu_get_devpts_sid() +{ + 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; +} diff --git a/kernel/selinux/selinux.h b/kernel/selinux/selinux.h index 0c4978568af7..88f1e7d30961 100644 --- a/kernel/selinux/selinux.h +++ b/kernel/selinux/selinux.h @@ -4,10 +4,6 @@ #include "linux/types.h" #include "linux/version.h" -#if (LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0)) || defined(KSU_COMPAT_HAS_SELINUX_STATE) -#define KSU_COMPAT_USE_SELINUX_STATE -#endif - void setup_selinux(const char *); void setenforce(bool); @@ -20,4 +16,6 @@ bool is_zygote(void *cred); void apply_kernelsu_rules(); +u32 ksu_get_devpts_sid(); + #endif diff --git a/kernel/selinux/sepolicy.c b/kernel/selinux/sepolicy.c index fd40cd23f580..7759602c765e 100644 --- a/kernel/selinux/sepolicy.c +++ b/kernel/selinux/sepolicy.c @@ -1,26 +1,15 @@ -#include "sepolicy.h" -#include "linux/gfp.h" -#include "linux/printk.h" -#include "linux/slab.h" -#include "linux/version.h" +#include +#include +#include +#include +#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 -/* - * 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) -#ifdef HISI_SELINUX_EBITMAP_RO -#define CONFIG_IS_HW_HISI -#endif -#endif - ////////////////////////////////////////////////////// // Declaration ////////////////////////////////////////////////////// @@ -535,7 +524,6 @@ static bool add_filename_trans(struct policydb *db, const char *s, return false; } -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 7, 0) struct filename_trans_key key; key.ttype = tgt->value; key.tclass = cls->value; @@ -543,13 +531,8 @@ static bool add_filename_trans(struct policydb *db, const char *s, struct filename_trans_datum *last = NULL; -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0) struct filename_trans_datum *trans = policydb_filenametr_search(db, &key); -#else - struct filename_trans_datum *trans = - hashtab_search(&db->filename_trans, &key); -#endif while (trans) { if (ebitmap_get_bit(&trans->stypes, src->value - 1)) { // Duplicate, overwrite existing data and return @@ -578,39 +561,6 @@ static bool add_filename_trans(struct policydb *db, const char *s, db->compat_filename_trans_count++; return ebitmap_set_bit(&trans->stypes, src->value - 1, 1) == 0; -#else // < 5.7.0, has no filename_trans_key, but struct filename_trans - - struct filename_trans key; - key.ttype = tgt->value; - key.tclass = cls->value; - key.name = (char *)o; - - struct filename_trans_datum *trans = - hashtab_search(db->filename_trans, &key); - - if (trans == NULL) { - trans = (struct filename_trans_datum *)kcalloc(sizeof(*trans), - 1, GFP_ATOMIC); - if (!trans) { - pr_err("add_filename_trans: Failed to alloc datum\n"); - return false; - } - struct filename_trans *new_key = - (struct filename_trans *)kmalloc(sizeof(*new_key), - GFP_ATOMIC); - if (!new_key) { - pr_err("add_filename_trans: Failed to alloc new_key\n"); - return false; - } - *new_key = key; - new_key->name = kstrdup(key.name, GFP_ATOMIC); - trans->otype = def->value; - hashtab_insert(db->filename_trans, new_key, trans); - } - - return ebitmap_set_bit(&db->filename_trans_ttypes, src->value - 1, 1) == - 0; -#endif } static bool add_genfscon(struct policydb *db, const char *fs_name, @@ -619,9 +569,24 @@ static bool add_genfscon(struct policydb *db, const char *fs_name, 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; +} + static bool add_type(struct policydb *db, const char *type_name, bool attr) { -#ifdef KSU_SUPPORT_ADD_TYPE struct type_datum *type = symtab_search(&db->p_types, type_name); if (type) { pr_warn("Type %s already exists\n", type_name); @@ -651,30 +616,30 @@ static bool add_type(struct policydb *db, const char *type_name, bool attr) return false; } -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 1, 0) - size_t new_size = sizeof(struct ebitmap) * db->p_types.nprim; struct ebitmap *new_type_attr_map_array = - (krealloc(db->type_attr_map_array, new_size, GFP_ATOMIC)); - - struct type_datum **new_type_val_to_struct = - krealloc(db->type_val_to_struct, - sizeof(*db->type_val_to_struct) * db->p_types.nprim, - GFP_ATOMIC); + 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; } + 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; } char **new_val_to_name_types = - krealloc(db->sym_val_to_name[SYM_TYPES], - sizeof(char *) * db->symtab[SYM_TYPES].nprim, - GFP_KERNEL); + 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; @@ -697,171 +662,6 @@ static bool add_type(struct policydb *db, const char *type_name, bool attr) } return true; -#elif defined(CONFIG_IS_HW_HISI) - /* - * Huawei use type_attr_map and type_val_to_struct. - * And use ebitmap not flex_array. - */ - size_t new_size = sizeof(struct ebitmap) * db->p_types.nprim; - struct ebitmap *new_type_attr_map = - (krealloc(db->type_attr_map, new_size, GFP_ATOMIC)); - - struct type_datum **new_type_val_to_struct = - krealloc(db->type_val_to_struct, - sizeof(*db->type_val_to_struct) * db->p_types.nprim, - GFP_ATOMIC); - - if (!new_type_attr_map) { - pr_err("add_type: alloc type_attr_map 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 = - krealloc(db->sym_val_to_name[SYM_TYPES], - sizeof(char *) * db->symtab[SYM_TYPES].nprim, - GFP_KERNEL); - if (!new_val_to_name_types) { - pr_err("add_type: alloc val_to_name failed\n"); - return false; - } - - db->type_attr_map = new_type_attr_map; - ebitmap_init(&db->type_attr_map[value - 1], HISI_SELINUX_EBITMAP_RO); - ebitmap_set_bit(&db->type_attr_map[value - 1], value - 1, 1); - - db->type_val_to_struct = new_type_val_to_struct; - db->type_val_to_struct[value - 1] = type; - - db->sym_val_to_name[SYM_TYPES] = new_val_to_name_types; - db->sym_val_to_name[SYM_TYPES][value - 1] = key; - - int i; - for (i = 0; i < db->p_roles.nprim; ++i) { - ebitmap_set_bit(&db->role_val_to_struct[i]->types, value - 1, - 1); - } - - return true; -#else - // flex_array is not extensible, we need to create a new bigger one instead - struct flex_array *new_type_attr_map_array = - flex_array_alloc(sizeof(struct ebitmap), db->p_types.nprim, - GFP_ATOMIC | __GFP_ZERO); - - struct flex_array *new_type_val_to_struct = - flex_array_alloc(sizeof(struct type_datum *), db->p_types.nprim, - GFP_ATOMIC | __GFP_ZERO); - - struct flex_array *new_val_to_name_types = - flex_array_alloc(sizeof(char *), db->symtab[SYM_TYPES].nprim, - GFP_ATOMIC | __GFP_ZERO); - - if (!new_type_attr_map_array) { - pr_err("add_type: alloc type_attr_map_array failed\n"); - return false; - } - - if (!new_type_val_to_struct) { - pr_err("add_type: alloc type_val_to_struct failed\n"); - return false; - } - - if (!new_val_to_name_types) { - pr_err("add_type: alloc val_to_name failed\n"); - return false; - } - - // preallocate so we don't have to worry about the put ever failing - if (flex_array_prealloc(new_type_attr_map_array, 0, db->p_types.nprim, - GFP_ATOMIC | __GFP_ZERO)) { - pr_err("add_type: prealloc type_attr_map_array failed\n"); - return false; - } - - if (flex_array_prealloc(new_type_val_to_struct, 0, db->p_types.nprim, - GFP_ATOMIC | __GFP_ZERO)) { - pr_err("add_type: prealloc type_val_to_struct_array failed\n"); - return false; - } - - if (flex_array_prealloc(new_val_to_name_types, 0, - db->symtab[SYM_TYPES].nprim, - GFP_ATOMIC | __GFP_ZERO)) { - pr_err("add_type: prealloc val_to_name_types failed\n"); - return false; - } - - int j; - void *old_elem; - // copy the old data or pointers to new flex arrays - for (j = 0; j < db->type_attr_map_array->total_nr_elements; j++) { - old_elem = flex_array_get(db->type_attr_map_array, j); - if (old_elem) - flex_array_put(new_type_attr_map_array, j, old_elem, - GFP_ATOMIC | __GFP_ZERO); - } - - for (j = 0; j < db->type_val_to_struct_array->total_nr_elements; j++) { - old_elem = flex_array_get_ptr(db->type_val_to_struct_array, j); - if (old_elem) - flex_array_put_ptr(new_type_val_to_struct, j, old_elem, - GFP_ATOMIC | __GFP_ZERO); - } - - for (j = 0; j < db->symtab[SYM_TYPES].nprim; j++) { - old_elem = - flex_array_get_ptr(db->sym_val_to_name[SYM_TYPES], j); - if (old_elem) - flex_array_put_ptr(new_val_to_name_types, j, old_elem, - GFP_ATOMIC | __GFP_ZERO); - } - - // store the pointer of old flex arrays first, when assigning new ones we - // should free it - struct flex_array *old_fa; - - old_fa = db->type_attr_map_array; - db->type_attr_map_array = new_type_attr_map_array; - if (old_fa) { - flex_array_free(old_fa); - } - - ebitmap_init(flex_array_get(db->type_attr_map_array, value - 1)); - ebitmap_set_bit(flex_array_get(db->type_attr_map_array, value - 1), - value - 1, 1); - - old_fa = db->type_val_to_struct_array; - db->type_val_to_struct_array = new_type_val_to_struct; - if (old_fa) { - flex_array_free(old_fa); - } - flex_array_put_ptr(db->type_val_to_struct_array, value - 1, type, - GFP_ATOMIC | __GFP_ZERO); - - old_fa = db->sym_val_to_name[SYM_TYPES]; - db->sym_val_to_name[SYM_TYPES] = new_val_to_name_types; - if (old_fa) { - flex_array_free(old_fa); - } - flex_array_put_ptr(db->sym_val_to_name[SYM_TYPES], value - 1, key, - GFP_ATOMIC | __GFP_ZERO); - - 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; -#endif - -#else - return false; -#endif } static bool set_type_state(struct policydb *db, const char *type_name, @@ -896,18 +696,7 @@ static bool set_type_state(struct policydb *db, const char *type_name, static void add_typeattribute_raw(struct policydb *db, struct type_datum *type, struct type_datum *attr) { -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 1, 0) struct ebitmap *sattr = &db->type_attr_map_array[type->value - 1]; -#elif defined(CONFIG_IS_HW_HISI) - /* - * HISI_SELINUX_EBITMAP_RO is Huawei's unique features. - */ - struct ebitmap *sattr = &db->type_attr_map[type->value - 1], - HISI_SELINUX_EBITMAP_RO; -#else - struct ebitmap *sattr = - flex_array_get(db->type_attr_map_array, type->value - 1); -#endif ebitmap_set_bit(sattr, attr->value - 1, 1); struct hashtab_node *node; diff --git a/kernel/selinux/sepolicy.h b/kernel/selinux/sepolicy.h index a50712369f42..675d1499e46d 100644 --- a/kernel/selinux/sepolicy.h +++ b/kernel/selinux/sepolicy.h @@ -1,7 +1,7 @@ #ifndef __KSU_H_SEPOLICY #define __KSU_H_SEPOLICY -#include "linux/types.h" +#include #include "ss/policydb.h" diff --git a/kernel/setup.sh b/kernel/setup.sh index 556a689dac9f..e688dbaf3ae5 100755 --- a/kernel/setup.sh +++ b/kernel/setup.sh @@ -1,50 +1,75 @@ #!/bin/sh -set -eux +set -eu GKI_ROOT=$(pwd) -echo "[+] GKI_ROOT: $GKI_ROOT" +display_usage() { + echo "Usage: $0 [--cleanup | ]" + echo " --cleanup: Cleans up previous modifications made by the script." + echo " : Sets up or updates the KernelSU to specified tag or commit." + echo " -h, --help: Displays this usage information." + echo " (no args): Sets up or updates the KernelSU environment to the latest tagged version." +} -if test -d "$GKI_ROOT/common/drivers"; then - DRIVER_DIR="$GKI_ROOT/common/drivers" -elif test -d "$GKI_ROOT/drivers"; then - DRIVER_DIR="$GKI_ROOT/drivers" -else - echo '[ERROR] "drivers/" directory is not found.' - echo '[+] You should modify this script by yourself.' - exit 127 -fi +initialize_variables() { + if test -d "$GKI_ROOT/common/drivers"; then + DRIVER_DIR="$GKI_ROOT/common/drivers" + elif test -d "$GKI_ROOT/drivers"; then + DRIVER_DIR="$GKI_ROOT/drivers" + else + echo '[ERROR] "drivers/" directory not found.' + exit 127 + fi -test -d "$GKI_ROOT/KernelSU" || git clone https://github.com/tiann/KernelSU -cd "$GKI_ROOT/KernelSU" -git stash -if [ "$(git status | grep -Po 'v\d+(\.\d+)*' | head -n1)" ]; then - git checkout main -fi -git pull -if [ -z "${1-}" ]; then - git checkout "$(git describe --abbrev=0 --tags)" -else - git checkout "$1" -fi -cd "$GKI_ROOT" + DRIVER_MAKEFILE=$DRIVER_DIR/Makefile + DRIVER_KCONFIG=$DRIVER_DIR/Kconfig +} -echo "[+] GKI_ROOT: $GKI_ROOT" -echo "[+] Copy kernel su driver to $DRIVER_DIR" +# Reverts modifications made by this script +perform_cleanup() { + echo "[+] Cleaning up..." + [ -L "$DRIVER_DIR/kernelsu" ] && rm "$DRIVER_DIR/kernelsu" && echo "[-] Symlink removed." + grep -q "kernelsu" "$DRIVER_MAKEFILE" && sed -i '/kernelsu/d' "$DRIVER_MAKEFILE" && echo "[-] Makefile reverted." + grep -q "drivers/kernelsu/Kconfig" "$DRIVER_KCONFIG" && sed -i '/drivers\/kernelsu\/Kconfig/d' "$DRIVER_KCONFIG" && echo "[-] Kconfig reverted." + if [ -d "$GKI_ROOT/KernelSU" ]; then + rm -rf "$GKI_ROOT/KernelSU" && echo "[-] KernelSU directory deleted." + fi +} -cd "$DRIVER_DIR" -if test -d "$GKI_ROOT/common/drivers"; then - ln -sf "../../KernelSU/kernel" "kernelsu" -elif test -d "$GKI_ROOT/drivers"; then - ln -sf "../KernelSU/kernel" "kernelsu" -fi -cd "$GKI_ROOT" - -echo '[+] Add kernel su driver to Makefile' +# Sets up or update KernelSU environment +setup_kernelsu() { + echo "[+] Setting up KernelSU..." + test -d "$GKI_ROOT/KernelSU" || git clone https://github.com/tiann/KernelSU && echo "[+] Repository cloned." + cd "$GKI_ROOT/KernelSU" + git stash && echo "[-] Stashed current changes." + if [ "$(git status | grep -Po 'v\d+(\.\d+)*' | head -n1)" ]; then + git checkout main && echo "[-] Switched to main branch." + fi + git pull && echo "[+] Repository updated." + if [ -z "${1-}" ]; then + git checkout "$(git describe --abbrev=0 --tags)" && echo "[-] Checked out latest tag." + else + git checkout "$1" && echo "[-] Checked out $1." || echo "[-] Checkout default branch" + fi + cd "$DRIVER_DIR" + ln -sf "$(realpath --relative-to="$DRIVER_DIR" "$GKI_ROOT/KernelSU/kernel")" "kernelsu" && echo "[+] Symlink created." -DRIVER_MAKEFILE=$DRIVER_DIR/Makefile -DRIVER_KCONFIG=$DRIVER_DIR/Kconfig -grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "obj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" -grep -q "kernelsu" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" + # Add entries in Makefile and Kconfig if not already existing + grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" && echo "[+] Modified Makefile." + grep -q "source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" && echo "[+] Modified Kconfig." + echo '[+] Done.' +} -echo '[+] Done.' \ No newline at end of file +# Process command-line arguments +if [ "$#" -eq 0 ]; then + initialize_variables + setup_kernelsu +elif [ "$1" = "-h" ] || [ "$1" = "--help" ]; then + display_usage +elif [ "$1" = "--cleanup" ]; then + initialize_variables + perform_cleanup +else + initialize_variables + setup_kernelsu "$@" +fi diff --git a/kernel/sucompat.c b/kernel/sucompat.c index ef08cabdb5e0..966cbf8fa854 100644 --- a/kernel/sucompat.c +++ b/kernel/sucompat.c @@ -1,17 +1,16 @@ -#include "asm/current.h" -#include "linux/cred.h" -#include "linux/err.h" -#include "linux/fs.h" -#include "linux/kprobes.h" -#include "linux/types.h" -#include "linux/uaccess.h" -#include "linux/version.h" -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0) -#include "linux/sched/task_stack.h" -#else -#include "linux/sched.h" -#endif - +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "objsec.h" #include "allowlist.h" #include "arch.h" #include "klog.h" // IWYU pragma: keep @@ -39,8 +38,15 @@ static char __user *sh_user_path(void) return userspace_stack_buffer(sh_path, sizeof(sh_path)); } +static char __user *ksud_user_path(void) +{ + static const char ksud_path[] = KSUD_PATH; + + return userspace_stack_buffer(ksud_path, sizeof(ksud_path)); +} + int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, - int *flags) + int *__unused_flags) { const char su[] = SU_PATH; @@ -75,19 +81,35 @@ int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags) 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)); +#else ksu_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(); } +#endif return 0; } // 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) + void *__never_use_argv, void *__never_use_envp, + int *__never_use_flags) { struct filename *filename; const char sh[] = KSUD_PATH; @@ -115,77 +137,124 @@ int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, 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]; + + 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; +} + +int ksu_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 (!ksu_is_allow_uid(uid)) + return 0; + + if (ksu_devpts_sid) { + struct inode_security_struct *sec = selinux_inode(inode); + if (sec) { + sec->sid = ksu_devpts_sid; + } + } + + return 0; +} + #ifdef CONFIG_KPROBES -static int faccessat_handler_pre(struct kprobe *p, struct pt_regs *regs) +static int sys_faccessat_handler_pre(struct kprobe *p, struct pt_regs *regs) { - int *dfd = (int *)PT_REGS_PARM1(regs); - const char __user **filename_user = (const char **)&PT_REGS_PARM2(regs); - int *mode = (int *)&PT_REGS_PARM3(regs); - // Both sys_ and do_ is C function - int *flags = (int *)&PT_REGS_CCALL_PARM4(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, flags); + return ksu_handle_faccessat(dfd, filename_user, mode, NULL); } -static int newfstatat_handler_pre(struct kprobe *p, struct pt_regs *regs) +static int sys_newfstatat_handler_pre(struct kprobe *p, struct pt_regs *regs) { - int *dfd = (int *)&PT_REGS_PARM1(regs); - const char __user **filename_user = (const char **)&PT_REGS_PARM2(regs); -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0) -// static int vfs_statx(int dfd, const char __user *filename, int flags, struct kstat *stat, u32 request_mask) - int *flags = (int *)&PT_REGS_PARM3(regs); -#else -// int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,int flag) - int *flags = (int *)&PT_REGS_CCALL_PARM4(regs); -#endif + 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); } -// https://elixir.bootlin.com/linux/v5.10.158/source/fs/exec.c#L1864 -static int execve_handler_pre(struct kprobe *p, struct pt_regs *regs) +static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs) { - int *fd = (int *)&PT_REGS_PARM1(regs); - struct filename **filename_ptr = - (struct filename **)&PT_REGS_PARM2(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_execveat_sucompat(fd, filename_ptr, NULL, NULL, NULL); + return ksu_handle_execve_sucompat(AT_FDCWD, filename_user, NULL, NULL, + NULL); } static struct kprobe faccessat_kp = { -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 17, 0) - .symbol_name = "do_faccessat", -#else - .symbol_name = "sys_faccessat", -#endif - .pre_handler = faccessat_handler_pre, + .symbol_name = SYS_FACCESSAT_SYMBOL, + .pre_handler = sys_faccessat_handler_pre, }; static struct kprobe newfstatat_kp = { -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 11, 0) - .symbol_name = "vfs_statx", -#else - .symbol_name = "vfs_fstatat", -#endif - .pre_handler = newfstatat_handler_pre, + .symbol_name = SYS_NEWFSTATAT_SYMBOL, + .pre_handler = sys_newfstatat_handler_pre, }; static struct kprobe execve_kp = { -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0) - .symbol_name = "do_execveat_common", -#elif LINUX_VERSION_CODE >= KERNEL_VERSION(4, 19, 0) - .symbol_name = "__do_execve_file", -#elif LINUX_VERSION_CODE >= KERNEL_VERSION(3, 19, 0) - .symbol_name = "do_execveat_common", -#endif - .pre_handler = execve_handler_pre, + .symbol_name = SYS_EXECVE_SYMBOL, + .pre_handler = sys_execve_handler_pre, }; +static int pts_unix98_lookup_pre(struct kprobe *p, struct pt_regs *regs) +{ + struct inode *inode; + struct file *file = (struct file *)PT_REGS_PARM2(regs); + inode = file->f_path.dentry->d_inode; + + return ksu_handle_devpts(inode); +} + +static struct kprobe pts_unix98_lookup_kp = { .symbol_name = + "pts_unix98_lookup", + .pre_handler = + pts_unix98_lookup_pre }; + #endif // sucompat: permited process can execute 'su' to gain root access. -void ksu_enable_sucompat() +void ksu_sucompat_init() { #ifdef CONFIG_KPROBES int ret; @@ -195,5 +264,17 @@ void ksu_enable_sucompat() pr_info("sucompat: newfstatat_kp: %d\n", ret); ret = register_kprobe(&faccessat_kp); pr_info("sucompat: faccessat_kp: %d\n", ret); + ret = register_kprobe(&pts_unix98_lookup_kp); + pr_info("sucompat: devpts_kp: %d\n", ret); +#endif +} + +void ksu_sucompat_exit() +{ +#ifdef CONFIG_KPROBES + unregister_kprobe(&execve_kp); + unregister_kprobe(&newfstatat_kp); + unregister_kprobe(&faccessat_kp); + unregister_kprobe(&pts_unix98_lookup_kp); #endif } diff --git a/kernel/throne_tracker.c b/kernel/throne_tracker.c new file mode 100644 index 000000000000..d7c1dae14a17 --- /dev/null +++ b/kernel/throne_tracker.c @@ -0,0 +1,378 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "allowlist.h" +#include "klog.h" // IWYU pragma: keep +#include "ksu.h" +#include "manager.h" +#include "throne_tracker.h" +#include "kernel_compat.h" + +uid_t ksu_manager_uid = KSU_INVALID_UID; + +#define SYSTEM_PACKAGES_LIST_PATH "/data/system/packages.list.tmp" + +struct uid_data { + struct list_head list; + u32 uid; + char package[KSU_MAX_PACKAGE_NAME]; +}; + +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; + + const char *last_slash = NULL; + const char *second_last_slash = NULL; + + int i; + for (i = len - 1; i >= 0; i--) { + if (path[i] == '/') { + if (!last_slash) { + last_slash = &path[i]; + } else { + second_last_slash = &path[i]; + break; + } + } + } + + if (!last_slash || !second_last_slash) + return -1; + + const char *last_hyphen = strchr(second_last_slash, '-'); + if (!last_hyphen || last_hyphen > last_slash) + return -1; + + int pkg_len = last_hyphen - second_last_slash - 1; + if (pkg_len >= KSU_MAX_PACKAGE_NAME || pkg_len <= 0) + return -1; + + // Copying the package name + strncpy(pkg, second_last_slash + 1, pkg_len); + pkg[pkg_len] = '\0'; + + return 0; +} + +static void crown_manager(const char *apk, struct list_head *uid_data) +{ + char pkg[KSU_MAX_PACKAGE_NAME]; + if (get_pkg_from_apk_path(pkg, apk) < 0) { + pr_err("Failed to get package name from apk path: %s\n", apk); + return; + } + + pr_info("manager pkg: %s\n", pkg); + +#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; + } +#endif + struct list_head *list = (struct list_head *)uid_data; + struct uid_data *np; + + list_for_each_entry (np, list, list) { + if (strncmp(np->package, pkg, KSU_MAX_PACKAGE_NAME) == 0) { + pr_info("Crowning manager: %s(uid=%d)\n", pkg, np->uid); + ksu_set_manager_uid(np->uid); + break; + } + } +} + +#define DATA_PATH_LEN 384 // 384 is enough for /data/app//base.apk + +struct data_path { + char dirpath[DATA_PATH_LEN]; + int depth; + struct list_head list; +}; + +struct apk_path_hash { + unsigned int hash; + bool exists; + struct list_head list; +}; + +static struct list_head apk_path_hash_list = LIST_HEAD_INIT(apk_path_hash_list); + +struct my_dir_context { + struct dir_context ctx; + struct list_head *data_path_list; + char *parent_dir; + void *private_data; + int depth; + int *stop; +}; +// https://docs.kernel.org/filesystems/porting.html +// filldir_t (readdir callbacks) calling conventions have changed. Instead of returning 0 or -E... it returns bool now. false means "no more" (as -E... used to) and true - "keep going" (as 0 in old calling conventions). Rationale: callers never looked at specific -E... values anyway. -> iterate_shared() instances require no changes at all, all filldir_t ones in the tree converted. +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 1, 0) +#define FILLDIR_RETURN_TYPE bool +#define FILLDIR_ACTOR_CONTINUE true +#define FILLDIR_ACTOR_STOP false +#else +#define FILLDIR_RETURN_TYPE int +#define FILLDIR_ACTOR_CONTINUE 0 +#define FILLDIR_ACTOR_STOP -EINVAL +#endif + +FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name, + int namelen, loff_t off, u64 ino, + unsigned int d_type) +{ + struct my_dir_context *my_ctx = + container_of(ctx, struct my_dir_context, ctx); + char dirpath[DATA_PATH_LEN]; + + if (!my_ctx) { + pr_err("Invalid context\n"); + return FILLDIR_ACTOR_STOP; + } + if (my_ctx->stop && *my_ctx->stop) { + pr_info("Stop searching\n"); + return FILLDIR_ACTOR_STOP; + } + + if (!strncmp(name, "..", namelen) || !strncmp(name, ".", namelen)) + return FILLDIR_ACTOR_CONTINUE; // Skip "." and ".." + + if (snprintf(dirpath, DATA_PATH_LEN, "%s/%.*s", my_ctx->parent_dir, + namelen, name) >= DATA_PATH_LEN) { + pr_err("Path too long: %s/%.*s\n", my_ctx->parent_dir, namelen, + name); + return FILLDIR_ACTOR_CONTINUE; + } + + if (d_type == DT_DIR && my_ctx->depth > 0 && + (my_ctx->stop && !*my_ctx->stop)) { + struct data_path *data = kmalloc(sizeof(struct data_path), GFP_ATOMIC); + + if (!data) { + pr_err("Failed to allocate memory for %s\n", dirpath); + return FILLDIR_ACTOR_CONTINUE; + } + + strscpy(data->dirpath, dirpath, DATA_PATH_LEN); + data->depth = my_ctx->depth - 1; + list_add_tail(&data->list, my_ctx->data_path_list); + } else { + if ((namelen == 8) && (strncmp(name, "base.apk", namelen) == 0)) { + struct apk_path_hash *pos, *n; + unsigned int hash = full_name_hash(NULL, dirpath, strlen(dirpath)); + list_for_each_entry(pos, &apk_path_hash_list, list) { + if (hash == pos->hash) { + pos->exists = true; + return FILLDIR_ACTOR_CONTINUE; + } + } + + bool is_manager = is_manager_apk(dirpath); + pr_info("Found new base.apk at path: %s, is_manager: %d\n", + dirpath, is_manager); + if (is_manager) { + crown_manager(dirpath, my_ctx->private_data); + *my_ctx->stop = 1; + + // Manager found, clear APK cache list + list_for_each_entry_safe(pos, n, &apk_path_hash_list, list) { + list_del(&pos->list); + kfree(pos); + } + } else { + struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC); + apk_data->hash = hash; + apk_data->exists = true; + list_add_tail(&apk_data->list, &apk_path_hash_list); + } + } + } + + return FILLDIR_ACTOR_CONTINUE; +} + +void search_manager(const char *path, int depth, struct list_head *uid_data) +{ + int i, stop = 0; + struct list_head data_path_list; + INIT_LIST_HEAD(&data_path_list); + + // Initialize APK cache list + struct apk_path_hash *pos, *n; + list_for_each_entry(pos, &apk_path_hash_list, list) { + pos->exists = false; + } + + // First depth + struct data_path data; + strscpy(data.dirpath, path, DATA_PATH_LEN); + data.depth = depth; + list_add_tail(&data.list, &data_path_list); + + for (i = depth; i > 0; i--) { + struct data_path *pos, *n; + + list_for_each_entry_safe(pos, n, &data_path_list, list) { + struct my_dir_context ctx = { .ctx.actor = my_actor, + .data_path_list = &data_path_list, + .parent_dir = pos->dirpath, + .private_data = uid_data, + .depth = pos->depth, + .stop = &stop }; + struct file *file; + + if (!stop) { + file = 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; + } + + iterate_dir(file, &ctx.ctx); + filp_close(file, NULL); + } +skip_iterate: + list_del(&pos->list); + if (pos != &data) + kfree(pos); + } + } + + // Remove stale cached APK entries + list_for_each_entry_safe(pos, n, &apk_path_hash_list, list) { + if (!pos->exists) { + list_del(&pos->list); + kfree(pos); + } + } +} + +static bool is_uid_exist(uid_t uid, char *package, void *data) +{ + struct list_head *list = (struct list_head *)data; + struct uid_data *np; + + bool exist = false; + list_for_each_entry (np, list, list) { + if (np->uid == uid % 100000 && + strncmp(np->package, package, KSU_MAX_PACKAGE_NAME) == 0) { + exist = true; + break; + } + } + return exist; +} + +void track_throne() +{ + struct file *fp = + ksu_filp_open_compat(SYSTEM_PACKAGES_LIST_PATH, O_RDONLY, 0); + if (IS_ERR(fp)) { + pr_err("%s: open " SYSTEM_PACKAGES_LIST_PATH " failed: %ld\n", + __func__, PTR_ERR(fp)); + return; + } + + struct list_head uid_list; + INIT_LIST_HEAD(&uid_list); + + char chr = 0; + loff_t pos = 0; + loff_t line_start = 0; + char buf[KSU_MAX_PACKAGE_NAME]; + for (;;) { + ssize_t count = + ksu_kernel_read_compat(fp, &chr, sizeof(chr), &pos); + if (count != sizeof(chr)) + break; + if (chr != '\n') + continue; + + count = ksu_kernel_read_compat(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); + + // now update uid list + struct uid_data *np; + struct uid_data *n; + + // first, check if manager_uid exist! + bool 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; + } + } + + if (!manager_exist) { + if (ksu_is_manager_uid_valid()) { + pr_info("manager is uninstalled, invalidate it!\n"); + ksu_invalidate_manager_uid(); + } + pr_info("Searching manager...\n"); + search_manager("/data/app", 2, &uid_list); + pr_info("Search manager finished\n"); + } + + // then prune the allowlist + ksu_prune_allowlist(is_uid_exist, &uid_list); +out: + // free uid_list + list_for_each_entry_safe (np, n, &uid_list, list) { + list_del(&np->list); + kfree(np); + } +} + +void ksu_throne_tracker_init() +{ + // nothing to do +} + +void ksu_throne_tracker_exit() +{ + // nothing to do +} diff --git a/kernel/throne_tracker.h b/kernel/throne_tracker.h new file mode 100644 index 000000000000..5d7f477003ac --- /dev/null +++ b/kernel/throne_tracker.h @@ -0,0 +1,10 @@ +#ifndef __KSU_H_UID_OBSERVER +#define __KSU_H_UID_OBSERVER + +void ksu_throne_tracker_init(); + +void ksu_throne_tracker_exit(); + +void track_throne(); + +#endif diff --git a/kernel/uid_observer.c b/kernel/uid_observer.c deleted file mode 100644 index 09b0d71f6a32..000000000000 --- a/kernel/uid_observer.c +++ /dev/null @@ -1,144 +0,0 @@ -#include "linux/err.h" -#include "linux/fs.h" -#include "linux/list.h" -#include "linux/slab.h" -#include "linux/string.h" -#include "linux/types.h" -#include "linux/version.h" -#include "linux/workqueue.h" - -#include "allowlist.h" -#include "klog.h" // IWYU pragma: keep -#include "ksu.h" -#include "manager.h" -#include "uid_observer.h" -#include "kernel_compat.h" - -#define SYSTEM_PACKAGES_LIST_PATH "/data/system/packages.list" -static struct work_struct ksu_update_uid_work; - -struct uid_data { - struct list_head list; - u32 uid; - char package[KSU_MAX_PACKAGE_NAME]; -}; - -static bool is_uid_exist(uid_t uid, char *package, void *data) -{ - struct list_head *list = (struct list_head *)data; - struct uid_data *np; - - bool exist = false; - list_for_each_entry (np, list, list) { - if (np->uid == uid % 100000 && - strncmp(np->package, package, KSU_MAX_PACKAGE_NAME) == 0) { - exist = true; - break; - } - } - return exist; -} - -static void do_update_uid(struct work_struct *work) -{ - struct file *fp = - ksu_filp_open_compat(SYSTEM_PACKAGES_LIST_PATH, O_RDONLY, 0); - if (IS_ERR(fp)) { - pr_err("do_update_uid, open " SYSTEM_PACKAGES_LIST_PATH - " failed: %d\n", - PTR_ERR(fp)); - return; - } - - struct list_head uid_list; - INIT_LIST_HEAD(&uid_list); - - char chr = 0; - loff_t pos = 0; - loff_t line_start = 0; - char buf[128]; - for (;;) { - ssize_t count = - ksu_kernel_read_compat(fp, &chr, sizeof(chr), &pos); - if (count != sizeof(chr)) - break; - if (chr != '\n') - continue; - - count = ksu_kernel_read_compat(fp, buf, sizeof(buf), - &line_start); - - struct uid_data *data = - kmalloc(sizeof(struct uid_data), GFP_ATOMIC); - if (!data) { - 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"); - continue; - } - - u32 res; - if (kstrtou32(uid, 10, &res)) { - pr_err("update_uid: uid parse err\n"); - continue; - } - data->uid = res; - strncpy(data->package, package, KSU_MAX_PACKAGE_NAME); - list_add_tail(&data->list, &uid_list); - // reset line start - line_start = pos; - } - - // now update uid list - struct uid_data *np; - struct uid_data *n; - - // first, check if manager_uid exist! - bool 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; - } - } - - if (!manager_exist && ksu_is_manager_uid_valid()) { - pr_info("manager is uninstalled, invalidate it!\n"); - ksu_invalidate_manager_uid(); - } - - // 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); - } - filp_close(fp, 0); -} - -void update_uid() -{ - ksu_queue_work(&ksu_update_uid_work); -} - -int ksu_uid_observer_init() -{ - INIT_WORK(&ksu_update_uid_work, do_update_uid); - return 0; -} - -int ksu_uid_observer_exit() -{ - return 0; -} diff --git a/kernel/uid_observer.h b/kernel/uid_observer.h deleted file mode 100644 index 6d06fd6ce192..000000000000 --- a/kernel/uid_observer.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef __KSU_H_UID_OBSERVER -#define __KSU_H_UID_OBSERVER - -int ksu_uid_observer_init(); - -int ksu_uid_observer_exit(); - -void update_uid(); - -#endif diff --git a/manager/.gitignore b/manager/.gitignore index dd42da43f547..a595ddf709dd 100644 --- a/manager/.gitignore +++ b/manager/.gitignore @@ -1,9 +1,10 @@ *.iml .gradle -local.properties .idea +.kotlin .DS_Store build captures .cxx +local.properties key.jks diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index 4463e4f0a561..1f98e4a1ba62 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -1,8 +1,12 @@ +@file:Suppress("UnstableApiUsage") + import com.android.build.gradle.internal.api.BaseVariantOutputImpl +import com.android.build.gradle.tasks.PackageAndroidArtifact plugins { alias(libs.plugins.agp.app) alias(libs.plugins.kotlin) + alias(libs.plugins.compose.compiler) alias(libs.plugins.ksp) alias(libs.plugins.lsplugin.apksign) id("kotlin-parcelize") @@ -33,14 +37,11 @@ android { aidl = true buildConfig = true compose = true + prefab = true } kotlinOptions { - jvmTarget = "17" - } - - composeOptions { - kotlinCompilerExtensionVersion = "1.4.3" + jvmTarget = "21" } packaging { @@ -48,7 +49,13 @@ android { useLegacyPackaging = true } resources { - excludes += "/META-INF/{AL2.0,LGPL2.1}" + // https://stackoverflow.com/a/58956288 + // It will break Layout Inspector, but it's unused for release build. + excludes += "META-INF/*.version" + // https://github.com/Kotlin/kotlinx.coroutines?tab=readme-ov-file#avoiding-including-the-debug-infrastructure-in-the-resulting-apk + excludes += "DebugProbesKt.bin" + // https://issueantenna.com/repo/kotlin/kotlinx.coroutines/issues/3158 + excludes += "kotlin-tooling-metadata.json" } } @@ -63,13 +70,26 @@ android { val output = it as BaseVariantOutputImpl output.outputFileName = "KernelSU_${managerVersionName}_${managerVersionCode}-$name.apk" } - kotlin.sourceSets { getByName(name) { kotlin.srcDir("build/generated/ksp/$name/kotlin") } } } + + // https://stackoverflow.com/a/77745844 + tasks.withType { + doFirst { appMetadata.asFile.orNull?.writeText("") } + } + + dependenciesInfo { + includeInApk = false + includeInBundle = false + } + + androidResources { + generateLocaleConfig = true + } } dependencies { @@ -90,15 +110,12 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.com.google.accompanist.drawablepainter) - implementation(libs.com.google.accompanist.navigation.animation) - implementation(libs.com.google.accompanist.systemuicontroller) - - implementation(libs.compose.destinations.animations.core) + implementation(libs.compose.destinations.core) ksp(libs.compose.destinations.ksp) implementation(libs.com.github.topjohnwu.libsu.core) implementation(libs.com.github.topjohnwu.libsu.service) + implementation(libs.com.github.topjohnwu.libsu.io) implementation(libs.dev.rikka.rikkax.parcelablelist) @@ -113,4 +130,7 @@ dependencies { implementation(libs.sheet.compose.dialogs.input) implementation(libs.markdown) -} + implementation(libs.androidx.webkit) + + implementation(libs.lsposed.cxx) +} \ No newline at end of file diff --git a/manager/app/proguard-rules.pro b/manager/app/proguard-rules.pro index dcaf39ce70fe..e69de29bb2d1 100644 --- a/manager/app/proguard-rules.pro +++ b/manager/app/proguard-rules.pro @@ -1,9 +0,0 @@ --dontwarn org.bouncycastle.jsse.BCSSLParameters --dontwarn org.bouncycastle.jsse.BCSSLSocket --dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider --dontwarn org.conscrypt.Conscrypt$Version --dontwarn org.conscrypt.Conscrypt --dontwarn org.conscrypt.ConscryptHostnameVerifier --dontwarn org.openjsse.javax.net.ssl.SSLParameters --dontwarn org.openjsse.javax.net.ssl.SSLSocket --dontwarn org.openjsse.net.ssl.OpenJSSE diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml index 96562cd86313..11cda5f21cd1 100644 --- a/manager/app/src/main/AndroidManifest.xml +++ b/manager/app/src/main/AndroidManifest.xml @@ -12,24 +12,27 @@ android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" + android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="true" android:theme="@style/Theme.KernelSU" - tools:targetApi="33"> + tools:targetApi="34"> - - - + + - + \ No newline at end of file diff --git a/manager/app/src/main/cpp/CMakeLists.txt b/manager/app/src/main/cpp/CMakeLists.txt index cca26d5e4aef..4556ce01d5ec 100644 --- a/manager/app/src/main/cpp/CMakeLists.txt +++ b/manager/app/src/main/cpp/CMakeLists.txt @@ -7,6 +7,9 @@ cmake_minimum_required(VERSION 3.18.1) project("kernelsu") +find_package(cxx REQUIRED CONFIG) +link_libraries(cxx::cxx) + add_library(kernelsu SHARED jni.cc diff --git a/manager/app/src/main/cpp/jni.cc b/manager/app/src/main/cpp/jni.cc index ce91c5dd929e..b0d28698742b 100644 --- a/manager/app/src/main/cpp/jni.cc +++ b/manager/app/src/main/cpp/jni.cc @@ -46,6 +46,12 @@ Java_me_weishu_kernelsu_Natives_isSafeMode(JNIEnv *env, jclass clazz) { return is_safe_mode(); } +extern "C" +JNIEXPORT jboolean JNICALL +Java_me_weishu_kernelsu_Natives_isLkmMode(JNIEnv *env, jclass clazz) { + return is_lkm_mode(); +} + static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) { auto cls = env->GetObjectClass(list); auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z"); @@ -156,13 +162,6 @@ Java_me_weishu_kernelsu_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, env->SetBooleanField(obj, allowSuField, false); env->SetBooleanField(obj, nonRootUseDefaultField, true); - jobject capList = env->GetObjectField(obj, capabilitiesField); - int DEFAULT_CAPS[] = {CAP_DAC_READ_SEARCH}; - - for (auto i: DEFAULT_CAPS) { - addIntToList(env, capList, i); - } - return obj; } diff --git a/manager/app/src/main/cpp/ksu.cc b/manager/app/src/main/cpp/ksu.cc index 1ad43763c62b..1e7981892f00 100644 --- a/manager/app/src/main/cpp/ksu.cc +++ b/manager/app/src/main/cpp/ksu.cc @@ -47,10 +47,14 @@ bool become_manager(const char* pkg) { return ksuctl(CMD_BECOME_MANAGER, param, nullptr); } +// cache the result to avoid unnecessary syscall +static bool is_lkm; int get_version() { int32_t version = -1; - if (ksuctl(CMD_GET_VERSION, &version, nullptr)) { - return version; + int32_t lkm = 0; + ksuctl(CMD_GET_VERSION, &version, &lkm); + if (!is_lkm && lkm != 0) { + is_lkm = true; } return version; } @@ -63,6 +67,11 @@ bool is_safe_mode() { return ksuctl(CMD_CHECK_SAFEMODE, nullptr, nullptr); } +bool is_lkm_mode() { + // you should call get_version first! + return is_lkm; +} + bool uid_should_umount(int uid) { bool should; return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, reinterpret_cast(uid), &should) && should; diff --git a/manager/app/src/main/cpp/ksu.h b/manager/app/src/main/cpp/ksu.h index 8f9c4c0bb953..160a9d6faa0b 100644 --- a/manager/app/src/main/cpp/ksu.h +++ b/manager/app/src/main/cpp/ksu.h @@ -17,6 +17,8 @@ bool uid_should_umount(int uid); bool is_safe_mode(); +bool is_lkm_mode(); + #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. diff --git a/manager/app/src/main/java/me/weishu/kernelsu/KernelSUApplication.kt b/manager/app/src/main/java/me/weishu/kernelsu/KernelSUApplication.kt index 11084934709c..0503b42f0f43 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/KernelSUApplication.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/KernelSUApplication.kt @@ -5,6 +5,7 @@ import coil.Coil import coil.ImageLoader import me.zhanghai.android.appiconloader.coil.AppIconFetcher import me.zhanghai.android.appiconloader.coil.AppIconKeyer +import java.io.File lateinit var ksuApp: KernelSUApplication @@ -24,6 +25,11 @@ class KernelSUApplication : Application() { } .build() ) + + val webroot = File(dataDir, "webroot") + if (!webroot.exists()) { + webroot.mkdir() + } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/Natives.kt b/manager/app/src/main/java/me/weishu/kernelsu/Natives.kt index 58624da772d3..bb788293aa31 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/Natives.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/Natives.kt @@ -18,6 +18,14 @@ object Natives { // 11071: Fix the issue of failing to set a custom SELinux type. const val MINIMAL_SUPPORTED_KERNEL = 11071 + // 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 + const val KERNEL_SU_DOMAIN = "u:r:su:s0" + + const val ROOT_UID = 0 + const val ROOT_GID = 0 + init { System.loadLibrary("kernelsu") } @@ -34,6 +42,9 @@ object Natives { val isSafeMode: Boolean external get + val isLkmMode: Boolean + external get + external fun uidShouldUmount(uid: Int): Boolean /** @@ -45,7 +56,6 @@ object Natives { external fun setAppProfile(profile: Profile?): Boolean private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$" - private const val ROOT_DEFAULT_PROFILE_KEY = "#" private const val NOBODY_UID = 9999 fun setDefaultUmountModules(umountModules: Boolean): Boolean { @@ -85,21 +95,21 @@ object Natives { // these are used for root profile val rootUseDefault: Boolean = true, val rootTemplate: String? = null, - val uid: Int = 0, - val gid: Int = 0, + val uid: Int = ROOT_UID, + val gid: Int = ROOT_GID, val groups: List = mutableListOf(), val capabilities: List = mutableListOf(), - val context: String = "u:r:su:s0", - val namespace: Int = Namespace.Inherited.ordinal, + val context: String = KERNEL_SU_DOMAIN, + val namespace: Int = Namespace.INHERITED.ordinal, val nonRootUseDefault: Boolean = true, val umountModules: Boolean = true, var rules: String = "", // this field is save in ksud!! ) : Parcelable { enum class Namespace { - Inherited, - Global, - Individual, + INHERITED, + GLOBAL, + INDIVIDUAL, } constructor() : this("") diff --git a/manager/app/src/main/java/me/weishu/kernelsu/profile/Groups.kt b/manager/app/src/main/java/me/weishu/kernelsu/profile/Groups.kt index 8dd5e9f2a5a3..2ddb94dc91a1 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/profile/Groups.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/profile/Groups.kt @@ -1,6 +1,7 @@ package me.weishu.kernelsu.profile /** + * https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h * @author weishu * @date 2023/6/3. */ @@ -60,17 +61,55 @@ enum class Groups(val gid: Int, val display: String, val desc: String) { FIREWALL(1048, "firewall", "firewall process"), TRUNKS(1049, "trunks", "trunksd process"), NVRAM(1050, "nvram", "nvram daemon"), - DNS_TETHER(1051, "dns_tether", "dns_tether device"), - DNS_TETHER_RESERVED(1052, "dns_tether_reserved", "Reserved range for dns_tether"), - WEBVIEW_ZYGOTE(1053, "webview_zygote", "zygote process"), - WEBVIEW_USER(1054, "webview_user", "webview chromium user"), - ETHERNET(1055, "ethernet", "Ethernet"), - TOMBSTONED(1056, "tombstoned", "tombstoned process"), - GRAPHICS_RW(1057, "graphics_rw", "graphics devices"), + DNS(1051, "dns", "DNS resolution daemon (system: netd)"), + DNS_TETHER(1052, "dns_tether", "DNS resolution daemon (tether: dnsmasq)"), + WEBVIEW_ZYGOTE(1053, "webview_zygote", "WebView zygote process"), + VEHICLE_NETWORK(1054, "vehicle_network", "Vehicle network service"), + MEDIA_AUDIO(1055, "media_audio", "GID for audio files on internal media storage"), + MEDIA_VIDEO(1056, "media_video", "GID for video files on internal media storage"), + MEDIA_IMAGE(1057, "media_image", "GID for image files on internal media storage"), + TOMBSTONED(1058, "tombstoned", "tombstoned user"), + MEDIA_OBB(1059, "media_obb", "GID for OBB files on internal media storage"), + ESE(1060, "ese", "embedded secure element (eSE) subsystem"), + OTA_UPDATE(1061, "ota_update", "resource tracking UID for OTA updates"), + AUTOMOTIVE_EVS(1062, "automotive_evs", "Automotive rear and surround view system"), + LOWPAN(1063, "lowpan", "LoWPAN subsystem"), + HSM(1064, "lowpan", "hardware security module subsystem"), + RESERVED_DISK(1065, "reserved_disk", "GID that has access to reserved disk space"), + STATSD(1066, "statsd", "statsd daemon"), + INCIDENTD(1067, "incidentd", "incidentd daemon"), + SECURE_ELEMENT(1068, "secure_element", "secure element subsystem"), + LMKD(1069, "lmkd", "low memory killer daemon"), + LLKD(1070, "llkd", "live lock daemon"), + IORAPD(1071, "iorapd", "input/output readahead and pin daemon"), + GPU_SERVICE(1072, "gpu_service", "GPU service daemon"), + NETWORK_STACK(1073, "network_stack", "network stack service"), + GSID(1074, "GSID", "GSI service daemon"), + FSVERITY_CERT(1075, "fsverity_cert", "fs-verity key ownership in keystore"), + CREDSTORE(1076, "credstore", "identity credential manager service"), + EXTERNAL_STORAGE(1077, "external_storage", "Full external storage access including USB OTG volumes"), + EXT_DATA_RW(1078, "ext_data_rw", "GID for app-private data directories on external storage"), + EXT_OBB_RW(1079, "ext_obb_rw", "GID for OBB directories on external storage"), + CONTEXT_HUB(1080, "context_hub", "GID for access to the Context Hub"), + VIRTUALIZATIONSERVICE(1081, "virtualizationservice", "VirtualizationService daemon"), + ARTD(1082, "artd", "ART Service daemon"), + UWB(1083, "uwb", "UWB subsystem"), + THREAD_NETWORK(1084, "thread_network", "Thread Network subsystem"), + DICED(1085, "diced", "Android's DICE daemon"), + DMESGD(1086, "dmesgd", "dmesg parsing daemon for kernel report collection"), + JC_WEAVER(1087, "jc_weaver", "Javacard Weaver HAL - to manage omapi ARA rules"), + JC_STRONGBOX(1088, "jc_strongbox", "Javacard Strongbox HAL - to manage omapi ARA rules"), + JC_IDENTITYCRED(1089, "jc_identitycred", "Javacard Identity Cred HAL - to manage omapi ARA rules"), + SDK_SANDBOX(1090, "sdk_sandbox", "SDK sandbox virtual UID"), + SECURITY_LOG_WRITER(1091, "security_log_writer", "write to security log"), + PRNG_SEEDER(1092, "prng_seeder", "PRNG seeder daemon"), SHELL(2000, "shell", "adb and debug shell user"), CACHE(2001, "cache", "cache access"), - DIAG(2002, "diag", "diagnostics"), + DIAG(2002, "diag", "access to diagnostic resources"), + + /* The 3000 series are intended for use as supplemental group id's only. + * They indicate special Android capabilities that the kernel is aware of. */ NET_BT_ADMIN(3001, "net_bt_admin", "bluetooth: create any socket"), NET_BT(3002, "net_bt", "bluetooth: create sco, rfcomm or l2cap sockets"), INET(3003, "inet", "can create AF_INET and AF_INET6 sockets"), @@ -79,7 +118,11 @@ enum class Groups(val gid: Int, val display: String, val desc: String) { NET_BW_STATS(3006, "net_bw_stats", "read bandwidth statistics"), NET_BW_ACCT(3007, "net_bw_acct", "change bandwidth statistics accounting"), NET_BT_STACK(3008, "net_bt_stack", "access to various bluetooth management functions"), - QCOM_DIAG(3009, "qcom_diag", "allow msm specific diag commands"), + READPROC(3009, "readproc", "Allow /proc read access"), + WAKELOCK(3010, "wakelock", "Allow system wakelock read/write access"), + UHID(3011, "uhid", "Allow read/write to /dev/uhid node"), + READTRACEFS(3012, "readtracefs", "Allow tracefs read"), + EVERYBODY(9997, "everybody", "Shared external storage read/write"), MISC(9998, "misc", "Access to misc storage"), NOBODY(9999, "nobody", "Reserved"), diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt index 74af1c5ba4a0..5737c524a940 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt @@ -1,15 +1,27 @@ package me.weishu.kernelsu.ui +import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.animation.ExperimentalAnimationApi +import androidx.activity.enableEdgeToEdge +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.displayCutout +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.union import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,42 +31,58 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.navigation.NavBackStackEntry import androidx.navigation.NavHostController -import com.google.accompanist.navigation.animation.rememberAnimatedNavController +import androidx.navigation.compose.rememberNavController import com.ramcosta.composedestinations.DestinationsNavHost -import com.ramcosta.composedestinations.navigation.popBackStack +import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle +import com.ramcosta.composedestinations.generated.NavGraphs import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState +import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp -import me.weishu.kernelsu.ui.component.rememberDialogHostState import me.weishu.kernelsu.ui.screen.BottomBarDestination -import me.weishu.kernelsu.ui.screen.NavGraphs import me.weishu.kernelsu.ui.theme.KernelSUTheme -import me.weishu.kernelsu.ui.util.LocalDialogHost import me.weishu.kernelsu.ui.util.LocalSnackbarHost +import me.weishu.kernelsu.ui.util.rootAvailable +import me.weishu.kernelsu.ui.util.install class MainActivity : ComponentActivity() { - @OptIn(ExperimentalAnimationApi::class) override fun onCreate(savedInstanceState: Bundle?) { + + // Enable edge to edge + enableEdgeToEdge() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.isNavigationBarContrastEnforced = false + } + super.onCreate(savedInstanceState) + val isManager = Natives.becomeManager(ksuApp.packageName) + if (isManager) install() + setContent { KernelSUTheme { - val navController = rememberAnimatedNavController() - val snackbarHostState = remember { SnackbarHostState() } + val navController = rememberNavController() + val snackBarHostState = remember { SnackbarHostState() } Scaffold( bottomBar = { BottomBar(navController) }, - snackbarHost = { SnackbarHost(snackbarHostState) } + contentWindowInsets = WindowInsets(0, 0, 0, 0) ) { innerPadding -> CompositionLocalProvider( - LocalSnackbarHost provides snackbarHostState, - LocalDialogHost provides rememberDialogHostState(), + LocalSnackbarHost provides snackBarHostState, ) { DestinationsNavHost( modifier = Modifier.padding(innerPadding), navGraph = NavGraphs.root, - navController = navController + navController = navController, + defaultTransitions = object : NavHostAnimatedDestinationStyle() { + override val enterTransition: AnimatedContentTransitionScope.() -> EnterTransition + get() = { fadeIn(animationSpec = tween(340)) } + override val exitTransition: AnimatedContentTransitionScope.() -> ExitTransition + get() = { fadeOut(animationSpec = tween(340)) } + } ) } } @@ -65,21 +93,26 @@ class MainActivity : ComponentActivity() { @Composable private fun BottomBar(navController: NavHostController) { + val navigator = navController.rememberDestinationsNavigator() val isManager = Natives.becomeManager(ksuApp.packageName) - val fullFeatured = isManager && !Natives.requireNewKernel() - NavigationBar(tonalElevation = 8.dp) { - BottomBarDestination.values().forEach { destination -> + val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() + NavigationBar( + tonalElevation = 8.dp, + windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only( + WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom + ) + ) { + BottomBarDestination.entries.forEach { destination -> if (!fullFeatured && destination.rootRequired) return@forEach val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction) NavigationBarItem( selected = isCurrentDestOnBackStack, onClick = { if (isCurrentDestOnBackStack) { - navController.popBackStack(destination.direction, false) + navigator.popBackStack(destination.direction, false) } - - navController.navigate(destination.direction.route) { - popUpTo(NavGraphs.root.route) { + navigator.navigate(destination.direction) { + popUpTo(NavGraphs.root) { saveState = true } launchSingleTop = true diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt index 973de2db153a..803447391af3 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/AboutCard.kt @@ -1,7 +1,5 @@ package me.weishu.kernelsu.ui.component -import android.text.method.LinkMovementMethod -import android.widget.TextView import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -11,25 +9,28 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.draw.scale +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Dialog -import androidx.core.content.res.ResourcesCompat -import androidx.core.text.HtmlCompat -import com.google.accompanist.drawablepainter.rememberDrawablePainter import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.R @@ -37,9 +38,8 @@ import me.weishu.kernelsu.R @Composable fun AboutCard() { ElevatedCard( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(8.dp), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) ) { Row( modifier = Modifier @@ -52,32 +52,31 @@ fun AboutCard() { } @Composable -fun AboutDialog(showAboutDialog: MutableState) { - if (showAboutDialog.value) { - Dialog(onDismissRequest = { showAboutDialog.value = false }) { - AboutCard() - } +fun AboutDialog(dismiss: () -> Unit) { + Dialog( + onDismissRequest = { dismiss() } + ) { + AboutCard() } } @Composable private fun AboutCardContent() { Column( - modifier = Modifier - .fillMaxWidth() + modifier = Modifier.fillMaxWidth() ) { - val drawable = ResourcesCompat.getDrawable( - LocalContext.current.resources, - R.mipmap.ic_launcher, - LocalContext.current.theme - ) - Row { - Image( - painter = rememberDrawablePainter(drawable), - contentDescription = "icon", - modifier = Modifier.size(40.dp) - ) + Surface( + modifier = Modifier.size(40.dp), + color = colorResource(id = R.color.ic_launcher_background), + shape = CircleShape + ) { + Image( + painter = painterResource(id = R.drawable.ic_launcher_foreground), + contentDescription = "icon", + modifier = Modifier.scale(1.4f) + ) + } Spacer(modifier = Modifier.width(12.dp)) @@ -96,31 +95,31 @@ private fun AboutCardContent() { Spacer(modifier = Modifier.height(8.dp)) - HtmlText( - html = stringResource( + val annotatedString = AnnotatedString.Companion.fromHtml( + htmlString = stringResource( id = R.string.about_source_code, "GitHub", "Telegram" + ), + linkStyles = TextLinkStyles( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + textDecoration = TextDecoration.Underline + ), + pressedStyle = SpanStyle( + color = MaterialTheme.colorScheme.primary, + background = MaterialTheme.colorScheme.secondaryContainer, + textDecoration = TextDecoration.Underline + ) + ) + ) + Text( + text = annotatedString, + style = TextStyle( + fontSize = 14.sp ) ) } } } -} - -@Composable -fun HtmlText(html: String, modifier: Modifier = Modifier) { - val contentColor = LocalContentColor.current - AndroidView( - modifier = modifier, - factory = { context -> - TextView(context).also { - it.movementMethod = LinkMovementMethod.getInstance() - } - }, - update = { - it.text = HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_COMPACT) - it.setTextColor(contentColor.toArgb()) - } - ) } \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt index 62b02a369a84..27adc3f03ccc 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/Dialog.kt @@ -1,8 +1,11 @@ package me.weishu.kernelsu.ui.component import android.graphics.text.LineBreaker +import android.os.Build +import android.os.Parcelable import android.text.Layout import android.text.method.LinkMovementMethod +import android.util.Log import android.view.ViewGroup import android.widget.TextView import androidx.compose.foundation.layout.Box @@ -10,14 +13,10 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton +import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.toArgb @@ -28,48 +27,48 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import io.noties.markwon.Markwon import io.noties.markwon.utils.NoCopySpannableFactory -import kotlinx.coroutines.CancellableContinuation -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import me.weishu.kernelsu.ui.util.LocalDialogHost +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.consumeAsFlow +import kotlinx.coroutines.flow.onEach +import kotlinx.parcelize.Parcelize import kotlin.coroutines.resume -interface DialogVisuals +private const val TAG = "DialogComponent" -interface LoadingDialogVisuals : DialogVisuals - -interface PromptDialogVisuals : DialogVisuals { +interface ConfirmDialogVisuals : Parcelable { val title: String val content: String -} - -interface ConfirmDialogVisuals : PromptDialogVisuals { + val isMarkdown: Boolean val confirm: String? val dismiss: String? - val isMarkdown: Boolean -} - - -sealed interface DialogData { - val visuals: DialogVisuals } -interface LoadingDialogData : DialogData { - override val visuals: LoadingDialogVisuals - fun dismiss() +@Parcelize +private data class ConfirmDialogVisualsImpl( + override val title: String, + override val content: String, + override val isMarkdown: Boolean, + override val confirm: String?, + override val dismiss: String?, +) : ConfirmDialogVisuals { + companion object { + val Empty: ConfirmDialogVisuals = ConfirmDialogVisualsImpl("", "", false, null, null) + } } -interface PromptDialogData : DialogData { - override val visuals: PromptDialogVisuals - fun dismiss() +interface DialogHandle { + val isShown: Boolean + val dialogType: String + fun show() + fun hide() } -interface ConfirmDialogData : PromptDialogData { - override val visuals: ConfirmDialogVisuals - fun confirm() +interface LoadingDialogHandle : DialogHandle { + suspend fun withLoading(block: suspend () -> R): R + fun showLoading() } sealed interface ConfirmResult { @@ -77,143 +76,313 @@ sealed interface ConfirmResult { object Canceled : ConfirmResult } -class DialogHostState { +interface ConfirmDialogHandle : DialogHandle { + val visuals: ConfirmDialogVisuals - private object LoadingDialogVisualsImpl : LoadingDialogVisuals + fun showConfirm( + title: String, + content: String, + markdown: Boolean = false, + confirm: String? = null, + dismiss: String? = null + ) - private data class PromptDialogVisualsImpl( - override val title: String, override val content: String - ) : PromptDialogVisuals + suspend fun awaitConfirm( + title: String, + content: String, + markdown: Boolean = false, + confirm: String? = null, + dismiss: String? = null + ): ConfirmResult +} - private data class ConfirmDialogVisualsImpl( - override val title: String, - override val content: String, - override val confirm: String?, - override val dismiss: String?, - override val isMarkdown: Boolean, - ) : ConfirmDialogVisuals +private abstract class DialogHandleBase( + val visible: MutableState, + val coroutineScope: CoroutineScope +) : DialogHandle { + override val isShown: Boolean + get() = visible.value - private data class LoadingDialogDataImpl( - override val visuals: LoadingDialogVisuals, - private val continuation: CancellableContinuation, - ) : LoadingDialogData { - override fun dismiss() { - if (continuation.isActive) continuation.resume(Unit) + override fun show() { + coroutineScope.launch { + visible.value = true } } - private data class PromptDialogDataImpl( - override val visuals: PromptDialogVisuals, - private val continuation: CancellableContinuation, - ) : PromptDialogData { - override fun dismiss() { - if (continuation.isActive) continuation.resume(Unit) + final override fun hide() { + coroutineScope.launch { + visible.value = false } } - private data class ConfirmDialogDataImpl( - override val visuals: ConfirmDialogVisuals, - private val continuation: CancellableContinuation - ) : ConfirmDialogData { + override fun toString(): String { + return dialogType + } +} - override fun confirm() { - if (continuation.isActive) continuation.resume(ConfirmResult.Confirmed) - } +private class LoadingDialogHandleImpl( + visible: MutableState, + coroutineScope: CoroutineScope +) : LoadingDialogHandle, DialogHandleBase(visible, coroutineScope) { + override suspend fun withLoading(block: suspend () -> R): R { + return coroutineScope.async { + try { + visible.value = true + block() + } finally { + visible.value = false + } + }.await() + } - override fun dismiss() { - if (continuation.isActive) continuation.resume(ConfirmResult.Canceled) - } + override fun showLoading() { + show() } - private val mutex = Mutex() + override val dialogType: String get() = "LoadingDialog" +} - var currentDialogData by mutableStateOf(null) - private set +typealias NullableCallback = (() -> Unit)? - suspend fun showLoading() { - try { - mutex.withLock { - suspendCancellableCoroutine { continuation -> - currentDialogData = LoadingDialogDataImpl( - visuals = LoadingDialogVisualsImpl, continuation = continuation - ) - } +interface ConfirmCallback { + + val onConfirm: NullableCallback + + val onDismiss: NullableCallback + + val isEmpty: Boolean get() = onConfirm == null && onDismiss == null + + companion object { + operator fun invoke(onConfirmProvider: () -> NullableCallback, onDismissProvider: () -> NullableCallback): ConfirmCallback { + return object : ConfirmCallback { + override val onConfirm: NullableCallback + get() = onConfirmProvider() + override val onDismiss: NullableCallback + get() = onDismissProvider() } - } finally { - currentDialogData = null } } +} - suspend fun withLoading(block: suspend () -> R) = coroutineScope { - val showLoading = launch { - showLoading() +private class ConfirmDialogHandleImpl( + visible: MutableState, + coroutineScope: CoroutineScope, + callback: ConfirmCallback, + override var visuals: ConfirmDialogVisuals = ConfirmDialogVisualsImpl.Empty, + private val resultFlow: ReceiveChannel +) : ConfirmDialogHandle, DialogHandleBase(visible, coroutineScope) { + private class ResultCollector( + private val callback: ConfirmCallback + ) : FlowCollector { + fun handleResult(result: ConfirmResult) { + Log.d(TAG, "handleResult: ${result.javaClass.simpleName}") + when (result) { + ConfirmResult.Confirmed -> onConfirm() + ConfirmResult.Canceled -> onDismiss() + } } - val result = block() + fun onConfirm() { + callback.onConfirm?.invoke() + } - showLoading.cancel() + fun onDismiss() { + callback.onDismiss?.invoke() + } - result + override suspend fun emit(value: ConfirmResult) { + handleResult(value) + } + } + + private val resultCollector = ResultCollector(callback) + + private var awaitContinuation: CancellableContinuation? = null + + private val isCallbackEmpty = callback.isEmpty + + init { + coroutineScope.launch { + resultFlow + .consumeAsFlow() + .onEach { result -> + awaitContinuation?.let { + awaitContinuation = null + if (it.isActive) { + it.resume(result) + } + } + } + .onEach { hide() } + .collect(resultCollector) + } } - suspend fun showPrompt(title: String, content: String) { - try { - mutex.withLock { - suspendCancellableCoroutine { continuation -> - currentDialogData = PromptDialogDataImpl( - visuals = PromptDialogVisualsImpl(title, content), - continuation = continuation - ) + private suspend fun awaitResult(): ConfirmResult { + return suspendCancellableCoroutine { + awaitContinuation = it.apply { + if (isCallbackEmpty) { + invokeOnCancellation { + visible.value = false + } } } - } finally { - currentDialogData = null } } - suspend fun showConfirm( + fun updateVisuals(visuals: ConfirmDialogVisuals) { + this.visuals = visuals + } + + override fun show() { + if (visuals !== ConfirmDialogVisualsImpl.Empty) { + super.show() + } else { + throw UnsupportedOperationException("can't show confirm dialog with the Empty visuals") + } + } + + override fun showConfirm( title: String, content: String, - markdown: Boolean = false, - confirm: String? = null, - dismiss: String? = null - ): ConfirmResult = mutex.withLock { - try { - return@withLock suspendCancellableCoroutine { continuation -> - currentDialogData = ConfirmDialogDataImpl( - visuals = ConfirmDialogVisualsImpl(title, content, confirm, dismiss, markdown), - continuation = continuation - ) - } - } finally { - currentDialogData = null + markdown: Boolean, + confirm: String?, + dismiss: String? + ) { + coroutineScope.launch { + updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, confirm, dismiss)) + show() + } + } + + override suspend fun awaitConfirm( + title: String, + content: String, + markdown: Boolean, + confirm: String?, + dismiss: String? + ): ConfirmResult { + coroutineScope.launch { + updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, confirm, dismiss)) + show() } + return awaitResult() } + + override val dialogType: String get() = "ConfirmDialog" + + override fun toString(): String { + return "${super.toString()}(visuals: $visuals)" + } + + companion object { + fun Saver( + visible: MutableState, + coroutineScope: CoroutineScope, + callback: ConfirmCallback, + resultChannel: ReceiveChannel + ) = Saver( + save = { + it.visuals + }, + restore = { + Log.d(TAG, "ConfirmDialog restore, visuals: $it") + ConfirmDialogHandleImpl(visible, coroutineScope, callback, it, resultChannel) + } + ) + } +} + +private class CustomDialogHandleImpl( + visible: MutableState, + coroutineScope: CoroutineScope +) : DialogHandleBase(visible, coroutineScope) { + override val dialogType: String get() = "CustomDialog" } @Composable -fun rememberDialogHostState(): DialogHostState { +fun rememberLoadingDialog(): LoadingDialogHandle { + val visible = remember { + mutableStateOf(false) + } + val coroutineScope = rememberCoroutineScope() + + if (visible.value) { + LoadingDialog() + } + return remember { - DialogHostState() + LoadingDialogHandleImpl(visible, coroutineScope) } } -private inline fun DialogData?.tryInto(): T? { - return when (this) { - is T -> this - else -> null +@Composable +private fun rememberConfirmDialog(visuals: ConfirmDialogVisuals, callback: ConfirmCallback): ConfirmDialogHandle { + val visible = rememberSaveable { + mutableStateOf(false) + } + val coroutineScope = rememberCoroutineScope() + val resultChannel = remember { + Channel() + } + + val handle = rememberSaveable( + saver = ConfirmDialogHandleImpl.Saver(visible, coroutineScope, callback, resultChannel), + init = { + ConfirmDialogHandleImpl(visible, coroutineScope, callback, visuals, resultChannel) + } + ) + + if (visible.value) { + ConfirmDialog( + handle.visuals, + confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } }, + dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } } + ) } + + return handle } @Composable -fun LoadingDialog( - state: DialogHostState = LocalDialogHost.current, -) { - state.currentDialogData.tryInto() ?: return - val dialogProperties = remember { - DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false) +fun rememberConfirmCallback(onConfirm: NullableCallback, onDismiss: NullableCallback): ConfirmCallback { + val currentOnConfirm by rememberUpdatedState(newValue = onConfirm) + val currentOnDismiss by rememberUpdatedState(newValue = onDismiss) + return remember { + ConfirmCallback({ currentOnConfirm }, { currentOnDismiss }) } - Dialog(onDismissRequest = {}, properties = dialogProperties) { +} + +@Composable +fun rememberConfirmDialog(onConfirm: NullableCallback = null, onDismiss: NullableCallback = null): ConfirmDialogHandle { + return rememberConfirmDialog(rememberConfirmCallback(onConfirm, onDismiss)) +} + +@Composable +fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle { + return rememberConfirmDialog(ConfirmDialogVisualsImpl.Empty, callback) +} + +@Composable +fun rememberCustomDialog(composable: @Composable (dismiss: () -> Unit) -> Unit): DialogHandle { + val visible = rememberSaveable { + mutableStateOf(false) + } + val coroutineScope = rememberCoroutineScope() + if (visible.value) { + composable { visible.value = false } + } + return remember { + CustomDialogHandleImpl(visible, coroutineScope) + } +} + +@Composable +private fun LoadingDialog() { + Dialog( + onDismissRequest = {}, + properties = DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false) + ) { Surface( modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp) ) { @@ -227,41 +396,10 @@ fun LoadingDialog( } @Composable -fun PromptDialog( - state: DialogHostState = LocalDialogHost.current, -) { - val promptDialogData = state.currentDialogData.tryInto() ?: return - - val visuals = promptDialogData.visuals +private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit) { AlertDialog( onDismissRequest = { - promptDialogData.dismiss() - }, - title = { - Text(text = visuals.title) - }, - text = { - Text(text = visuals.content) - }, - confirmButton = { - TextButton(onClick = { promptDialogData.dismiss() }) { - Text(text = stringResource(id = android.R.string.ok)) - } - }, - dismissButton = null, - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) { - val confirmDialogData = state.currentDialogData.tryInto() ?: return - - val visuals = confirmDialogData.visuals - - AlertDialog( - onDismissRequest = { - confirmDialogData.dismiss() + dismiss() }, title = { Text(text = visuals.title) @@ -274,17 +412,18 @@ fun ConfirmDialog(state: DialogHostState = LocalDialogHost.current) { } }, confirmButton = { - TextButton(onClick = { confirmDialogData.confirm() }) { + TextButton(onClick = confirm) { Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok)) } }, dismissButton = { - TextButton(onClick = { confirmDialogData.dismiss() }) { + TextButton(onClick = dismiss) { Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel)) } }, ) } + @Composable private fun MarkdownContent(content: String) { val contentColor = LocalContentColor.current @@ -294,7 +433,9 @@ private fun MarkdownContent(content: String) { TextView(context).apply { movementMethod = LinkMovementMethod.getInstance() setSpannableFactory(NoCopySpannableFactory.getInstance()) - breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE + 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 @@ -307,5 +448,6 @@ private fun MarkdownContent(content: String) { update = { Markwon.create(it.context).setMarkdown(it, content) it.setTextColor(contentColor.toArgb()) - }) + } + ) } \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt index af47ee64a6bb..b388cb49d44a 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchBar.kt @@ -5,20 +5,25 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.outlined.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -27,7 +32,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -39,7 +43,7 @@ import androidx.compose.ui.unit.dp private const val TAG = "SearchBar" -@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchAppBar( title: @Composable () -> Unit, @@ -49,6 +53,7 @@ fun SearchAppBar( onBackClick: (() -> Unit)? = null, onConfirm: (() -> Unit)? = null, dropdownContent: @Composable (() -> Unit)? = null, + scrollBehavior: TopAppBarScrollBehavior? = null ) { val keyboardController = LocalSoftwareKeyboardController.current val focusRequester = remember { FocusRequester() } @@ -115,7 +120,7 @@ fun SearchAppBar( if (onBackClick != null) { IconButton( onClick = onBackClick, - content = { Icon(Icons.Outlined.ArrowBack, null) } + content = { Icon(Icons.AutoMirrored.Outlined.ArrowBack, null) } ) } }, @@ -133,10 +138,13 @@ fun SearchAppBar( dropdownContent() } - } + }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior ) } +@OptIn(ExperimentalMaterial3Api::class) @Preview @Composable private fun SearchAppBarPreview() { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SettingsItem.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SettingsItem.kt index c3e3b5c4e65b..e537175fd2c4 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SettingsItem.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/SettingsItem.kt @@ -1,12 +1,18 @@ package me.weishu.kernelsu.ui.component +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.RadioButton import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.semantics.Role @Composable fun SwitchItem( @@ -17,7 +23,18 @@ fun SwitchItem( enabled: Boolean = true, onCheckedChange: (Boolean) -> Unit ) { + val interactionSource = remember { MutableInteractionSource() } + ListItem( + modifier = Modifier + .toggleable( + value = checked, + interactionSource = interactionSource, + role = Role.Switch, + enabled = enabled, + indication = LocalIndication.current, + onValueChange = onCheckedChange + ), headlineContent = { Text(title) }, @@ -25,7 +42,12 @@ fun SwitchItem( { Icon(icon, title) } }, trailingContent = { - Switch(checked = checked, enabled = enabled, onCheckedChange = onCheckedChange) + Switch( + checked = checked, + enabled = enabled, + onCheckedChange = onCheckedChange, + interactionSource = interactionSource + ) }, supportingContent = { if (summary != null) { @@ -47,6 +69,6 @@ fun RadioItem( }, leadingContent = { RadioButton(selected = selected, onClick = onClick) - }, + } ) } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt index 3ba0b1634621..1a39dcc35ea5 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfig.kt @@ -1,11 +1,10 @@ -@file:OptIn(ExperimentalMaterial3Api::class) - package me.weishu.kernelsu.ui.component.profile import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions @@ -20,16 +19,17 @@ import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource @@ -53,6 +53,7 @@ import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.profile.Capabilities import me.weishu.kernelsu.profile.Groups +import me.weishu.kernelsu.ui.component.rememberCustomDialog import me.weishu.kernelsu.ui.util.isSepolicyValid @OptIn(ExperimentalMaterial3Api::class) @@ -74,9 +75,9 @@ fun RootProfileConfig( var expanded by remember { mutableStateOf(false) } val currentNamespace = when (profile.namespace) { - Natives.Profile.Namespace.Inherited.ordinal -> stringResource(R.string.profile_namespace_inherited) - Natives.Profile.Namespace.Global.ordinal -> stringResource(R.string.profile_namespace_global) - Natives.Profile.Namespace.Individual.ordinal -> stringResource(R.string.profile_namespace_individual) + Natives.Profile.Namespace.INHERITED.ordinal -> stringResource(R.string.profile_namespace_inherited) + Natives.Profile.Namespace.GLOBAL.ordinal -> stringResource(R.string.profile_namespace_global) + Natives.Profile.Namespace.INDIVIDUAL.ordinal -> stringResource(R.string.profile_namespace_individual) else -> stringResource(R.string.profile_namespace_inherited) } ListItem(headlineContent = { @@ -86,7 +87,7 @@ fun RootProfileConfig( ) { OutlinedTextField( modifier = Modifier - .menuAnchor() + .menuAnchor(MenuAnchorType.PrimaryNotEditable) .fillMaxWidth(), readOnly = true, label = { Text(stringResource(R.string.profile_namespace)) }, @@ -104,21 +105,21 @@ fun RootProfileConfig( DropdownMenuItem( text = { Text(stringResource(R.string.profile_namespace_inherited)) }, onClick = { - onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.Inherited.ordinal)) + onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INHERITED.ordinal)) expanded = false }, ) DropdownMenuItem( text = { Text(stringResource(R.string.profile_namespace_global)) }, onClick = { - onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.Global.ordinal)) + onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.GLOBAL.ordinal)) expanded = false }, ) DropdownMenuItem( text = { Text(stringResource(R.string.profile_namespace_individual)) }, onClick = { - onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.Individual.ordinal)) + onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INDIVIDUAL.ordinal)) expanded = false }, ) @@ -146,7 +147,7 @@ fun RootProfileConfig( val selectedGroups = profile.groups.ifEmpty { listOf(0) }.let { e -> e.mapNotNull { g -> - Groups.values().find { it.gid == g } + Groups.entries.find { it.gid == g } } } GroupsPanel(selectedGroups) { @@ -159,7 +160,7 @@ fun RootProfileConfig( } val selectedCaps = profile.capabilities.mapNotNull { e -> - Capabilities.values().find { it.cap == e } + Capabilities.entries.find { it.cap == e } } CapsPanel(selectedCaps) { @@ -184,14 +185,23 @@ fun RootProfileConfig( } } -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun GroupsPanel(selected: List, closeSelection: (selection: Set) -> Unit) { + val selectGroupsDialog = rememberCustomDialog { dismiss: () -> Unit -> + val groups = Groups.entries.toTypedArray().sortedWith( + compareBy { if (selected.contains(it)) 0 else 1 } + .then(compareBy { + when (it) { + Groups.ROOT -> 0 + Groups.SYSTEM -> 1 + Groups.SHELL -> 2 + else -> Int.MAX_VALUE + } + }) + .then(compareBy { it.name }) - var showDialog by remember { mutableStateOf(false) } - - if (showDialog) { - val groups = Groups.values() + ) val options = groups.map { value -> ListOption( titleText = value.display, @@ -205,7 +215,7 @@ fun GroupsPanel(selected: List, closeSelection: (selection: Set) state = rememberUseCaseState(visible = true, onFinishedRequest = { closeSelection(selection) }, onCloseRequest = { - showDialog = false + dismiss() }), header = Header.Default( title = stringResource(R.string.profile_groups), @@ -225,14 +235,20 @@ fun GroupsPanel(selected: List, closeSelection: (selection: Set) ) } - OutlinedCard(modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clickable { - showDialog = true - }) { + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { - Column(modifier = Modifier.padding(16.dp)) { + Column( + modifier = Modifier + .fillMaxSize() + .clickable { + selectGroupsDialog.show() + } + .padding(16.dp) + ) { Text(stringResource(R.string.profile_groups)) FlowRow { selected.forEach { group -> @@ -247,17 +263,17 @@ fun GroupsPanel(selected: List, closeSelection: (selection: Set) } } -@OptIn(ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable fun CapsPanel( selected: Collection, closeSelection: (selection: Set) -> Unit ) { - - var showDialog by remember { mutableStateOf(false) } - - if (showDialog) { - val caps = Capabilities.values() + val selectCapabilitiesDialog = rememberCustomDialog { dismiss -> + val caps = Capabilities.entries.toTypedArray().sortedWith( + compareBy { if (selected.contains(it)) 0 else 1 } + .then(compareBy { it.name }) + ) val options = caps.map { value -> ListOption( titleText = value.display, @@ -271,7 +287,7 @@ fun CapsPanel( state = rememberUseCaseState(visible = true, onFinishedRequest = { closeSelection(selection) }, onCloseRequest = { - showDialog = false + dismiss() }), header = Header.Default( title = stringResource(R.string.profile_capabilities), @@ -290,14 +306,20 @@ fun CapsPanel( ) } - OutlinedCard(modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clickable { - showDialog = true - }) { + OutlinedCard( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { - Column(modifier = Modifier.padding(16.dp)) { + Column( + modifier = Modifier + .fillMaxSize() + .clickable { + selectCapabilitiesDialog.show() + } + .padding(16.dp) + ) { Text(stringResource(R.string.profile_capabilities)) FlowRow { selected.forEach { group -> @@ -312,7 +334,6 @@ fun CapsPanel( } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun UidPanel(uid: Int, label: String, onUidChange: (Int) -> Unit) { @@ -321,10 +342,10 @@ private fun UidPanel(uid: Int, label: String, onUidChange: (Int) -> Unit) { mutableStateOf(false) } var lastValidUid by remember { - mutableStateOf(uid) + mutableIntStateOf(uid) } - val keyboardController = LocalSoftwareKeyboardController.current + OutlinedTextField( modifier = Modifier.fillMaxWidth(), label = { Text(label) }, @@ -357,13 +378,13 @@ private fun UidPanel(uid: Int, label: String, onUidChange: (Int) -> Unit) { }) } +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun SELinuxPanel( profile: Natives.Profile, onSELinuxChange: (domain: String, rules: String) -> Unit ) { - var showDialog by remember { mutableStateOf(false) } - if (showDialog) { + val editSELinuxDialog = rememberCustomDialog { dismiss -> var domain by remember { mutableStateOf(profile.context) } var rules by remember { mutableStateOf(profile.rules) } @@ -415,7 +436,7 @@ private fun SELinuxPanel( onSELinuxChange(domain, rules) }, onCloseRequest = { - showDialog = false + dismiss() }), header = Header.Default( title = stringResource(R.string.profile_selinux_context), @@ -434,10 +455,10 @@ private fun SELinuxPanel( modifier = Modifier .fillMaxWidth() .clickable { - showDialog = true + editSELinuxDialog.show() }, enabled = false, - colors = TextFieldDefaults.outlinedTextFieldColors( + colors = OutlinedTextFieldDefaults.colors( disabledTextColor = MaterialTheme.colorScheme.onSurface, disabledBorderColor = MaterialTheme.colorScheme.outline, disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, @@ -445,7 +466,7 @@ private fun SELinuxPanel( ), label = { Text(text = stringResource(R.string.profile_selinux_context)) }, value = profile.context, - onValueChange = { }, + onValueChange = { } ) }) } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfig.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfig.kt new file mode 100644 index 000000000000..b60e8ea46495 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfig.kt @@ -0,0 +1,117 @@ +package me.weishu.kernelsu.ui.component.profile + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ReadMore +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.material.icons.filled.Create +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MenuAnchorType +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import me.weishu.kernelsu.Natives +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.util.listAppProfileTemplates +import me.weishu.kernelsu.ui.util.setSepolicy +import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById + +/** + * @author weishu + * @date 2023/10/21. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TemplateConfig( + profile: Natives.Profile, + onViewTemplate: (id: String) -> Unit = {}, + onManageTemplate: () -> Unit = {}, + onProfileChange: (Natives.Profile) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + var template by rememberSaveable { + mutableStateOf(profile.rootTemplate ?: "") + } + val profileTemplates = listAppProfileTemplates() + val noTemplates = profileTemplates.isEmpty() + + ListItem(headlineContent = { + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = it }, + ) { + OutlinedTextField( + modifier = Modifier + .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .fillMaxWidth(), + readOnly = true, + label = { Text(stringResource(R.string.profile_template)) }, + value = template.ifEmpty { "None" }, + onValueChange = {}, + trailingIcon = { + if (noTemplates) { + IconButton( + onClick = onManageTemplate + ) { + Icon(Icons.Filled.Create, null) + } + } else if (expanded) Icon(Icons.Filled.ArrowDropUp, null) + else Icon(Icons.Filled.ArrowDropDown, null) + }, + ) + if (profileTemplates.isEmpty()) { + return@ExposedDropdownMenuBox + } + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + profileTemplates.forEach { tid -> + val templateInfo = + getTemplateInfoById(tid) ?: return@forEach + DropdownMenuItem( + text = { Text(tid) }, + onClick = { + template = tid + if (setSepolicy(tid, templateInfo.rules.joinToString("\n"))) { + onProfileChange( + profile.copy( + rootTemplate = tid, + rootUseDefault = false, + uid = templateInfo.uid, + gid = templateInfo.gid, + groups = templateInfo.groups, + capabilities = templateInfo.capabilities, + context = templateInfo.context, + namespace = templateInfo.namespace, + ) + ) + } + expanded = false + }, + trailingIcon = { + IconButton(onClick = { + onViewTemplate(tid) + }) { + Icon(Icons.AutoMirrored.Filled.ReadMore, null) + } + } + ) + } + } + } + }) +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt index 1c353e4baa25..30e96e97aac3 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/AppProfile.kt @@ -7,33 +7,36 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Android -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.ArrowDropUp import androidx.compose.material.icons.filled.Security -import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.FilterChip +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -43,6 +46,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -54,6 +58,9 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import coil.request.ImageRequest 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.TemplateEditorScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch import me.weishu.kernelsu.Natives @@ -61,6 +68,7 @@ import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.SwitchItem import me.weishu.kernelsu.ui.component.profile.AppProfileConfig import me.weishu.kernelsu.ui.component.profile.RootProfileConfig +import me.weishu.kernelsu.ui.component.profile.TemplateConfig import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.forceStopApp import me.weishu.kernelsu.ui.util.getSepolicy @@ -68,24 +76,25 @@ import me.weishu.kernelsu.ui.util.launchApp import me.weishu.kernelsu.ui.util.restartApp import me.weishu.kernelsu.ui.util.setSepolicy import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel +import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById /** * @author weishu * @date 2023/5/16. */ -@Destination +@OptIn(ExperimentalMaterial3Api::class) +@Destination @Composable fun AppProfileScreen( navigator: DestinationsNavigator, appInfo: SuperUserViewModel.AppInfo, ) { val context = LocalContext.current - val snackbarHost = LocalSnackbarHost.current + val snackBarHost = LocalSnackbarHost.current + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() val scope = rememberCoroutineScope() - val failToUpdateAppProfile = - stringResource(R.string.failed_to_update_app_profile).format(appInfo.label) - val failToUpdateSepolicy = - stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label) + val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(appInfo.label) + val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(appInfo.label) val packageName = appInfo.packageName val initialProfile = Natives.getAppProfile(packageName, appInfo.uid) @@ -97,20 +106,25 @@ fun AppProfileScreen( } Scaffold( - topBar = { TopBar { navigator.popBackStack() } }, + topBar = { + TopBar( + onBack = { navigator.popBackStack() }, + scrollBehavior = scrollBehavior + ) + }, + snackbarHost = { SnackbarHost(hostState = snackBarHost) }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) ) { paddingValues -> AppProfileInner( modifier = Modifier .padding(paddingValues) + .nestedScroll(scrollBehavior.nestedScrollConnection) .verticalScroll(rememberScrollState()), packageName = appInfo.packageName, appLabel = appInfo.label, appIcon = { AsyncImage( - model = ImageRequest.Builder(context) - .data(appInfo.packageInfo) - .crossfade(true) - .build(), + model = ImageRequest.Builder(context).data(appInfo.packageInfo).crossfade(true).build(), contentDescription = appInfo.label, modifier = Modifier .padding(4.dp) @@ -119,16 +133,24 @@ fun AppProfileScreen( ) }, profile = profile, + onViewTemplate = { + getTemplateInfoById(it)?.let { info -> + navigator.navigate(TemplateEditorScreenDestination(info)) + } + }, + onManageTemplate = { + navigator.navigate(AppProfileTemplateScreenDestination()) + }, onProfileChange = { scope.launch { if (it.allowSu && !it.rootUseDefault && it.rules.isNotEmpty()) { if (!setSepolicy(profile.name, it.rules)) { - snackbarHost.showSnackbar(failToUpdateSepolicy) + snackBarHost.showSnackbar(failToUpdateSepolicy) return@launch } } if (!Natives.setAppProfile(it)) { - snackbarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid)) + snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid)) } else { profile = it } @@ -138,7 +160,6 @@ fun AppProfileScreen( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun AppProfileInner( modifier: Modifier = Modifier, @@ -146,6 +167,8 @@ private fun AppProfileInner( appLabel: String, appIcon: @Composable () -> Unit, profile: Natives.Profile, + onViewTemplate: (id: String) -> Unit = {}, + onManageTemplate: () -> Unit = {}, onProfileChange: (Natives.Profile) -> Unit, ) { val isRootGranted = profile.allowSu @@ -167,7 +190,9 @@ private fun AppProfileInner( ) Crossfade(targetState = isRootGranted, label = "") { current -> - Column { + Column( + modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */) + ) { if (current) { val initialMode = if (profile.rootUseDefault) { Mode.Default @@ -176,10 +201,10 @@ private fun AppProfileInner( } else { Mode.Custom } - var mode by remember { + var mode by rememberSaveable { mutableStateOf(initialMode) } - ProfileBox(mode, false) { + ProfileBox(mode, true) { // template mode shouldn't change profile here! if (it == Mode.Default || it == Mode.Custom) { onProfileChange(profile.copy(rootUseDefault = it == Mode.Default)) @@ -188,43 +213,12 @@ private fun AppProfileInner( } Crossfade(targetState = mode, label = "") { currentMode -> if (currentMode == Mode.Template) { - var expanded by remember { mutableStateOf(false) } - val templateNone = "None" - var template by rememberSaveable { - mutableStateOf( - profile.rootTemplate - ?: templateNone - ) - } - ListItem(headlineContent = { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = it }, - ) { - OutlinedTextField( - modifier = Modifier.menuAnchor(), - readOnly = true, - label = { Text(stringResource(R.string.profile_template)) }, - value = template, - onValueChange = { - if (template != templateNone) { - onProfileChange( - profile.copy( - rootTemplate = it, - rootUseDefault = false - ) - ) - template = it - } - }, - trailingIcon = { - if (expanded) Icon(Icons.Filled.ArrowDropUp, null) - else Icon(Icons.Filled.ArrowDropDown, null) - }, - ) - // TODO: Template - } - }) + TemplateConfig( + profile = profile, + onViewTemplate = onViewTemplate, + onManageTemplate = onManageTemplate, + onProfileChange = onProfileChange + ) } else if (mode == Mode.Custom) { RootProfileConfig( fixedName = true, @@ -254,9 +248,7 @@ private fun AppProfileInner( } private enum class Mode(@StringRes private val res: Int) { - Default(R.string.profile_default), - Template(R.string.profile_template), - Custom(R.string.profile_custom); + Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom); val text: String @Composable get() = stringResource(res) @@ -264,7 +256,10 @@ private enum class Mode(@StringRes private val res: Int) { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TopBar(onBack: () -> Unit) { +private fun TopBar( + onBack: () -> Unit, + scrollBehavior: TopAppBarScrollBehavior? = null +) { TopAppBar( title = { Text(stringResource(R.string.profile)) @@ -272,12 +267,13 @@ private fun TopBar(onBack: () -> Unit) { navigationIcon = { IconButton( onClick = onBack - ) { Icon(Icons.Filled.ArrowBack, contentDescription = null) } + ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior ) } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ProfileBox( mode: Mode, @@ -289,11 +285,10 @@ private fun ProfileBox( supportingContent = { Text(mode.text) }, leadingContent = { Icon(Icons.Filled.AccountCircle, null) }, ) - Divider(thickness = Dp.Hairline) + HorizontalDivider(thickness = Dp.Hairline) ListItem(headlineContent = { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly ) { FilterChip( selected = mode == Mode.Default, diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt index 9345ced52ff0..c9637ed2e979 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/BottomBarDestination.kt @@ -5,11 +5,11 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.ui.graphics.vector.ImageVector +import com.ramcosta.composedestinations.generated.destinations.HomeScreenDestination +import com.ramcosta.composedestinations.generated.destinations.ModuleScreenDestination +import com.ramcosta.composedestinations.generated.destinations.SuperUserScreenDestination import com.ramcosta.composedestinations.spec.DirectionDestinationSpec import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.screen.destinations.HomeScreenDestination -import me.weishu.kernelsu.ui.screen.destinations.SuperUserScreenDestination -import me.weishu.kernelsu.ui.screen.destinations.ModuleScreenDestination enum class BottomBarDestination( val direction: DirectionDestinationSpec, diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt new file mode 100644 index 000000000000..93b06a0ca514 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/ExecuteModuleAction.kt @@ -0,0 +1,149 @@ +package me.weishu.kernelsu.ui.screen + +import android.os.Environment +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Save +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +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 kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.component.KeyEventBlocker +import me.weishu.kernelsu.ui.util.LocalSnackbarHost +import me.weishu.kernelsu.ui.util.runModuleAction +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Composable +@Destination +fun ExecuteModuleActionScreen(navigator: DestinationsNavigator, moduleId: String) { + var text by rememberSaveable { mutableStateOf("") } + var tempText : String + val logContent = rememberSaveable { StringBuilder() } + val snackBarHost = LocalSnackbarHost.current + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + var actionResult: Boolean + + LaunchedEffect(Unit) { + if (text.isNotEmpty()) { + return@LaunchedEffect + } + withContext(Dispatchers.IO) { + runModuleAction( + moduleId = moduleId, + onStdout = { + tempText = "$it\n" + if (tempText.startsWith("")) { // clear command + text = tempText.substring(6) + } else { + text += tempText + } + logContent.append(it).append("\n") + }, + onStderr = { + logContent.append(it).append("\n") + } + ).let { + actionResult = it + } + } + if (actionResult) navigator.popBackStack() + } + + Scaffold( + topBar = { + TopBar( + onBack = { + navigator.popBackStack() + }, + onSave = { + scope.launch { + val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) + val date = format.format(Date()) + val file = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "KernelSU_module_action_log_${date}.log" + ) + file.writeText(logContent.toString()) + snackBarHost.showSnackbar("Log saved to ${file.absolutePath}") + } + } + ) + }, + snackbarHost = { SnackbarHost(snackBarHost) } + ) { innerPadding -> + KeyEventBlocker { + it.key == Key.VolumeDown || it.key == Key.VolumeUp + } + Column( + modifier = Modifier + .fillMaxSize(1f) + .padding(innerPadding) + .verticalScroll(scrollState), + ) { + LaunchedEffect(text) { + scrollState.animateScrollTo(scrollState.maxValue) + } + Text( + modifier = Modifier.padding(8.dp), + text = text, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontFamily = FontFamily.Monospace, + lineHeight = MaterialTheme.typography.bodySmall.lineHeight, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) { + TopAppBar( + title = { Text(stringResource(R.string.action)) }, + navigationIcon = { + IconButton( + onClick = onBack + ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + }, + actions = { + IconButton(onClick = onSave) { + Icon( + imageVector = Icons.Filled.Save, + contentDescription = stringResource(id = R.string.save_log), + ) + } + } + ) +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt new file mode 100644 index 000000000000..c8bd8c861056 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Flash.kt @@ -0,0 +1,266 @@ +package me.weishu.kernelsu.ui.screen + +import android.net.Uri +import android.os.Environment +import android.os.Parcelable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Save +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.tooling.preview.Preview +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.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.component.KeyEventBlocker +import me.weishu.kernelsu.ui.util.LkmSelection +import me.weishu.kernelsu.ui.util.LocalSnackbarHost +import me.weishu.kernelsu.ui.util.flashModule +import me.weishu.kernelsu.ui.util.installBoot +import me.weishu.kernelsu.ui.util.reboot +import me.weishu.kernelsu.ui.util.restoreBoot +import me.weishu.kernelsu.ui.util.uninstallPermanently +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +enum class FlashingStatus { + FLASHING, + SUCCESS, + FAILED +} + +/** + * @author weishu + * @date 2023/1/1. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@Destination +fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { + + var text by rememberSaveable { mutableStateOf("") } + var tempText : String + val logContent = rememberSaveable { StringBuilder() } + var showFloatAction by rememberSaveable { mutableStateOf(false) } + + val snackBarHost = LocalSnackbarHost.current + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + var flashing by rememberSaveable { + mutableStateOf(FlashingStatus.FLASHING) + } + + LaunchedEffect(Unit) { + if (text.isNotEmpty()) { + return@LaunchedEffect + } + withContext(Dispatchers.IO) { + flashIt(flashIt, onFinish = { showReboot, code -> + if (code != 0) { + text += "Error: exit code = $code.\nPlease save and check the log.\n" + } + if (showReboot) { + text += "\n\n\n" + showFloatAction = true + } + flashing = if (code == 0) FlashingStatus.SUCCESS else FlashingStatus.FAILED + }, onStdout = { + tempText = "$it\n" + if (tempText.startsWith("")) { // clear command + text = tempText.substring(6) + } else { + text += tempText + } + logContent.append(it).append("\n") + }, onStderr = { + logContent.append(it).append("\n") + }) + } + } + + Scaffold( + topBar = { + TopBar( + flashing, + onBack = { + navigator.popBackStack() + }, + onSave = { + scope.launch { + val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) + val date = format.format(Date()) + val file = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + "KernelSU_install_log_${date}.log" + ) + file.writeText(logContent.toString()) + snackBarHost.showSnackbar("Log saved to ${file.absolutePath}") + } + }, + scrollBehavior = scrollBehavior + ) + }, + floatingActionButton = { + if (showFloatAction) { + val reboot = stringResource(id = R.string.reboot) + ExtendedFloatingActionButton( + onClick = { + scope.launch { + withContext(Dispatchers.IO) { + reboot() + } + } + }, + icon = { Icon(Icons.Filled.Refresh, reboot) }, + text = { Text(text = reboot) }, + ) + } + }, + snackbarHost = { SnackbarHost(hostState = snackBarHost) }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) { innerPadding -> + KeyEventBlocker { + it.key == Key.VolumeDown || it.key == Key.VolumeUp + } + Column( + modifier = Modifier + .fillMaxSize(1f) + .padding(innerPadding) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .verticalScroll(scrollState), + ) { + LaunchedEffect(text) { + scrollState.animateScrollTo(scrollState.maxValue) + } + Text( + modifier = Modifier.padding(8.dp), + text = text, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontFamily = FontFamily.Monospace, + lineHeight = MaterialTheme.typography.bodySmall.lineHeight, + ) + } + } +} + +@Parcelize +sealed class FlashIt : Parcelable { + data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : + FlashIt() + + data class FlashModule(val uri: Uri) : FlashIt() + + data object FlashRestore : FlashIt() + + data object FlashUninstall : FlashIt() +} + +fun flashIt( + flashIt: FlashIt, onFinish: (Boolean, Int) -> Unit, + onStdout: (String) -> Unit, + onStderr: (String) -> Unit +) { + when (flashIt) { + is FlashIt.FlashBoot -> installBoot( + flashIt.boot, + flashIt.lkm, + flashIt.ota, + onFinish, + onStdout, + onStderr + ) + + is FlashIt.FlashModule -> flashModule(flashIt.uri, onFinish, onStdout, onStderr) + + FlashIt.FlashRestore -> restoreBoot(onFinish, onStdout, onStderr) + + FlashIt.FlashUninstall -> uninstallPermanently(onFinish, onStdout, onStderr) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + status: FlashingStatus, + onBack: () -> Unit = {}, + onSave: () -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null +) { + TopAppBar( + title = { + Text( + stringResource( + when (status) { + FlashingStatus.FLASHING -> R.string.flashing + FlashingStatus.SUCCESS -> R.string.flash_success + FlashingStatus.FAILED -> R.string.flash_failed + } + ) + ) + }, + navigationIcon = { + IconButton( + onClick = onBack + ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + }, + actions = { + IconButton(onClick = onSave) { + Icon( + imageVector = Icons.Filled.Save, + contentDescription = "Localized description" + ) + } + }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior + ) +} + +@Preview +@Composable +fun InstallPreview() { + InstallScreen(EmptyDestinationsNavigator) +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt index 9d3f5fac18cc..4631733e8db0 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Home.kt @@ -5,11 +5,13 @@ import android.os.Build import android.os.PowerManager import android.system.Os import androidx.annotation.StringRes +import androidx.compose.animation.* import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Archive import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.outlined.Block @@ -20,48 +22,65 @@ import androidx.compose.runtime.* 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.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.content.pm.PackageInfoCompat import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination +import com.ramcosta.composedestinations.generated.destinations.SettingScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.* import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.ConfirmDialog -import me.weishu.kernelsu.ui.component.ConfirmResult -import me.weishu.kernelsu.ui.screen.destinations.SettingScreenDestination +import me.weishu.kernelsu.ui.component.rememberConfirmDialog import me.weishu.kernelsu.ui.util.* +import me.weishu.kernelsu.ui.util.module.LatestVersionInfo -@RootNavGraph(start = true) -@Destination +@OptIn(ExperimentalMaterial3Api::class) +@Destination(start = true) @Composable fun HomeScreen(navigator: DestinationsNavigator) { - Scaffold(topBar = { - TopBar(onSettingsClick = { - navigator.navigate(SettingScreenDestination) - }) - }) { innerPadding -> + val kernelVersion = getKernelVersion() + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + Scaffold( + topBar = { + TopBar( + kernelVersion, + onSettingsClick = { + navigator.navigate(SettingScreenDestination) + }, + onInstallClick = { + navigator.navigate(InstallScreenDestination) + }, + scrollBehavior = scrollBehavior + ) + }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) - .padding(horizontal = 16.dp) - .verticalScroll(rememberScrollState()), + .nestedScroll(scrollBehavior.nestedScrollConnection) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - val kernelVersion = getKernelVersion() val isManager = Natives.becomeManager(ksuApp.packageName) - SideEffect { - if (isManager) install() - } val ksuVersion = if (isManager) Natives.version else null + val lkmMode = ksuVersion?.let { + if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) Natives.isLkmMode else null + } - StatusCard(kernelVersion, ksuVersion) + StatusCard(kernelVersion, ksuVersion, lkmMode) { + navigator.navigate(InstallScreenDestination) + } if (isManager && Natives.requireNewKernel()) { WarningCard( stringResource(id = R.string.require_kernel_version).format( @@ -69,12 +88,21 @@ fun HomeScreen(navigator: DestinationsNavigator) { ) ) } - UpdateCard() + if (ksuVersion != null && !rootAvailable()) { + WarningCard( + stringResource(id = R.string.grant_root_failed) + ) + } + val checkUpdate = + LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) + .getBoolean("check_update", true) + if (checkUpdate) { + UpdateCard() + } InfoCard() DonateCard() LearnMoreCard() Spacer(Modifier) - ConfirmDialog() } } } @@ -82,35 +110,41 @@ fun HomeScreen(navigator: DestinationsNavigator) { @Composable fun UpdateCard() { val context = LocalContext.current - val newVersion by produceState(initialValue = Triple(0, "", "")) { - value = withContext(Dispatchers.IO) { checkNewVersion() } + val latestVersionInfo = LatestVersionInfo() + val newVersion by produceState(initialValue = latestVersionInfo) { + value = withContext(Dispatchers.IO) { + checkNewVersion() + } } + val currentVersionCode = getManagerVersion(context).second - val newVersionCode = newVersion.first - val newVersionUrl = newVersion.second - val changelog = newVersion.third - if (newVersionCode <= currentVersionCode) { - return - } + val newVersionCode = newVersion.versionCode + val newVersionUrl = newVersion.downloadUrl + val changelog = newVersion.changelog val uriHandler = LocalUriHandler.current - val dialogHost = LocalDialogHost.current val title = stringResource(id = R.string.module_changelog) val updateText = stringResource(id = R.string.module_update) - val scope = rememberCoroutineScope() - WarningCard( - message = stringResource(id = R.string.new_version_available).format(newVersionCode), - MaterialTheme.colorScheme.outlineVariant + + AnimatedVisibility( + visible = newVersionCode > currentVersionCode, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut() ) { - scope.launch { - if (changelog.isEmpty() || dialogHost.showConfirm( + val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) }) + WarningCard( + message = stringResource(id = R.string.new_version_available).format(newVersionCode), + MaterialTheme.colorScheme.outlineVariant + ) { + if (changelog.isEmpty()) { + uriHandler.openUri(newVersionUrl) + } else { + updateDialog.showConfirm( title = title, content = changelog, markdown = true, - confirm = updateText, - ) == ConfirmResult.Confirmed - ) { - uriHandler.openUri(newVersionUrl) + confirm = updateText + ) } } } @@ -127,72 +161,104 @@ fun RebootDropdownItem(@StringRes id: Int, reason: String = "") { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TopBar(onSettingsClick: () -> Unit) { - TopAppBar(title = { Text(stringResource(R.string.app_name)) }, actions = { - var showDropdown by remember { mutableStateOf(false) } - IconButton(onClick = { - showDropdown = true - }) { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = stringResource(id = R.string.reboot) - ) +private fun TopBar( + kernelVersion: KernelVersion, + onInstallClick: () -> Unit, + onSettingsClick: () -> Unit, + scrollBehavior: TopAppBarScrollBehavior? = null +) { + TopAppBar( + title = { Text(stringResource(R.string.app_name)) }, + actions = { + if (kernelVersion.isGKI()) { + IconButton(onClick = onInstallClick) { + Icon( + imageVector = Icons.Filled.Archive, + contentDescription = stringResource(id = R.string.install) + ) + } + } - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false + var showDropdown by remember { mutableStateOf(false) } + IconButton(onClick = { + showDropdown = true }) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringResource(id = R.string.reboot) + ) + + DropdownMenu(expanded = showDropdown, onDismissRequest = { + showDropdown = false + }) { - RebootDropdownItem(id = R.string.reboot) + RebootDropdownItem(id = R.string.reboot) - val pm = - LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { - RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace") + 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") } - } - IconButton(onClick = onSettingsClick) { - Icon( - imageVector = Icons.Filled.Settings, - contentDescription = stringResource(id = R.string.settings) - ) - } - }) + IconButton(onClick = onSettingsClick) { + Icon( + imageVector = Icons.Filled.Settings, + contentDescription = stringResource(id = R.string.settings) + ) + } + }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior + ) } @Composable -private fun StatusCard(kernelVersion: KernelVersion, ksuVersion: Int?) { +private fun StatusCard( + kernelVersion: KernelVersion, + ksuVersion: Int?, + lkmMode: Boolean?, + onClickInstall: () -> Unit = {} +) { ElevatedCard( colors = CardDefaults.elevatedCardColors(containerColor = run { if (ksuVersion != null) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer }) ) { - val uriHandler = LocalUriHandler.current Row(modifier = Modifier .fillMaxWidth() .clickable { - if (kernelVersion.isGKI() && ksuVersion == null) { - uriHandler.openUri("https://kernelsu.org/guide/installation.html") + if (kernelVersion.isGKI()) { + onClickInstall() } } .padding(24.dp), verticalAlignment = Alignment.CenterVertically) { when { ksuVersion != null -> { - val appendText = if (Natives.isSafeMode) { - " [${stringResource(id = R.string.safe_mode)}]" - } else { - "" + val safeMode = when { + Natives.isSafeMode -> " [${stringResource(id = R.string.safe_mode)}]" + else -> "" } + + val workingMode = when (lkmMode) { + null -> "" + true -> " " + else -> " " + } + + val workingText = + "${stringResource(id = R.string.home_working)}$workingMode$safeMode" + Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working)) Column(Modifier.padding(start = 20.dp)) { Text( - text = stringResource(R.string.home_working) + appendText, + text = workingText, style = MaterialTheme.typography.titleMedium ) Spacer(Modifier.height(4.dp)) @@ -283,7 +349,7 @@ fun LearnMoreCard() { uriHandler.openUri(url) } .padding(24.dp), verticalAlignment = Alignment.CenterVertically) { - Column() { + Column { Text( text = stringResource(R.string.home_learn_kernelsu), style = MaterialTheme.typography.titleSmall @@ -310,7 +376,7 @@ fun DonateCard() { uriHandler.openUri("https://patreon.com/weishu") } .padding(24.dp), verticalAlignment = Alignment.CenterVertically) { - Column() { + Column { Text( text = stringResource(R.string.home_support_title), style = MaterialTheme.typography.titleSmall @@ -363,18 +429,20 @@ private fun InfoCard() { } } -fun getManagerVersion(context: Context): Pair { - val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) - return Pair(packageInfo.versionName, packageInfo.versionCode) +fun getManagerVersion(context: Context): Pair { + val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!! + val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo) + return Pair(packageInfo.versionName!!, versionCode) } @Preview @Composable private fun StatusCardPreview() { Column { - StatusCard(KernelVersion(5, 10, 101), 1) - StatusCard(KernelVersion(5, 10, 101), null) - StatusCard(KernelVersion(4, 10, 101), null) + StatusCard(KernelVersion(5, 10, 101), 1, null) + StatusCard(KernelVersion(5, 10, 101), 20000, true) + StatusCard(KernelVersion(5, 10, 101), null, true) + StatusCard(KernelVersion(4, 10, 101), null, false) } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt index 04f012b4eafd..aa4843295159 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Install.kt @@ -1,160 +1,361 @@ package me.weishu.kernelsu.ui.screen +import android.app.Activity +import android.content.Intent import android.net.Uri -import android.os.Environment +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.StringRes +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.FileUpload +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.RadioButton +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.input.key.Key -import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +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.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.KeyEventBlocker -import me.weishu.kernelsu.ui.util.LocalSnackbarHost -import me.weishu.kernelsu.ui.util.installModule -import me.weishu.kernelsu.ui.util.reboot -import java.io.File -import java.text.SimpleDateFormat -import java.util.* +import me.weishu.kernelsu.ui.component.DialogHandle +import me.weishu.kernelsu.ui.component.rememberConfirmDialog +import me.weishu.kernelsu.ui.component.rememberCustomDialog +import me.weishu.kernelsu.ui.util.LkmSelection +import me.weishu.kernelsu.ui.util.getCurrentKmi +import me.weishu.kernelsu.ui.util.getSupportedKmis +import me.weishu.kernelsu.ui.util.isAbDevice +import me.weishu.kernelsu.ui.util.isInitBoot +import me.weishu.kernelsu.ui.util.rootAvailable /** * @author weishu - * @date 2023/1/1. + * @date 2024/3/12. */ -@OptIn(ExperimentalComposeUiApi::class) +@OptIn(ExperimentalMaterial3Api::class) +@Destination @Composable -@Destination -fun InstallScreen(navigator: DestinationsNavigator, uri: Uri) { +fun InstallScreen(navigator: DestinationsNavigator) { + var installMethod by remember { + mutableStateOf(null) + } - var text by rememberSaveable { mutableStateOf("") } - val logContent = StringBuilder() - var showFloatAction by rememberSaveable { mutableStateOf(false) } + var lkmSelection by remember { + mutableStateOf(LkmSelection.KmiNone) + } - val snackBarHost = LocalSnackbarHost.current - val scope = rememberCoroutineScope() - val scrollState = rememberScrollState() + val onInstall = { + installMethod?.let { method -> + val flashIt = FlashIt.FlashBoot( + boot = if (method is InstallMethod.SelectFile) method.uri else null, + lkm = lkmSelection, + ota = method is InstallMethod.DirectInstallToInactiveSlot + ) + navigator.navigate(FlashScreenDestination(flashIt)) + } + } - LaunchedEffect(Unit) { - if (text.isNotEmpty()) { - return@LaunchedEffect + val currentKmi by produceState(initialValue = "") { value = getCurrentKmi() } + + val selectKmiDialog = rememberSelectKmiDialog { kmi -> + kmi?.let { + lkmSelection = LkmSelection.KmiString(it) + onInstall() } - withContext(Dispatchers.IO) { - installModule(uri, onFinish = { success -> - if (success) { - showFloatAction = true - } - }, onStdout = { - text += "$it\n" - scope.launch { - scrollState.animateScrollTo(scrollState.maxValue) + } + + val onClickNext = { + if (lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) { + // no lkm file selected and cannot get current kmi + selectKmiDialog.show() + } else { + onInstall() + } + } + + val selectLkmLauncher = + rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + it.data?.data?.let { uri -> + lkmSelection = LkmSelection.LkmUri(uri) } - logContent.append(it).append("\n") - }, onStderr = { - logContent.append(it).append("\n") - }); + } } + + val onLkmUpload = { + selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { + type = "application/octet-stream" + }) } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + Scaffold( topBar = { TopBar( - onBack = { - navigator.popBackStack() - }, - onSave = { - scope.launch { - val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) - val date = format.format(Date()) - val file = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), - "KernelSU_install_log_${date}.log" - ) - file.writeText(logContent.toString()) - snackBarHost.showSnackbar("Log saved to ${file.absolutePath}") - } - } + onBack = { navigator.popBackStack() }, + onLkmUpload = onLkmUpload, + scrollBehavior = scrollBehavior ) }, - floatingActionButton = { - if (showFloatAction) { - val reboot = stringResource(id = R.string.reboot) - ExtendedFloatingActionButton( + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .verticalScroll(rememberScrollState()) + ) { + SelectInstallMethod { method -> + installMethod = method + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + (lkmSelection as? LkmSelection.LkmUri)?.let { + Text( + stringResource( + id = R.string.selected_lkm, + it.uri.lastPathSegment ?: "(file)" + ) + ) + } + Button(modifier = Modifier.fillMaxWidth(), + enabled = installMethod != null, onClick = { - scope.launch { - withContext(Dispatchers.IO) { - reboot() - } - } + onClickNext() + }) { + Text( + stringResource(id = R.string.install_next), + fontSize = MaterialTheme.typography.bodyMedium.fontSize + ) + } + } + } + } +} + +sealed class InstallMethod { + data class SelectFile( + val uri: Uri? = null, + @StringRes override val label: Int = R.string.select_file, + override val summary: String? + ) : InstallMethod() + + data object DirectInstall : InstallMethod() { + override val label: Int + get() = R.string.direct_install + } + + data object DirectInstallToInactiveSlot : InstallMethod() { + override val label: Int + get() = R.string.install_inactive_slot + } + + abstract val label: Int + open val summary: String? = null +} + +@Composable +private fun SelectInstallMethod(onSelected: (InstallMethod) -> Unit = {}) { + val rootAvailable = rootAvailable() + val isAbDevice = isAbDevice() + val selectFileTip = stringResource( + id = R.string.select_file_tip, if (isInitBoot()) "init_boot" else "boot" + ) + val radioOptions = + mutableListOf(InstallMethod.SelectFile(summary = selectFileTip)) + if (rootAvailable) { + radioOptions.add(InstallMethod.DirectInstall) + + if (isAbDevice) { + radioOptions.add(InstallMethod.DirectInstallToInactiveSlot) + } + } + + var selectedOption by remember { mutableStateOf(null) } + val selectImageLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + it.data?.data?.let { uri -> + val option = InstallMethod.SelectFile(uri, summary = selectFileTip) + selectedOption = option + onSelected(option) + } + } + } + + val confirmDialog = rememberConfirmDialog(onConfirm = { + selectedOption = InstallMethod.DirectInstallToInactiveSlot + onSelected(InstallMethod.DirectInstallToInactiveSlot) + }, onDismiss = null) + val dialogTitle = stringResource(id = android.R.string.dialog_alert_title) + val dialogContent = stringResource(id = R.string.install_inactive_slot_warning) + + val onClick = { option: InstallMethod -> + + when (option) { + is InstallMethod.SelectFile -> { + selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { + type = "application/octet-stream" + }) + } + + is InstallMethod.DirectInstall -> { + selectedOption = option + onSelected(option) + } + + is InstallMethod.DirectInstallToInactiveSlot -> { + confirmDialog.showConfirm(dialogTitle, dialogContent) + } + } + } + + Column { + radioOptions.forEach { option -> + val interactionSource = remember { MutableInteractionSource() } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .toggleable( + value = option.javaClass == selectedOption?.javaClass, + onValueChange = { + onClick(option) + }, + role = Role.RadioButton, + indication = LocalIndication.current, + interactionSource = interactionSource + ) + ) { + RadioButton( + selected = option.javaClass == selectedOption?.javaClass, + onClick = { + onClick(option) }, - icon = { Icon(Icons.Filled.Refresh, reboot) }, - text = { Text(text = reboot) }, + interactionSource = interactionSource ) + Column( + modifier = Modifier.padding(vertical = 12.dp) + ) { + Text( + text = stringResource(id = option.label), + fontSize = MaterialTheme.typography.titleMedium.fontSize, + fontFamily = MaterialTheme.typography.titleMedium.fontFamily, + fontStyle = MaterialTheme.typography.titleMedium.fontStyle + ) + option.summary?.let { + Text( + text = it, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + fontFamily = MaterialTheme.typography.bodySmall.fontFamily, + fontStyle = MaterialTheme.typography.bodySmall.fontStyle + ) + } + } } - } - ) { innerPadding -> - KeyEventBlocker { - it.key == Key.VolumeDown || it.key == Key.VolumeUp + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle { + return rememberCustomDialog { dismiss -> + val supportedKmi by produceState(initialValue = emptyList()) { + value = getSupportedKmis() } - Column( - modifier = Modifier - .fillMaxSize(1f) - .padding(innerPadding) - .verticalScroll(scrollState), - ) { - Text( - modifier = Modifier.padding(8.dp), - text = text, - fontSize = MaterialTheme.typography.bodySmall.fontSize, - fontFamily = FontFamily.Monospace, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, + val options = supportedKmi.map { value -> + ListOption( + titleText = value ) } + + var selection by remember { mutableStateOf(null) } + ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = { + onSelected(selection) + }, onCloseRequest = { + dismiss() + }), header = Header.Default( + title = stringResource(R.string.select_kmi), + ), selection = ListSelection.Single( + showRadioButtons = true, + options = options, + ) { _, option -> + selection = option.titleText + }) } } @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TopBar(onBack: () -> Unit = {}, onSave: () -> Unit = {}) { +private fun TopBar( + onBack: () -> Unit = {}, + onLkmUpload: () -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null +) { TopAppBar( - title = { Text(stringResource(R.string.install)) }, - navigationIcon = { + title = { Text(stringResource(R.string.install)) }, navigationIcon = { IconButton( onClick = onBack - ) { Icon(Icons.Filled.ArrowBack, contentDescription = null) } - }, - actions = { - IconButton(onClick = onSave) { - Icon( - imageVector = Icons.Filled.Save, - contentDescription = "Localized description" - ) + ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + }, actions = { + IconButton(onClick = onLkmUpload) { + Icon(Icons.Filled.FileUpload, contentDescription = null) } - } + }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior ) } -@Preview @Composable -fun InstallPreview() { -// InstallScreen(DestinationsNavigator(), uri = Uri.EMPTY) +@Preview +fun SelectInstallPreview() { + InstallScreen(EmptyDestinationsNavigator) } \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt index 61a46f2aca00..9463d9449a69 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Module.kt @@ -1,29 +1,78 @@ package me.weishu.kernelsu.ui.screen import android.app.Activity.RESULT_OK +import android.content.Context import android.content.Intent import android.net.Uri import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.LocalIndication +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.foundation.selection.toggleable import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Wysiwyg import androidx.compose.material.icons.filled.Add -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.outlined.PlayArrow +import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarDuration +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.SnackbarResult +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration @@ -33,27 +82,45 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination +import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.ConfirmDialog import me.weishu.kernelsu.ui.component.ConfirmResult -import me.weishu.kernelsu.ui.component.LoadingDialog -import me.weishu.kernelsu.ui.screen.destinations.InstallScreenDestination -import me.weishu.kernelsu.ui.util.* +import me.weishu.kernelsu.ui.component.SearchAppBar +import me.weishu.kernelsu.ui.component.rememberConfirmDialog +import me.weishu.kernelsu.ui.component.rememberLoadingDialog +import me.weishu.kernelsu.ui.util.DownloadListener +import me.weishu.kernelsu.ui.util.LocalSnackbarHost +import me.weishu.kernelsu.ui.util.download +import me.weishu.kernelsu.ui.util.hasMagisk +import me.weishu.kernelsu.ui.util.reboot +import me.weishu.kernelsu.ui.util.toggleModule +import me.weishu.kernelsu.ui.util.uninstallModule import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel +import me.weishu.kernelsu.ui.webui.WebUIActivity import okhttp3.OkHttpClient -@Destination +@OptIn(ExperimentalMaterial3Api::class) +@Destination @Composable fun ModuleScreen(navigator: DestinationsNavigator) { val viewModel = viewModel() + val context = LocalContext.current + val snackBarHost = LocalSnackbarHost.current + val scope = rememberCoroutineScope() + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) LaunchedEffect(Unit) { if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) { + viewModel.sortEnabledFirst = prefs.getBoolean("module_sort_enabled_first", false) + viewModel.sortActionFirst = prefs.getBoolean("module_sort_action_first", false) viewModel.fetchModuleList() } } @@ -63,46 +130,108 @@ fun ModuleScreen(navigator: DestinationsNavigator) { val hideInstallButton = isSafeMode || hasMagisk - Scaffold(topBar = { - TopBar() - }, floatingActionButton = if (hideInstallButton) { - { /* Empty */ } - } else { - { - val moduleInstall = stringResource(id = R.string.module_install) - val selectZipLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { - if (it.resultCode != RESULT_OK) { - return@rememberLauncherForActivityResult - } - val data = it.data ?: return@rememberLauncherForActivityResult - val uri = data.data ?: return@rememberLauncherForActivityResult + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - navigator.navigate(InstallScreenDestination(uri)) + val webUILauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { viewModel.fetchModuleList() } - viewModel.markNeedRefresh() + Scaffold( + topBar = { + SearchAppBar( + title = { Text(stringResource(R.string.module)) }, + searchText = viewModel.search, + onSearchTextChange = { viewModel.search = it }, + onClearClick = { viewModel.search = "" }, + dropdownContent = { + var showDropdown by remember { mutableStateOf(false) } - Log.i("ModuleScreen", "select zip result: ${it.data}") - } + IconButton( + onClick = { showDropdown = true }, + ) { + Icon( + imageVector = Icons.Filled.MoreVert, + contentDescription = stringResource(id = R.string.settings) + ) - ExtendedFloatingActionButton( - onClick = { - // select the zip file to install - val intent = Intent(Intent.ACTION_GET_CONTENT) - intent.type = "application/zip" - selectZipLauncher.launch(intent) + DropdownMenu(expanded = showDropdown, onDismissRequest = { + showDropdown = false + }) { + DropdownMenuItem(text = { + Text(stringResource(R.string.module_sort_action_first)) + }, trailingIcon = { + Checkbox(viewModel.sortActionFirst, null) + }, onClick = { + viewModel.sortActionFirst = + !viewModel.sortActionFirst + prefs.edit() + .putBoolean( + "module_sort_action_first", + viewModel.sortActionFirst + ) + .apply() + scope.launch { + viewModel.fetchModuleList() + } + }) + DropdownMenuItem(text = { + Text(stringResource(R.string.module_sort_enabled_first)) + }, trailingIcon = { + Checkbox(viewModel.sortEnabledFirst, null) + }, onClick = { + viewModel.sortEnabledFirst = + !viewModel.sortEnabledFirst + prefs.edit() + .putBoolean( + "module_sort_enabled_first", + viewModel.sortEnabledFirst + ) + .apply() + scope.launch { + viewModel.fetchModuleList() + } + }) + } + } }, - icon = { Icon(Icons.Filled.Add, moduleInstall) }, - text = { Text(text = moduleInstall) }, + scrollBehavior = scrollBehavior, ) - } - }) { innerPadding -> + }, + floatingActionButton = { + if (!hideInstallButton) { + val moduleInstall = stringResource(id = R.string.module_install) + val selectZipLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode != RESULT_OK) { + return@rememberLauncherForActivityResult + } + val data = it.data ?: return@rememberLauncherForActivityResult + val uri = data.data ?: return@rememberLauncherForActivityResult - ConfirmDialog() + navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri))) - LoadingDialog() + viewModel.markNeedRefresh() + Log.i("ModuleScreen", "select zip result: ${it.data}") + } + + ExtendedFloatingActionButton( + onClick = { + // select the zip file to install + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + type = "application/zip" + } + selectZipLauncher.launch(intent) + }, + icon = { Icon(Icons.Filled.Add, moduleInstall) }, + text = { Text(text = moduleInstall) }, + ) + } + }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + snackbarHost = { SnackbarHost(hostState = snackBarHost) } + ) { innerPadding -> when { hasMagisk -> { Box( @@ -120,40 +249,61 @@ fun ModuleScreen(navigator: DestinationsNavigator) { else -> { ModuleList( - viewModel = viewModel, modifier = Modifier - .padding(innerPadding) - .fillMaxSize() - ) { - navigator.navigate(InstallScreenDestination(it)) - } + navigator, + viewModel = viewModel, + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + boxModifier = Modifier.padding(innerPadding), + onInstallModule = { + navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(it))) + }, + onClickModule = { id, name, hasWebUi -> + if (hasWebUi) { + webUILauncher.launch( + Intent(context, WebUIActivity::class.java) + .setData(Uri.parse("kernelsu://webui/$id")) + .putExtra("id", id) + .putExtra("name", name) + ) + } + }, + context = context, + snackBarHost = snackBarHost + ) } } } } -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable private fun ModuleList( - viewModel: ModuleViewModel, modifier: Modifier = Modifier, onInstallModule: (Uri) -> Unit + navigator: DestinationsNavigator, + viewModel: ModuleViewModel, + modifier: Modifier = Modifier, + boxModifier: Modifier = Modifier, + onInstallModule: (Uri) -> Unit, + onClickModule: (id: String, name: String, hasWebUi: Boolean) -> Unit, + context: Context, + snackBarHost: SnackbarHostState ) { val failedEnable = stringResource(R.string.module_failed_to_enable) val failedDisable = stringResource(R.string.module_failed_to_disable) val failedUninstall = stringResource(R.string.module_uninstall_failed) val successUninstall = stringResource(R.string.module_uninstall_success) - val reboot = stringResource(id = R.string.reboot) - val rebootToApply = stringResource(id = R.string.reboot_to_apply) - val moduleStr = stringResource(id = R.string.module) - val uninstall = stringResource(id = R.string.uninstall) - val cancel = stringResource(id = android.R.string.cancel) - val moduleUninstallConfirm = stringResource(id = R.string.module_uninstall_confirm) + val reboot = stringResource(R.string.reboot) + val rebootToApply = stringResource(R.string.reboot_to_apply) + val moduleStr = stringResource(R.string.module) + val uninstall = stringResource(R.string.uninstall) + val cancel = stringResource(android.R.string.cancel) + val moduleUninstallConfirm = stringResource(R.string.module_uninstall_confirm) val updateText = stringResource(R.string.module_update) val changelogText = stringResource(R.string.module_changelog) val downloadingText = stringResource(R.string.module_downloading) val startDownloadingText = stringResource(R.string.module_start_downloading) + val fetchChangeLogFailed = stringResource(R.string.module_changelog_failed) - val dialogHost = LocalDialogHost.current - val snackBarHost = LocalSnackbarHost.current - val context = LocalContext.current + val loadingDialog = rememberLoadingDialog() + val confirmDialog = rememberConfirmDialog() suspend fun onModuleUpdate( module: ModuleViewModel.ModuleInfo, @@ -161,36 +311,48 @@ private fun ModuleList( downloadUrl: String, fileName: String ) { - val changelog = dialogHost.withLoading { + val changelogResult = loadingDialog.withLoading { withContext(Dispatchers.IO) { - OkHttpClient().newCall( - okhttp3.Request.Builder().url(changelogUrl).build() - ).execute().body!!.string() + runCatching { + OkHttpClient().newCall( + okhttp3.Request.Builder().url(changelogUrl).build() + ).execute().body!!.string() + } } } - if (changelog.isNotEmpty()) { - // changelog is not empty, show it and wait for confirm - val confirmResult = dialogHost.showConfirm( - changelogText, - content = changelog, - markdown = true, - confirm = updateText, - ) - - if (confirmResult != ConfirmResult.Confirmed) { - return + val showToast: suspend (String) -> Unit = { msg -> + withContext(Dispatchers.Main) { + Toast.makeText( + context, + msg, + Toast.LENGTH_SHORT + ).show() } } - withContext(Dispatchers.Main) { - Toast.makeText( - context, - startDownloadingText.format(module.name), - Toast.LENGTH_SHORT - ).show() + val changelog = changelogResult.getOrElse { + showToast(fetchChangeLogFailed.format(it.message)) + return + }.ifBlank { + showToast(fetchChangeLogFailed.format(module.name)) + return } + // changelog is not empty, show it and wait for confirm + val confirmResult = confirmDialog.awaitConfirm( + changelogText, + content = changelog, + markdown = true, + confirm = updateText, + ) + + if (confirmResult != ConfirmResult.Confirmed) { + return + } + + showToast(startDownloadingText.format(module.name)) + val downloading = downloadingText.format(module.name) withContext(Dispatchers.IO) { download( @@ -209,7 +371,7 @@ private fun ModuleList( } suspend fun onModuleUninstall(module: ModuleViewModel.ModuleInfo) { - val confirmResult = dialogHost.showConfirm( + val confirmResult = confirmDialog.awaitConfirm( moduleStr, content = moduleUninstallConfirm.format(module.name), confirm = uninstall, @@ -219,7 +381,7 @@ private fun ModuleList( return } - val success = dialogHost.withLoading { + val success = loadingDialog.withLoading { withContext(Dispatchers.IO) { uninstallModule(module.id) } @@ -238,26 +400,31 @@ private fun ModuleList( } else { null } - val result = snackBarHost.showSnackbar(message, actionLabel = actionLabel) + val result = snackBarHost.showSnackbar( + message = message, + actionLabel = actionLabel, + duration = SnackbarDuration.Long + ) if (result == SnackbarResult.ActionPerformed) { reboot() } } - - val refreshState = rememberPullRefreshState(refreshing = viewModel.isRefreshing, - onRefresh = { viewModel.fetchModuleList() }) - Box(modifier.pullRefresh(refreshState)) { - val context = LocalContext.current - + PullToRefreshBox( + modifier = boxModifier, + onRefresh = { + viewModel.fetchModuleList() + }, + isRefreshing = viewModel.isRefreshing + ) { LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = remember { PaddingValues( start = 16.dp, top = 16.dp, end = 16.dp, - bottom = 16.dp + 16.dp + 56.dp /* Scaffold Fab Spacing + Fab container height */ + bottom = 16.dp + 56.dp + 16.dp + 48.dp + 6.dp /* Scaffold Fab Spacing + Fab container height + SnackBar height */ ) }, ) { @@ -292,7 +459,6 @@ private fun ModuleList( else -> { items(viewModel.moduleList) { module -> - var isChecked by rememberSaveable(module) { mutableStateOf(module.enabled) } val scope = rememberCoroutineScope() val updatedModule by produceState(initialValue = Triple("", "", "")) { scope.launch(Dispatchers.IO) { @@ -300,40 +466,51 @@ private fun ModuleList( } } - ModuleItem(module, isChecked, updatedModule.first, onUninstall = { - scope.launch { onModuleUninstall(module) } - }, onCheckChanged = { - scope.launch { - val success = dialogHost.withLoading { - withContext(Dispatchers.IO) { - toggleModule(module.id, !isChecked) + ModuleItem( + navigator = navigator, + module = module, + updateUrl = updatedModule.first, + onUninstall = { + scope.launch { onModuleUninstall(module) } + }, + onCheckChanged = { + scope.launch { + val success = loadingDialog.withLoading { + withContext(Dispatchers.IO) { + toggleModule(module.id, !module.enabled) + } + } + if (success) { + viewModel.fetchModuleList() + + val result = snackBarHost.showSnackbar( + message = rebootToApply, + actionLabel = reboot, + duration = SnackbarDuration.Long + ) + if (result == SnackbarResult.ActionPerformed) { + reboot() + } + } else { + val message = if (module.enabled) failedDisable else failedEnable + snackBarHost.showSnackbar(message.format(module.name)) } } - if (success) { - isChecked = it - viewModel.fetchModuleList() - - val result = snackBarHost.showSnackbar( - rebootToApply, actionLabel = reboot + }, + onUpdate = { + scope.launch { + onModuleUpdate( + module, + updatedModule.third, + updatedModule.first, + "${module.name}-${updatedModule.second}.zip" ) - if (result == SnackbarResult.ActionPerformed) { - reboot() - } - } else { - val message = if (isChecked) failedDisable else failedEnable - snackBarHost.showSnackbar(message.format(module.name)) } + }, + onClick = { + onClickModule(it.id, it.name, it.hasWebUi) } - }, onUpdate = { - scope.launch { - onModuleUpdate( - module, - updatedModule.third, - updatedModule.first, - "${module.name}-${updatedModule.second}.zip" - ) - } - }) + ) // fix last item shadow incomplete in LazyColumn Spacer(Modifier.height(1.dp)) @@ -344,37 +521,45 @@ private fun ModuleList( DownloadListener(context, onInstallModule) - PullRefreshIndicator( - refreshing = viewModel.isRefreshing, state = refreshState, modifier = Modifier.align( - Alignment.TopCenter - ) - ) } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar() { - TopAppBar(title = { Text(stringResource(R.string.module)) }) -} - @Composable -private fun ModuleItem( +fun ModuleItem( + navigator: DestinationsNavigator, module: ModuleViewModel.ModuleInfo, - isChecked: Boolean, updateUrl: String, onUninstall: (ModuleViewModel.ModuleInfo) -> Unit, onCheckChanged: (Boolean) -> Unit, onUpdate: (ModuleViewModel.ModuleInfo) -> Unit, + onClick: (ModuleViewModel.ModuleInfo) -> Unit ) { ElevatedCard( - modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.surface) + modifier = Modifier.fillMaxWidth() ) { - val textDecoration = if (!module.remove) null else TextDecoration.LineThrough - - Column(modifier = Modifier.padding(24.dp, 16.dp, 24.dp, 0.dp)) { + val interactionSource = remember { MutableInteractionSource() } + val indication = LocalIndication.current + val viewModel = viewModel() + + Column( + modifier = Modifier + .run { + if (module.hasWebUi) { + toggleable( + value = module.enabled, + enabled = !module.remove && module.enabled, + interactionSource = interactionSource, + role = Role.Button, + indication = indication, + onValueChange = { onClick(module) } + ) + } else { + this + } + } + .padding(22.dp, 18.dp, 22.dp, 12.dp) + ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -382,7 +567,9 @@ private fun ModuleItem( val moduleVersion = stringResource(id = R.string.module_version) val moduleAuthor = stringResource(id = R.string.module_author) - Column(modifier = Modifier.fillMaxWidth(0.8f)) { + Column( + modifier = Modifier.fillMaxWidth(0.8f) + ) { Text( text = module.name, fontSize = MaterialTheme.typography.titleMedium.fontSize, @@ -417,8 +604,9 @@ private fun ModuleItem( ) { Switch( enabled = !module.update, - checked = isChecked, - onCheckedChange = onCheckChanged + checked = module.enabled, + onCheckedChange = onCheckChanged, + interactionSource = if (!module.hasWebUi) interactionSource else null ) } } @@ -436,43 +624,116 @@ private fun ModuleItem( textDecoration = textDecoration ) - Spacer(modifier = Modifier.height(16.dp)) - Divider(thickness = Dp.Hairline) + HorizontalDivider(thickness = Dp.Hairline) + + Spacer(modifier = Modifier.height(4.dp)) Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { + + if (module.hasActionScript) { + FilledTonalButton( + modifier = Modifier.defaultMinSize(52.dp, 32.dp), + enabled = !module.remove && module.enabled, + onClick = { + navigator.navigate(ExecuteModuleActionScreenDestination(module.id)) + viewModel.markNeedRefresh() + }, + contentPadding = ButtonDefaults.TextButtonContentPadding + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = Icons.Outlined.PlayArrow, + contentDescription = null + ) + if (!module.hasWebUi && updateUrl.isEmpty()) { + Text( + modifier = Modifier.padding(start = 7.dp), + text = stringResource(R.string.action), + fontFamily = MaterialTheme.typography.labelMedium.fontFamily, + fontSize = MaterialTheme.typography.labelMedium.fontSize + ) + } + } + + Spacer(modifier = Modifier.weight(0.1f, true)) + } + + if (module.hasWebUi) { + FilledTonalButton( + modifier = Modifier.defaultMinSize(52.dp, 32.dp), + enabled = !module.remove && module.enabled, + onClick = { onClick(module) }, + interactionSource = interactionSource, + contentPadding = ButtonDefaults.TextButtonContentPadding + ) { + Icon( + modifier = Modifier.size(20.dp), + imageVector = Icons.AutoMirrored.Outlined.Wysiwyg, + contentDescription = null + ) + if (!module.hasActionScript && updateUrl.isEmpty()) { + Text( + modifier = Modifier.padding(start = 7.dp), + fontFamily = MaterialTheme.typography.labelMedium.fontFamily, + fontSize = MaterialTheme.typography.labelMedium.fontSize, + text = stringResource(R.string.open) + ) + } + } + } + Spacer(modifier = Modifier.weight(1f, true)) if (updateUrl.isNotEmpty()) { Button( - modifier = Modifier - .padding(0.dp) - .defaultMinSize(52.dp, 32.dp), + modifier = Modifier.defaultMinSize(52.dp, 32.dp), + enabled = !module.remove, onClick = { onUpdate(module) }, - shape = RoundedCornerShape(6.dp), - contentPadding = PaddingValues(0.dp) + shape = ButtonDefaults.textShape, + contentPadding = ButtonDefaults.TextButtonContentPadding ) { - Text( - fontFamily = MaterialTheme.typography.labelMedium.fontFamily, - fontSize = MaterialTheme.typography.labelMedium.fontSize, - text = stringResource(R.string.module_update), + Icon( + modifier = Modifier.size(20.dp), + imageVector = Icons.Outlined.Download, + contentDescription = null ) + if (!module.hasActionScript || !module.hasWebUi) { + Text( + modifier = Modifier.padding(start = 7.dp), + fontFamily = MaterialTheme.typography.labelMedium.fontFamily, + fontSize = MaterialTheme.typography.labelMedium.fontSize, + text = stringResource(R.string.module_update) + ) + } } + + Spacer(modifier = Modifier.weight(0.1f, true)) } - TextButton( + FilledTonalButton( + modifier = Modifier.defaultMinSize(52.dp, 32.dp), enabled = !module.remove, onClick = { onUninstall(module) }, + contentPadding = ButtonDefaults.TextButtonContentPadding ) { - Text( - fontFamily = MaterialTheme.typography.labelMedium.fontFamily, - fontSize = MaterialTheme.typography.labelMedium.fontSize, - text = stringResource(R.string.uninstall), + Icon( + modifier = Modifier.size(20.dp), + imageVector = Icons.Outlined.Delete, + contentDescription = null ) + if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) { + Text( + modifier = Modifier.padding(start = 7.dp), + fontFamily = MaterialTheme.typography.labelMedium.fontFamily, + fontSize = MaterialTheme.typography.labelMedium.fontSize, + text = stringResource(R.string.uninstall) + ) + } } } } @@ -491,8 +752,10 @@ fun ModuleItemPreview() { description = "I am a test module and i do nothing but show a very long description", enabled = true, update = true, - remove = true, - updateJson = "" + remove = false, + updateJson = "", + hasWebUi = false, + hasActionScript = false ) - ModuleItem(module, true, "", {}, {}, {}) -} \ No newline at end of file + ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {}) +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt index 03216b5ef0c8..fe6b8786f335 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Settings.kt @@ -1,23 +1,78 @@ package me.weishu.kernelsu.ui.screen +import android.content.Context import android.content.Intent import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Undo import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Compress import androidx.compose.material.icons.filled.ContactPage +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material.icons.filled.DeveloperMode +import androidx.compose.material.icons.filled.Fence import androidx.compose.material.icons.filled.RemoveModerator -import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.Update +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.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.style.LineHeightStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider +import com.maxkeppeker.sheets.core.models.base.Header +import com.maxkeppeker.sheets.core.models.base.IconSource +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.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.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -25,36 +80,82 @@ import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.AboutDialog -import me.weishu.kernelsu.ui.component.LoadingDialog +import me.weishu.kernelsu.ui.component.ConfirmResult +import me.weishu.kernelsu.ui.component.DialogHandle import me.weishu.kernelsu.ui.component.SwitchItem -import me.weishu.kernelsu.ui.util.LocalDialogHost +import me.weishu.kernelsu.ui.component.rememberConfirmDialog +import me.weishu.kernelsu.ui.component.rememberCustomDialog +import me.weishu.kernelsu.ui.component.rememberLoadingDialog +import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.getBugreportFile +import me.weishu.kernelsu.ui.util.shrinkModules +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter /** * @author weishu * @date 2023/1/1. */ -@Destination +@OptIn(ExperimentalMaterial3Api::class) +@Destination @Composable fun SettingScreen(navigator: DestinationsNavigator) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val snackBarHost = LocalSnackbarHost.current Scaffold( topBar = { - TopBar(onBack = { - navigator.popBackStack() - }) - } + TopBar( + onBack = { + navigator.popBackStack() + }, + scrollBehavior = scrollBehavior + ) + }, + snackbarHost = { SnackbarHost(snackBarHost) }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) ) { paddingValues -> - LoadingDialog() - - val showAboutDialog = remember { mutableStateOf(false) } - AboutDialog(showAboutDialog) + val aboutDialog = rememberCustomDialog { + AboutDialog(it) + } + val loadingDialog = rememberLoadingDialog() + val shrinkDialog = rememberConfirmDialog() - Column(modifier = Modifier.padding(paddingValues)) { + Column( + modifier = Modifier + .padding(paddingValues) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .verticalScroll(rememberScrollState()) + ) { val context = LocalContext.current val scope = rememberCoroutineScope() - val dialogHost = LocalDialogHost.current + + val exportBugreportLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.CreateDocument("application/gzip") + ) { uri: Uri? -> + if (uri == null) return@rememberLauncherForActivityResult + scope.launch(Dispatchers.IO) { + loadingDialog.show() + context.contentResolver.openOutputStream(uri)?.use { output -> + getBugreportFile(context).inputStream().use { + it.copyTo(output) + } + } + loadingDialog.hide() + snackBarHost.showSnackbar(context.getString(R.string.log_saved)) + } + } + + val profileTemplate = stringResource(id = R.string.settings_profile_template) + ListItem( + leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) }, + headlineContent = { Text(profileTemplate) }, + supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) }, + modifier = Modifier.clickable { + navigator.navigate(AppProfileTemplateScreenDestination) + } + ) var umountChecked by rememberSaveable { mutableStateOf(Natives.isDefaultUmountModules()) @@ -70,60 +171,316 @@ fun SettingScreen(navigator: DestinationsNavigator) { } } + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + var checkUpdate by rememberSaveable { + mutableStateOf( + prefs.getBoolean("check_update", true) + ) + } + SwitchItem( + icon = Icons.Filled.Update, + title = stringResource(id = R.string.settings_check_update), + summary = stringResource(id = R.string.settings_check_update_summary), + checked = checkUpdate + ) { + prefs.edit().putBoolean("check_update", it).apply() + checkUpdate = it + } + + var enableWebDebugging by rememberSaveable { + mutableStateOf( + prefs.getBoolean("enable_web_debugging", false) + ) + } + SwitchItem( + icon = Icons.Filled.DeveloperMode, + title = stringResource(id = R.string.enable_web_debugging), + summary = stringResource(id = R.string.enable_web_debugging_summary), + checked = enableWebDebugging + ) { + prefs.edit().putBoolean("enable_web_debugging", it).apply() + enableWebDebugging = it + } + + var showBottomsheet by remember { mutableStateOf(false) } + ListItem( - leadingContent = { Icon(Icons.Filled.BugReport, stringResource(id = R.string.send_log)) }, + leadingContent = { + Icon( + Icons.Filled.BugReport, + stringResource(id = R.string.send_log) + ) + }, headlineContent = { Text(stringResource(id = R.string.send_log)) }, modifier = Modifier.clickable { - scope.launch { - val bugreport = dialogHost.withLoading { - withContext(Dispatchers.IO) { - getBugreportFile(context) + showBottomsheet = true + } + ) + if (showBottomsheet) { + ModalBottomSheet( + onDismissRequest = { showBottomsheet = false }, + content = { + Row( + modifier = Modifier + .padding(10.dp) + .align(Alignment.CenterHorizontally) + + ) { + Box { + Column( + modifier = Modifier + .padding(16.dp) + .clickable { + val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") + val current = LocalDateTime.now().format(formatter) + exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz") + showBottomsheet = false + } + ) { + Icon( + Icons.Filled.Save, + contentDescription = null, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Text( + text = stringResource(id = R.string.save_log), + modifier = Modifier.padding(top = 16.dp), + textAlign = TextAlign.Center.also { + LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ) + } + + ) + } + } + Box { + Column( + modifier = Modifier + .padding(16.dp) + .clickable { + scope.launch { + val bugreport = loadingDialog.withLoading { + withContext(Dispatchers.IO) { + getBugreportFile(context) + } + } + + val uri: Uri = + FileProvider.getUriForFile( + context, + "${BuildConfig.APPLICATION_ID}.fileprovider", + bugreport + ) + + val shareIntent = Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, uri) + setDataAndType(uri, "application/gzip") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + context.startActivity( + Intent.createChooser( + shareIntent, + context.getString(R.string.send_log) + ) + ) + } + } + ) { + Icon( + Icons.Filled.Share, + contentDescription = null, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Text( + text = stringResource(id = R.string.send_log), + modifier = Modifier.padding(top = 16.dp), + textAlign = TextAlign.Center.also { + LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None + ) + } + ) + } } } + } + ) + } - val uri: Uri = - FileProvider.getUriForFile( - context, - "${BuildConfig.APPLICATION_ID}.fileprovider", - bugreport - ) - - val shareIntent = Intent(Intent.ACTION_SEND) - shareIntent.putExtra(Intent.EXTRA_STREAM, uri) - shareIntent.setDataAndType(uri, "application/zip") - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - - context.startActivity( - Intent.createChooser( - shareIntent, - context.getString(R.string.send_log) - ) - ) + val shrink = stringResource(id = R.string.shrink_sparse_image) + val shrinkMessage = stringResource(id = R.string.shrink_sparse_image_message) + ListItem( + leadingContent = { + Icon( + Icons.Filled.Compress, + shrink + ) + }, + headlineContent = { Text(shrink) }, + modifier = Modifier.clickable { + scope.launch { + val result = shrinkDialog.awaitConfirm(title = shrink, content = shrinkMessage) + if (result == ConfirmResult.Confirmed) { + loadingDialog.withLoading { + shrinkModules() + } + } } } ) + val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode + if (lkmMode) { + UninstallItem(navigator) { + loadingDialog.withLoading(it) + } + } + val about = stringResource(id = R.string.about) ListItem( - leadingContent = { Icon(Icons.Filled.ContactPage, stringResource(id = R.string.about)) }, + leadingContent = { + Icon( + Icons.Filled.ContactPage, + about + ) + }, headlineContent = { Text(about) }, modifier = Modifier.clickable { - showAboutDialog.value = true + aboutDialog.show() } ) } } } +@Composable +fun UninstallItem( + navigator: DestinationsNavigator, + withLoading: suspend (suspend () -> Unit) -> Unit +) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + val uninstallConfirmDialog = rememberConfirmDialog() + val showTodo = { + Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show() + } + val uninstallDialog = rememberUninstallDialog { uninstallType -> + scope.launch { + val result = uninstallConfirmDialog.awaitConfirm( + title = context.getString(uninstallType.title), + content = context.getString(uninstallType.message) + ) + if (result == ConfirmResult.Confirmed) { + withLoading { + when (uninstallType) { + UninstallType.TEMPORARY -> showTodo() + UninstallType.PERMANENT -> navigator.navigate( + FlashScreenDestination(FlashIt.FlashUninstall) + ) + UninstallType.RESTORE_STOCK_IMAGE -> navigator.navigate( + FlashScreenDestination(FlashIt.FlashRestore) + ) + UninstallType.NONE -> Unit + } + } + } + } + } + val uninstall = stringResource(id = R.string.settings_uninstall) + ListItem( + leadingContent = { + Icon( + Icons.Filled.Delete, + uninstall + ) + }, + headlineContent = { Text(uninstall) }, + modifier = Modifier.clickable { + uninstallDialog.show() + } + ) +} + +enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector) { + TEMPORARY( + R.string.settings_uninstall_temporary, + R.string.settings_uninstall_temporary_message, + Icons.Filled.Delete + ), + PERMANENT( + R.string.settings_uninstall_permanent, + R.string.settings_uninstall_permanent_message, + Icons.Filled.DeleteForever + ), + RESTORE_STOCK_IMAGE( + R.string.settings_restore_stock_image, + R.string.settings_restore_stock_image_message, + Icons.AutoMirrored.Filled.Undo + ), + NONE(0, 0, Icons.Filled.Delete) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun TopBar(onBack: () -> Unit = {}) { +fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle { + return rememberCustomDialog { dismiss -> + val options = listOf( + // UninstallType.TEMPORARY, + UninstallType.PERMANENT, + UninstallType.RESTORE_STOCK_IMAGE + ) + val listOptions = options.map { + ListOption( + titleText = stringResource(it.title), + subtitleText = if (it.message != 0) stringResource(it.message) else null, + icon = IconSource(it.icon) + ) + } + + var selection = UninstallType.NONE + ListDialog(state = rememberUseCaseState(visible = true, onFinishedRequest = { + if (selection != UninstallType.NONE) { + onSelected(selection) + } + }, onCloseRequest = { + dismiss() + }), header = Header.Default( + title = stringResource(R.string.settings_uninstall), + ), selection = ListSelection.Single( + showRadioButtons = false, + options = listOptions, + ) { index, _ -> + selection = options[index] + }) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + onBack: () -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null +) { TopAppBar( title = { Text(stringResource(R.string.settings)) }, navigationIcon = { IconButton( onClick = onBack - ) { Icon(Icons.Filled.ArrowBack, contentDescription = null) } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior ) } + +@Preview +@Composable +private fun SettingsPreview() { + SettingScreen(EmptyDestinationsNavigator) +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt index 643c4741fe2f..a2510a7fff94 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/SuperUser.kt @@ -4,19 +4,18 @@ 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.rememberLazyListState 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.filled.MoreVert -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState import androidx.compose.material3.* +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* -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.TextStyle @@ -26,28 +25,37 @@ import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import coil.request.ImageRequest import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.generated.destinations.AppProfileScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import kotlinx.coroutines.launch import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R -import me.weishu.kernelsu.ui.component.ConfirmDialog import me.weishu.kernelsu.ui.component.SearchAppBar -import me.weishu.kernelsu.ui.screen.destinations.AppProfileScreenDestination import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel -@OptIn(ExperimentalMaterialApi::class) -@Destination +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@Destination @Composable fun SuperUserScreen(navigator: DestinationsNavigator) { val viewModel = viewModel() val scope = rememberCoroutineScope() + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val listState = rememberLazyListState() - LaunchedEffect(Unit) { + LaunchedEffect(key1 = navigator) { + viewModel.search = "" if (viewModel.appList.isEmpty()) { viewModel.fetchAppList() } } + LaunchedEffect(viewModel.search) { + if (viewModel.search.isEmpty()) { + listState.scrollToItem(0) + } + } + Scaffold( topBar = { SearchAppBar( @@ -92,35 +100,30 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { } } }, + scrollBehavior = scrollBehavior ) - } + }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) ) { innerPadding -> - - ConfirmDialog() - - val refreshState = rememberPullRefreshState( - refreshing = viewModel.isRefreshing, - onRefresh = { scope.launch { viewModel.fetchAppList() } }, - ) - Box( - modifier = Modifier - .padding(innerPadding) - .pullRefresh(refreshState) + PullToRefreshBox( + modifier = Modifier.padding(innerPadding), + onRefresh = { + scope.launch { viewModel.fetchAppList() } + }, + isRefreshing = viewModel.isRefreshing ) { - LazyColumn(Modifier.fillMaxSize()) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { items(viewModel.appList, key = { it.packageName + it.uid }) { app -> AppItem(app) { navigator.navigate(AppProfileScreenDestination(app)) } - } } - - PullRefreshIndicator( - refreshing = viewModel.isRefreshing, - state = refreshState, - modifier = Modifier.align(Alignment.TopCenter) - ) } } } diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Template.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Template.kt new file mode 100644 index 000000000000..3074f4e19e0c --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/Template.kt @@ -0,0 +1,268 @@ +package me.weishu.kernelsu.ui.screen + +import android.widget.Toast +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ImportExport +import androidx.compose.material.icons.filled.Sync +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.generated.destinations.TemplateEditorScreenDestination +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.ramcosta.composedestinations.result.ResultRecipient +import com.ramcosta.composedestinations.result.getOr +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel + +/** + * @author weishu + * @date 2023/10/20. + */ + +@OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) +@Destination +@Composable +fun AppProfileTemplateScreen( + navigator: DestinationsNavigator, + resultRecipient: ResultRecipient +) { + val viewModel = viewModel() + val scope = rememberCoroutineScope() + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + LaunchedEffect(Unit) { + if (viewModel.templateList.isEmpty()) { + viewModel.fetchTemplates() + } + } + + // handle result from TemplateEditorScreen, refresh if needed + resultRecipient.onNavResult { result -> + if (result.getOr { false }) { + scope.launch { viewModel.fetchTemplates() } + } + } + + Scaffold( + topBar = { + val clipboardManager = LocalClipboardManager.current + val context = LocalContext.current + val showToast = fun(msg: String) { + scope.launch(Dispatchers.Main) { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() + } + } + TopBar( + onBack = { navigator.popBackStack() }, + onSync = { + scope.launch { viewModel.fetchTemplates(true) } + }, + onImport = { + clipboardManager.getText()?.text?.let { + if (it.isEmpty()) { + showToast(context.getString(R.string.app_profile_template_import_empty)) + return@let + } + scope.launch { + viewModel.importTemplates( + it, { + showToast(context.getString(R.string.app_profile_template_import_success)) + viewModel.fetchTemplates(false) + }, + showToast + ) + } + } + }, + onExport = { + scope.launch { + viewModel.exportTemplates( + { + showToast(context.getString(R.string.app_profile_template_export_empty)) + } + ) { + clipboardManager.setText(AnnotatedString(it)) + } + } + }, + scrollBehavior = scrollBehavior + ) + }, + floatingActionButton = { + ExtendedFloatingActionButton( + onClick = { + navigator.navigate( + TemplateEditorScreenDestination( + TemplateViewModel.TemplateInfo(), + false + ) + ) + }, + icon = { Icon(Icons.Filled.Add, null) }, + text = { Text(stringResource(id = R.string.app_profile_template_create)) }, + ) + }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) { innerPadding -> + PullToRefreshBox( + modifier = Modifier.padding(innerPadding), + isRefreshing = viewModel.isRefreshing, + onRefresh = { + scope.launch { viewModel.fetchTemplates() } + } + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection), + contentPadding = remember { + PaddingValues(bottom = 16.dp + 56.dp + 16.dp /* Scaffold Fab Spacing + Fab container height */) + } + ) { + items(viewModel.templateList, key = { it.id }) { app -> + TemplateItem(navigator, app) + } + } + } + } +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun TemplateItem( + navigator: DestinationsNavigator, + template: TemplateViewModel.TemplateInfo +) { + ListItem( + modifier = Modifier + .clickable { + navigator.navigate(TemplateEditorScreenDestination(template, !template.local)) + }, + headlineContent = { Text(template.name) }, + supportingContent = { + Column { + Text( + text = "${template.id}${if (template.author.isEmpty()) "" else "@${template.author}"}", + style = MaterialTheme.typography.bodySmall, + fontSize = MaterialTheme.typography.bodySmall.fontSize, + ) + Text(template.description) + FlowRow { + LabelText(label = "UID: ${template.uid}") + LabelText(label = "GID: ${template.gid}") + LabelText(label = template.context) + if (template.local) { + LabelText(label = "local") + } else { + LabelText(label = "remote") + } + } + } + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + onBack: () -> Unit, + onSync: () -> Unit = {}, + onImport: () -> Unit = {}, + onExport: () -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null +) { + TopAppBar( + title = { + Text(stringResource(R.string.settings_profile_template)) + }, + navigationIcon = { + IconButton( + onClick = onBack + ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + }, + actions = { + IconButton(onClick = onSync) { + Icon( + Icons.Filled.Sync, + contentDescription = stringResource(id = R.string.app_profile_template_sync) + ) + } + + var showDropdown by remember { mutableStateOf(false) } + IconButton(onClick = { + showDropdown = true + }) { + Icon( + imageVector = Icons.Filled.ImportExport, + contentDescription = stringResource(id = R.string.app_profile_import_export) + ) + + DropdownMenu(expanded = showDropdown, onDismissRequest = { + showDropdown = false + }) { + DropdownMenuItem(text = { + Text(stringResource(id = R.string.app_profile_import_from_clipboard)) + }, onClick = { + onImport() + showDropdown = false + }) + DropdownMenuItem(text = { + Text(stringResource(id = R.string.app_profile_export_to_clipboard)) + }, onClick = { + onExport() + showDropdown = false + }) + } + } + }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior + ) +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/TemplateEditor.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/TemplateEditor.kt new file mode 100644 index 000000000000..030320ba329a --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/screen/TemplateEditor.kt @@ -0,0 +1,339 @@ +package me.weishu.kernelsu.ui.screen + +import android.widget.Toast +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material.icons.filled.Save +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.result.ResultBackNavigator +import me.weishu.kernelsu.Natives +import me.weishu.kernelsu.R +import me.weishu.kernelsu.ui.component.profile.RootProfileConfig +import me.weishu.kernelsu.ui.util.deleteAppProfileTemplate +import me.weishu.kernelsu.ui.util.getAppProfileTemplate +import me.weishu.kernelsu.ui.util.setAppProfileTemplate +import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel +import me.weishu.kernelsu.ui.viewmodel.toJSON + +/** + * @author weishu + * @date 2023/10/20. + */ +@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) +@Destination +@Composable +fun TemplateEditorScreen( + navigator: ResultBackNavigator, + initialTemplate: TemplateViewModel.TemplateInfo, + readOnly: Boolean = true, +) { + + val isCreation = initialTemplate.id.isBlank() + val autoSave = !isCreation + + var template by rememberSaveable { + mutableStateOf(initialTemplate) + } + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + + BackHandler { + navigator.navigateBack(result = !readOnly) + } + + Scaffold( + topBar = { + val author = + if (initialTemplate.author.isNotEmpty()) "@${initialTemplate.author}" else "" + val readOnlyHint = if (readOnly) { + " - ${stringResource(id = R.string.app_profile_template_readonly)}" + } else { + "" + } + val titleSummary = "${initialTemplate.id}$author$readOnlyHint" + val saveTemplateFailed = stringResource(id = R.string.app_profile_template_save_failed) + val context = LocalContext.current + + TopBar( + title = if (isCreation) { + stringResource(R.string.app_profile_template_create) + } else if (readOnly) { + stringResource(R.string.app_profile_template_view) + } else { + stringResource(R.string.app_profile_template_edit) + }, + readOnly = readOnly, + summary = titleSummary, + onBack = { navigator.navigateBack(result = !readOnly) }, + onDelete = { + if (deleteAppProfileTemplate(template.id)) { + navigator.navigateBack(result = true) + } + }, + onSave = { + if (saveTemplate(template, isCreation)) { + navigator.navigateBack(result = true) + } else { + Toast.makeText(context, saveTemplateFailed, Toast.LENGTH_SHORT).show() + } + }, + scrollBehavior = scrollBehavior + ) + }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) { innerPadding -> + Column( + modifier = Modifier + .padding(innerPadding) + .nestedScroll(scrollBehavior.nestedScrollConnection) + .verticalScroll(rememberScrollState()) + .pointerInteropFilter { + // disable click and ripple if readOnly + readOnly + } + ) { + if (isCreation) { + var errorHint by remember { + mutableStateOf("") + } + val idConflictError = stringResource(id = R.string.app_profile_template_id_exist) + val idInvalidError = stringResource(id = R.string.app_profile_template_id_invalid) + TextEdit( + label = stringResource(id = R.string.app_profile_template_id), + text = template.id, + errorHint = errorHint, + isError = errorHint.isNotEmpty() + ) { value -> + errorHint = if (isTemplateExist(value)) { + idConflictError + } else if (!isValidTemplateId(value)) { + idInvalidError + } else { + "" + } + template = template.copy(id = value) + } + } + + TextEdit( + label = stringResource(id = R.string.app_profile_template_name), + text = template.name + ) { value -> + template.copy(name = value).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } + } + template = this + } + } + TextEdit( + label = stringResource(id = R.string.app_profile_template_description), + text = template.description + ) { value -> + template.copy(description = value).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } + } + template = this + } + } + + RootProfileConfig(fixedName = true, + profile = toNativeProfile(template), + onProfileChange = { + template.copy( + uid = it.uid, + gid = it.gid, + groups = it.groups, + capabilities = it.capabilities, + context = it.context, + namespace = it.namespace, + rules = it.rules.split("\n") + ).run { + if (autoSave) { + if (!saveTemplate(this)) { + // failed + return@run + } + } + template = this + } + }) + } + } +} + +fun toNativeProfile(templateInfo: TemplateViewModel.TemplateInfo): Natives.Profile { + return Natives.Profile().copy(rootTemplate = templateInfo.id, + uid = templateInfo.uid, + gid = templateInfo.gid, + groups = templateInfo.groups, + capabilities = templateInfo.capabilities, + context = templateInfo.context, + namespace = templateInfo.namespace, + rules = templateInfo.rules.joinToString("\n").ifBlank { "" }) +} + +fun isTemplateValid(template: TemplateViewModel.TemplateInfo): Boolean { + if (template.id.isBlank()) { + return false + } + + if (!isValidTemplateId(template.id)) { + return false + } + + return true +} + +fun saveTemplate(template: TemplateViewModel.TemplateInfo, isCreation: Boolean = false): Boolean { + if (!isTemplateValid(template)) { + return false + } + + if (isCreation && isTemplateExist(template.id)) { + return false + } + + val json = template.toJSON() + json.put("local", true) + return setAppProfileTemplate(template.id, json.toString()) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + title: String, + readOnly: Boolean, + summary: String = "", + onBack: () -> Unit, + onDelete: () -> Unit = {}, + onSave: () -> Unit = {}, + scrollBehavior: TopAppBarScrollBehavior? = null +) { + TopAppBar( + title = { + Column { + Text(title) + if (summary.isNotBlank()) { + Text( + text = summary, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + }, navigationIcon = { + IconButton( + onClick = onBack + ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } + }, actions = { + if (readOnly) { + return@TopAppBar + } + IconButton(onClick = onDelete) { + Icon( + Icons.Filled.DeleteForever, + contentDescription = stringResource(id = R.string.app_profile_template_delete) + ) + } + IconButton(onClick = onSave) { + Icon( + imageVector = Icons.Filled.Save, + contentDescription = stringResource(id = R.string.app_profile_template_save) + ) + } + }, + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior + ) +} + +@Composable +private fun TextEdit( + label: String, + text: String, + errorHint: String = "", + isError: Boolean = false, + onValueChange: (String) -> Unit = {} +) { + ListItem(headlineContent = { + val keyboardController = LocalSoftwareKeyboardController.current + OutlinedTextField( + value = text, + modifier = Modifier.fillMaxWidth(), + label = { Text(label) }, + suffix = { + if (errorHint.isNotBlank()) { + Text( + text = if (isError) errorHint else "", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + } + }, + isError = isError, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions(onDone = { + keyboardController?.hide() + }), + onValueChange = onValueChange + ) + }) +} + +private fun isValidTemplateId(id: String): Boolean { + return Regex("""^([A-Za-z][A-Za-z\d_]*\.)*[A-Za-z][A-Za-z\d_]*$""").matches(id) +} + +private fun isTemplateExist(id: String): Boolean { + return getAppProfileTemplate(id).isNotBlank() +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Theme.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Theme.kt index 040b2bf86971..903ee94e0b54 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Theme.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Theme.kt @@ -1,18 +1,14 @@ package me.weishu.kernelsu.ui.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.unit.dp -import androidx.core.view.ViewCompat -import com.google.accompanist.systemuicontroller.rememberSystemUiController private val DarkColorScheme = darkColorScheme( primary = YELLOW, @@ -42,20 +38,6 @@ fun KernelSUTheme( else -> LightColorScheme } - val systemUiController = rememberSystemUiController() - SideEffect { - systemUiController.setStatusBarColor( - color = colorScheme.surface, - darkIcons = !darkTheme - ) - - // To match the App Navbar color - systemUiController.setNavigationBarColor( - color = colorScheme.surfaceColorAtElevation(8.dp), - darkIcons = !darkTheme, - ) - } - MaterialTheme( colorScheme = colorScheme, typography = Typography, diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt index 63b4b520f4d2..c1b57483a02f 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt @@ -2,12 +2,7 @@ package me.weishu.kernelsu.ui.util import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.compositionLocalOf -import me.weishu.kernelsu.ui.component.DialogHostState val LocalSnackbarHost = compositionLocalOf { error("CompositionLocal LocalSnackbarController not present") -} - -val LocalDialogHost = compositionLocalOf { - error("CompositionLocal LocalDialogController not present") } \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/Downloader.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/Downloader.kt index cfe98fae5827..bdf8496633fd 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/Downloader.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/Downloader.kt @@ -10,6 +10,8 @@ import android.net.Uri import android.os.Environment import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.core.content.ContextCompat +import me.weishu.kernelsu.ui.util.module.LatestVersionInfo /** * @author weishu @@ -24,8 +26,7 @@ fun download( onDownloaded: (Uri) -> Unit = {}, onDownloading: () -> Unit = {} ) { - val downloadManager = - context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager val query = DownloadManager.Query() query.setFilterByStatus(DownloadManager.STATUS_RUNNING or DownloadManager.STATUS_PAUSED or DownloadManager.STATUS_PENDING) @@ -60,9 +61,10 @@ fun download( downloadManager.enqueue(request) } -fun checkNewVersion(): Triple { +fun checkNewVersion(): LatestVersionInfo { val url = "https://api.github.com/repos/tiann/KernelSU/releases/latest" - val defaultValue = Triple(0, "", "") + // default null value if failed + val defaultValue = LatestVersionInfo() runCatching { okhttp3.OkHttpClient().newCall(okhttp3.Request.Builder().url(url).build()).execute() .use { response -> @@ -87,7 +89,11 @@ fun checkNewVersion(): Triple { val versionCode = matchResult.groupValues[2].toInt() val downloadUrl = asset.getString("browser_download_url") - return Triple(versionCode, downloadUrl, changelog) + return LatestVersionInfo( + versionCode, + downloadUrl, + changelog + ) } } @@ -123,12 +129,14 @@ fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) { } } } - context.registerReceiver( + ContextCompat.registerReceiver( + context, receiver, - IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) + IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), + ContextCompat.RECEIVER_EXPORTED ) onDispose { context.unregisterReceiver(receiver) } } -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/HanziToPinyin.java b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/HanziToPinyin.java index ab905a388520..d3d57cefe2ea 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/HanziToPinyin.java +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/HanziToPinyin.java @@ -391,9 +391,9 @@ public static HanziToPinyin getInstance() { return sInstance; } // Check if zh_CN collation data is available - final Locale locale[] = Collator.getAvailableLocales(); - for (int i = 0; i < locale.length; i++) { - if (locale[i].equals(Locale.CHINA) || locale[i].getLanguage().contains("zh")) { + 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()); @@ -508,7 +508,7 @@ private Token getToken(char character) { * Token. If these is no China collator, the empty token array is returned. */ public ArrayList get(final String input) { - ArrayList tokens = new ArrayList(); + ArrayList tokens = new ArrayList<>(); if (!mHasChinaCollator || TextUtils.isEmpty(input)) { // return empty tokens. return tokens; diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt index 39fbb28d79b4..1a2416b280d2 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt @@ -1,11 +1,21 @@ package me.weishu.kernelsu.ui.util +import android.content.ContentResolver +import android.content.Context +import android.database.Cursor import android.net.Uri +import android.os.Environment +import android.os.Parcelable import android.os.SystemClock +import android.provider.OpenableColumns +import android.system.Os import android.util.Log import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ShellUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp @@ -25,31 +35,72 @@ private fun getKsuDaemonPath(): String { object KsuCli { val SHELL: Shell = createRootShell() + val GLOBAL_MNT_SHELL: Shell = createRootShell(true) } -fun getRootShell(): Shell { - return KsuCli.SHELL +fun getRootShell(globalMnt: Boolean = false): Shell { + return if (globalMnt) KsuCli.GLOBAL_MNT_SHELL else { + KsuCli.SHELL + } +} + +inline fun withNewRootShell( + globalMnt: Boolean = false, + block: Shell.() -> T +): T { + return createRootShell(globalMnt).use(block) +} + +fun Uri.getFileName(context: Context): String? { + var fileName: String? = null + val contentResolver: ContentResolver = context.contentResolver + val cursor: Cursor? = contentResolver.query(this, null, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + fileName = it.getString(it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) + } + } + return fileName } -fun createRootShell(): Shell { +fun createRootShell(globalMnt: Boolean = false): Shell { Shell.enableVerboseLogging = BuildConfig.DEBUG val builder = Shell.Builder.create() return try { - builder.build(getKsuDaemonPath(), "debug", "su") + if (globalMnt) { + builder.build(getKsuDaemonPath(), "debug", "su", "-g") + } else { + builder.build(getKsuDaemonPath(), "debug", "su") + } } catch (e: Throwable) { - Log.e(TAG, "su failed: ", e) - builder.build("sh") + Log.w(TAG, "ksu failed: ", e) + try { + if (globalMnt) { + builder.build("su") + } else { + builder.build("su", "-mm") + } + } catch (e: Throwable) { + Log.e(TAG, "su failed: ", e) + builder.build("sh") + } } } -fun execKsud(args: String): Boolean { - val shell = getRootShell() - return ShellUtils.fastCmdResult(shell, "${getKsuDaemonPath()} $args") +fun execKsud(args: String, newShell: Boolean = false): Boolean { + return if (newShell) { + withNewRootShell { + ShellUtils.fastCmdResult(this, "${getKsuDaemonPath()} $args") + } + } else { + ShellUtils.fastCmdResult(getRootShell(), "${getKsuDaemonPath()} $args") + } } fun install() { val start = SystemClock.elapsedRealtime() - val result = execKsud("install") + 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") } @@ -79,19 +130,47 @@ fun toggleModule(id: String, enable: Boolean): Boolean { } else { "module disable $id" } - val result = execKsud(cmd) + val result = execKsud(cmd, true) Log.i(TAG, "$cmd result: $result") return result } fun uninstallModule(id: String): Boolean { val cmd = "module uninstall $id" - val result = execKsud(cmd) + val result = execKsud(cmd, true) Log.i(TAG, "uninstall module $id result: $result") return result } -fun installModule(uri: Uri, onFinish: (Boolean) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit): Boolean { +private fun flashWithIO( + cmd: String, + onStdout: (String) -> Unit, + onStderr: (String) -> Unit +): Shell.Result { + + val stdoutCallback: CallbackList = object : CallbackList() { + override fun onAddElement(s: String?) { + onStdout(s ?: "") + } + } + + val stderrCallback: CallbackList = object : CallbackList() { + override fun onAddElement(s: String?) { + onStderr(s ?: "") + } + } + + return withNewRootShell { + newJob().add(cmd).to(stdoutCallback, stderrCallback).exec() + } +} + +fun flashModule( + uri: Uri, + onFinish: (Boolean, Int) -> Unit, + onStdout: (String) -> Unit, + onStderr: (String) -> Unit +): Boolean { val resolver = ksuApp.contentResolver with(resolver.openInputStream(uri)) { val file = File(ksuApp.cacheDir, "module.zip") @@ -99,30 +178,141 @@ fun installModule(uri: Uri, onFinish: (Boolean) -> Unit, onStdout: (String) -> U this?.copyTo(output) } val cmd = "module install ${file.absolutePath}" + val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr) + Log.i("KernelSU", "install module $uri result: $result") + + file.delete() - val shell = getRootShell() + onFinish(result.isSuccess, result.code) + return result.isSuccess + } +} - val stdoutCallback: CallbackList = object : CallbackList() { - override fun onAddElement(s: String?) { - onStdout(s ?: "") - } +fun runModuleAction( + moduleId: String, onStdout: (String) -> Unit, onStderr: (String) -> Unit +): Boolean { + val shell = getRootShell() + + val stdoutCallback: CallbackList = object : CallbackList() { + override fun onAddElement(s: String?) { + onStdout(s ?: "") } + } + + val stderrCallback: CallbackList = object : CallbackList() { + override fun onAddElement(s: String?) { + onStderr(s ?: "") + } + } + + val result = shell.newJob().add("${getKsuDaemonPath()} module action $moduleId") + .to(stdoutCallback, stderrCallback).exec() + Log.i("KernelSU", "Module runAction result: $result") + + return result.isSuccess +} + +fun restoreBoot( + onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit +): Boolean { + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") + val result = flashWithIO("${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot", onStdout, onStderr) + onFinish(result.isSuccess, result.code) + return result.isSuccess +} + +fun uninstallPermanently( + onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit +): Boolean { + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") + val result = flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr) + onFinish(result.isSuccess, result.code) + return result.isSuccess +} + +suspend fun shrinkModules(): Boolean = withContext(Dispatchers.IO) { + execKsud("module shrink", true) +} + +@Parcelize +sealed class LkmSelection : Parcelable { + data class LkmUri(val uri: Uri) : LkmSelection() + data class KmiString(val value: String) : LkmSelection() + data object KmiNone : LkmSelection() +} + +fun installBoot( + bootUri: Uri?, + lkm: LkmSelection, + ota: Boolean, + onFinish: (Boolean, Int) -> Unit, + onStdout: (String) -> Unit, + onStderr: (String) -> Unit, +): Boolean { + val resolver = ksuApp.contentResolver - val stderrCallback: CallbackList = object : CallbackList() { - override fun onAddElement(s: String?) { - onStderr(s ?: "") + val bootFile = bootUri?.let { uri -> + with(resolver.openInputStream(uri)) { + val bootFile = File(ksuApp.cacheDir, "boot.img") + bootFile.outputStream().use { output -> + this?.copyTo(output) } + + bootFile } + } - val result = - shell.newJob().add("${getKsuDaemonPath()} $cmd").to(stdoutCallback, stderrCallback).exec() - Log.i("KernelSU", "install module $uri result: $result") + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") + var cmd = "boot-patch --magiskboot ${magiskboot.absolutePath}" - file.delete() + cmd += if (bootFile == null) { + // no boot.img, use -f to force install + " -f" + } else { + " -b ${bootFile.absolutePath}" + } - onFinish(result.isSuccess) - return result.isSuccess + if (ota) { + cmd += " -u" + } + + var lkmFile: File? = null + when (lkm) { + is LkmSelection.LkmUri -> { + lkmFile = with(resolver.openInputStream(lkm.uri)) { + val file = File(ksuApp.cacheDir, "kernelsu-tmp-lkm.ko") + file.outputStream().use { output -> + this?.copyTo(output) + } + + file + } + cmd += " -m ${lkmFile.absolutePath}" + } + + is LkmSelection.KmiString -> { + cmd += " --kmi ${lkm.value}" + } + + LkmSelection.KmiNone -> { + // do nothing + } } + + // output dir + val downloadsDir = + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + cmd += " -o $downloadsDir" + + val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr) + Log.i("KernelSU", "install boot result: ${result.isSuccess}") + + bootFile?.delete() + lkmFile?.delete() + + // if boot uri is empty, it is direct install, when success, we should show reboot button + onFinish(bootUri == null && result.isSuccess, result.code) + return result.isSuccess } fun reboot(reason: String = "") { @@ -134,6 +324,33 @@ fun reboot(reason: String = "") { ShellUtils.fastCmd(shell, "/system/bin/svc power reboot $reason || /system/bin/reboot $reason") } +fun rootAvailable(): Boolean { + val shell = getRootShell() + 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() + val cmd = "boot-info current-kmi" + ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd") +} + +suspend fun getSupportedKmis(): List = withContext(Dispatchers.IO) { + val shell = getRootShell() + val cmd = "boot-info supported-kmi" + val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out + out.filter { it.isNotBlank() }.map { it.trim() } +} + fun overlayFsAvailable(): Boolean { val shell = getRootShell() // check /proc/filesystems @@ -141,8 +358,8 @@ fun overlayFsAvailable(): Boolean { } fun hasMagisk(): Boolean { - val shell = getRootShell() - val result = shell.newJob().add("nsenter --mount=/proc/1/ns/mnt which magisk").exec() + val shell = getRootShell(true) + val result = shell.newJob().add("which magisk").exec() Log.i(TAG, "has magisk: ${result.isSuccess}") return result.isSuccess } @@ -153,26 +370,54 @@ fun isSepolicyValid(rules: String?): Boolean { } val shell = getRootShell() val result = - shell.newJob().add("${getKsuDaemonPath()} sepolicy check '$rules'").to(ArrayList(), null).exec() + shell.newJob().add("${getKsuDaemonPath()} sepolicy check '$rules'").to(ArrayList(), null) + .exec() return result.isSuccess } fun getSepolicy(pkg: String): String { val shell = getRootShell() val result = - shell.newJob().add("${getKsuDaemonPath()} profile get-sepolicy $pkg").to(ArrayList(), null).exec() + shell.newJob().add("${getKsuDaemonPath()} profile get-sepolicy $pkg").to(ArrayList(), null) + .exec() Log.i(TAG, "code: ${result.code}, out: ${result.out}, err: ${result.err}") return result.out.joinToString("\n") } fun setSepolicy(pkg: String, rules: String): Boolean { val shell = getRootShell() - val result = - shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'").to(ArrayList(), null).exec() + val result = shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'") + .to(ArrayList(), null).exec() Log.i(TAG, "set sepolicy result: ${result.code}") return result.isSuccess } +fun listAppProfileTemplates(): List { + val shell = getRootShell() + return shell.newJob().add("${getKsuDaemonPath()} profile list-templates").to(ArrayList(), null) + .exec().out +} + +fun getAppProfileTemplate(id: String): String { + val shell = getRootShell() + return shell.newJob().add("${getKsuDaemonPath()} profile get-template '${id}'") + .to(ArrayList(), null).exec().out.joinToString("\n") +} + +fun setAppProfileTemplate(id: String, template: String): Boolean { + val shell = getRootShell() + val escapedTemplate = template.replace("\"", "\\\"") + val cmd = """${getKsuDaemonPath()} profile set-template "$id" "$escapedTemplate'"""" + return shell.newJob().add(cmd) + .to(ArrayList(), null).exec().isSuccess +} + +fun deleteAppProfileTemplate(id: String): Boolean { + val shell = getRootShell() + return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'") + .to(ArrayList(), null).exec().isSuccess +} + fun forceStopApp(packageName: String) { val shell = getRootShell() val result = shell.newJob().add("am force-stop $packageName").exec() @@ -182,11 +427,14 @@ fun forceStopApp(packageName: String) { fun launchApp(packageName: String) { val shell = getRootShell() - val result = shell.newJob().add("monkey -p $packageName -c android.intent.category.LAUNCHER 1").exec() + val result = + shell.newJob() + .add("cmd package resolve-activity --brief $packageName | tail -n 1 | xargs cmd activity start-activity -n") + .exec() Log.i(TAG, "launch $packageName result: $result") } fun restartApp(packageName: String) { forceStopApp(packageName) launchApp(packageName) -} \ No newline at end of file +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/LogEvent.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/LogEvent.kt index 17d3ec9c914f..a8363120c0fb 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/LogEvent.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/LogEvent.kt @@ -22,33 +22,46 @@ fun getBugreportFile(context: Context): File { val tombstonesFile = File(bugreportDir, "tombstones.tar.gz") val dropboxFile = File(bugreportDir, "dropbox.tar.gz") val pstoreFile = File(bugreportDir, "pstore.tar.gz") + // Xiaomi/Readmi devices have diag in /data/vendor/diag val diagFile = File(bugreportDir, "diag.tar.gz") + val opulsFile = File(bugreportDir, "opuls.tar.gz") val bootlogFile = File(bugreportDir, "bootlog.tar.gz") val mountsFile = File(bugreportDir, "mounts.txt") val fileSystemsFile = File(bugreportDir, "filesystems.txt") - val ksuFileTree = File(bugreportDir, "ksu_tree.txt") + val adbFileTree = File(bugreportDir, "adb_tree.txt") + val adbFileDetails = File(bugreportDir, "adb_details.txt") + val ksuFileSize = File(bugreportDir, "ksu_size.txt") val appListFile = File(bugreportDir, "packages.txt") val propFile = File(bugreportDir, "props.txt") val allowListFile = File(bugreportDir, "allowlist.bin") + val procModules = File(bugreportDir, "proc_modules.txt") + val bootConfig = File(bugreportDir, "boot_config.txt") + val kernelConfig = File(bugreportDir, "defconfig.gz") - val shell = getRootShell() + val shell = getRootShell(true) shell.newJob().add("dmesg > ${dmesgFile.absolutePath}").exec() shell.newJob().add("logcat -d > ${logcatFile.absolutePath}").exec() shell.newJob().add("tar -czf ${tombstonesFile.absolutePath} -C /data/tombstones .").exec() shell.newJob().add("tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .").exec() shell.newJob().add("tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .").exec() - shell.newJob().add("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag .").exec() + shell.newJob().add("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag . --exclude=./minidump.gz").exec() + shell.newJob().add("tar -czf ${opulsFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .").exec() shell.newJob().add("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .").exec() shell.newJob().add("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec() shell.newJob().add("cat /proc/filesystems > ${fileSystemsFile.absolutePath}").exec() - shell.newJob().add("ls -alRZ /data/adb > ${ksuFileTree.absolutePath}").exec() + shell.newJob().add("busybox tree /data/adb > ${adbFileTree.absolutePath}").exec() + shell.newJob().add("ls -alRZ /data/adb > ${adbFileDetails.absolutePath}").exec() + shell.newJob().add("du -sh /data/adb/ksu/* > ${ksuFileSize.absolutePath}").exec() shell.newJob().add("cp /data/system/packages.list ${appListFile.absolutePath}").exec() shell.newJob().add("getprop > ${propFile.absolutePath}").exec() shell.newJob().add("cp /data/adb/ksu/.allowlist ${allowListFile.absolutePath}").exec() + shell.newJob().add("cp /proc/modules ${procModules.absolutePath}").exec() + shell.newJob().add("cp /proc/bootconfig ${bootConfig.absolutePath}").exec() + shell.newJob().add("cp /proc/config.gz ${kernelConfig.absolutePath}").exec() - val selinux = ShellUtils.fastCmd(shell, "getenforce"); + val selinux = ShellUtils.fastCmd(shell, "getenforce") // basic information val buildInfo = File(bugreportDir, "basic.txt") @@ -68,7 +81,7 @@ fun getBugreportFile(context: Context): File { val uname = Os.uname() pw.println("KernelRelease: ${uname.release}") pw.println("KernelVersion: ${uname.version}") - pw.println("Mahcine: ${uname.machine}") + pw.println("Machine: ${uname.machine}") pw.println("Nodename: ${uname.nodename}") pw.println("Sysname: ${uname.sysname}") @@ -76,6 +89,8 @@ fun getBugreportFile(context: Context): File { pw.println("KernelSU: $ksuKernel") val safeMode = Natives.isSafeMode pw.println("SafeMode: $safeMode") + val lkmMode = Natives.isLkmMode + pw.println("LKM: $lkmMode") } // modules @@ -93,3 +108,4 @@ fun getBugreportFile(context: Context): File { return targetFile } + diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/SELinuxChecker.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/SELinuxChecker.kt index d51fd91ef035..6b0d704ece67 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/SELinuxChecker.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/SELinuxChecker.kt @@ -1,7 +1,7 @@ package me.weishu.kernelsu.ui.util -import androidx.compose.ui.res.stringResource import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource import com.topjohnwu.superuser.Shell import me.weishu.kernelsu.R @@ -12,7 +12,9 @@ fun getSELinuxStatus(): String { .build("sh") val list = ArrayList() - val result = shell.newJob().add("getenforce").to(list, list).exec() + val result = shell.use { + it.newJob().add("getenforce").to(list, list).exec() + } val output = result.out.joinToString("\n").trim() if (result.isSuccess) { diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/LatestVersionInfo.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/LatestVersionInfo.kt new file mode 100644 index 000000000000..374b3853ab1a --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/LatestVersionInfo.kt @@ -0,0 +1,7 @@ +package me.weishu.kernelsu.ui.util.module + +data class LatestVersionInfo( + val versionCode : Int = 0, + val downloadUrl : String = "", + val changelog : String = "" +) diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt index 1d3bee0cf0b6..6561aea19358 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt @@ -1,6 +1,5 @@ package me.weishu.kernelsu.ui.viewmodel -import android.net.Uri import android.os.SystemClock import android.util.Log import androidx.compose.runtime.derivedStateOf @@ -11,12 +10,13 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import me.weishu.kernelsu.ui.util.HanziToPinyin import me.weishu.kernelsu.ui.util.listModules import me.weishu.kernelsu.ui.util.overlayFsAvailable import org.json.JSONArray import org.json.JSONObject import java.text.Collator -import java.util.* +import java.util.Locale class ModuleViewModel : ViewModel() { @@ -36,6 +36,8 @@ class ModuleViewModel : ViewModel() { val update: Boolean, val remove: Boolean, val updateJson: String, + val hasWebUi: Boolean, + val hasActionScript: Boolean, ) data class ModuleUpdateInfo( @@ -47,13 +49,23 @@ class ModuleViewModel : ViewModel() { var isRefreshing by mutableStateOf(false) private set + var search by mutableStateOf("") var isOverlayAvailable by mutableStateOf(overlayFsAvailable()) private set + var sortEnabledFirst by mutableStateOf(false) + var sortActionFirst by mutableStateOf(false) val moduleList by derivedStateOf { - val comparator = compareBy(Collator.getInstance(Locale.getDefault()), ModuleInfo::id) - modules.sortedWith(comparator).also { + val comparator = + compareBy( + { if (sortEnabledFirst) !it.enabled else 0 }, + { if (sortActionFirst) !it.hasWebUi && !it.hasActionScript else 0 }, + ).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) + }.sortedWith(comparator).also { isRefreshing = false } } @@ -87,7 +99,6 @@ class ModuleViewModel : ViewModel() { .map { obj -> ModuleInfo( obj.getString("id"), - obj.optString("name"), obj.optString("author", "Unknown"), obj.optString("version", "Unknown"), @@ -96,7 +107,9 @@ class ModuleViewModel : ViewModel() { obj.getBoolean("enabled"), obj.getBoolean("update"), obj.getBoolean("remove"), - obj.optString("updateJson") + obj.optString("updateJson"), + obj.optBoolean("web"), + obj.optBoolean("action") ) }.toList() isNeedRefresh = false @@ -157,4 +170,4 @@ class ModuleViewModel : ViewModel() { return Triple(zipUrl, version, changelog) } -} +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt index 41bc1a9d54ac..ecb5024aaed0 100644 --- a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt @@ -45,7 +45,7 @@ class SuperUserViewModel : ViewModel() { val packageName: String get() = packageInfo.packageName val uid: Int - get() = packageInfo.applicationInfo.uid + get() = packageInfo.applicationInfo!!.uid val allowSu: Boolean get() = profile != null && profile.allowSu @@ -83,11 +83,14 @@ class SuperUserViewModel : ViewModel() { val appList by derivedStateOf { sortedList.filter { - it.label.contains(search) || it.packageName.contains(search) || HanziToPinyin.getInstance() - .toPinyinString(it.label).contains(search) + it.label.contains(search, true) || it.packageName.contains( + search, + true + ) || HanziToPinyin.getInstance() + .toPinyinString(it.label).contains(search, true) }.filter { it.uid == 2000 // Always show shell - || showSystemApps || it.packageInfo.applicationInfo.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 + || showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 } } @@ -104,7 +107,7 @@ class SuperUserViewModel : ViewModel() { } } - val intent = Intent(ksuApp, KsuService::class.java); + val intent = Intent(ksuApp, KsuService::class.java) val task = KsuService.bindOrTask( intent, @@ -116,7 +119,7 @@ class SuperUserViewModel : ViewModel() { } private fun stopKsuService() { - val intent = Intent(ksuApp, KsuService::class.java); + val intent = Intent(ksuApp, KsuService::class.java) KsuService.stop(intent) } @@ -143,7 +146,7 @@ class SuperUserViewModel : ViewModel() { apps = packages.map { val appInfo = it.applicationInfo - val uid = appInfo.uid + val uid = appInfo!!.uid val profile = Natives.getAppProfile(it.packageName, uid) AppInfo( label = appInfo.loadLabel(pm).toString(), diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/TemplateViewModel.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/TemplateViewModel.kt new file mode 100644 index 000000000000..cbed82f21c05 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/TemplateViewModel.kt @@ -0,0 +1,328 @@ +package me.weishu.kernelsu.ui.viewmodel + +import android.os.Parcelable +import android.util.Log +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import me.weishu.kernelsu.Natives +import me.weishu.kernelsu.profile.Capabilities +import me.weishu.kernelsu.profile.Groups +import me.weishu.kernelsu.ui.util.getAppProfileTemplate +import me.weishu.kernelsu.ui.util.listAppProfileTemplates +import me.weishu.kernelsu.ui.util.setAppProfileTemplate +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONArray +import org.json.JSONObject +import java.text.Collator +import java.util.Locale +import java.util.concurrent.TimeUnit + + +/** + * @author weishu + * @date 2023/10/20. + */ +const val TEMPLATE_INDEX_URL = "https://kernelsu.org/templates/index.json" +const val TEMPLATE_URL = "https://kernelsu.org/templates/%s" + +const val TAG = "TemplateViewModel" + +class TemplateViewModel : ViewModel() { + companion object { + + private var templates by mutableStateOf>(emptyList()) + } + + @Parcelize + data class TemplateInfo( + val id: String = "", + val name: String = "", + val description: String = "", + val author: String = "", + val local: Boolean = true, + + val namespace: Int = Natives.Profile.Namespace.INHERITED.ordinal, + val uid: Int = Natives.ROOT_UID, + val gid: Int = Natives.ROOT_GID, + val groups: List = mutableListOf(), + val capabilities: List = mutableListOf(), + val context: String = Natives.KERNEL_SU_DOMAIN, + val rules: List = mutableListOf(), + ) : Parcelable + + var isRefreshing by mutableStateOf(false) + private set + + val templateList by derivedStateOf { + val comparator = compareBy(TemplateInfo::local).reversed().then( + compareBy( + Collator.getInstance(Locale.getDefault()), TemplateInfo::id + ) + ) + templates.sortedWith(comparator).apply { + isRefreshing = false + } + } + + suspend fun fetchTemplates(sync: Boolean = false) { + isRefreshing = true + withContext(Dispatchers.IO) { + val localTemplateIds = listAppProfileTemplates() + Log.i(TAG, "localTemplateIds: $localTemplateIds") + if (localTemplateIds.isEmpty() || sync) { + // if no templates, fetch remote templates + fetchRemoteTemplates() + } + + // fetch templates again + templates = listAppProfileTemplates().mapNotNull(::getTemplateInfoById) + + isRefreshing = false + } + } + + suspend fun importTemplates( + templates: String, + onSuccess: suspend () -> Unit, + onFailure: suspend (String) -> Unit + ) { + withContext(Dispatchers.IO) { + runCatching { + JSONArray(templates) + }.getOrElse { + runCatching { + val json = JSONObject(templates) + JSONArray().apply { put(json) } + }.getOrElse { + onFailure("invalid templates: $templates") + return@withContext + } + }.let { + 0.until(it.length()).forEach { i -> + runCatching { + val template = it.getJSONObject(i) + val id = template.getString("id") + template.put("local", true) + setAppProfileTemplate(id, template.toString()) + }.onFailure { e -> + Log.e(TAG, "ignore invalid template: $it", e) + } + } + onSuccess() + } + } + } + + suspend fun exportTemplates(onTemplateEmpty: () -> Unit, callback: (String) -> Unit) { + withContext(Dispatchers.IO) { + val templates = listAppProfileTemplates().mapNotNull(::getTemplateInfoById).filter { + it.local + } + templates.ifEmpty { + onTemplateEmpty() + return@withContext + } + JSONArray(templates.map { + it.toJSON() + }).toString().let(callback) + } + } +} + +private fun fetchRemoteTemplates() { + runCatching { + val client: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(5, TimeUnit.SECONDS) + .writeTimeout(5, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build() + + client.newCall( + Request.Builder().url(TEMPLATE_INDEX_URL).build() + ).execute().use { response -> + if (!response.isSuccessful) { + return + } + val remoteTemplateIds = JSONArray(response.body!!.string()) + Log.i(TAG, "fetchRemoteTemplates: $remoteTemplateIds") + 0.until(remoteTemplateIds.length()).forEach { i -> + val id = remoteTemplateIds.getString(i) + Log.i(TAG, "fetch template: $id") + val templateJson = client.newCall( + Request.Builder().url(TEMPLATE_URL.format(id)).build() + ).runCatching { + execute().use { response -> + if (!response.isSuccessful) { + return@forEach + } + response.body!!.string() + } + }.getOrNull() ?: return@forEach + Log.i(TAG, "template: $templateJson") + + // validate remote template + runCatching { + val json = JSONObject(templateJson) + fromJSON(json)?.let { + // force local template + json.put("local", false) + setAppProfileTemplate(id, json.toString()) + } + }.onFailure { + Log.e(TAG, "ignore invalid template: $it", it) + return@forEach + } + } + } + }.onFailure { Log.e(TAG, "fetchRemoteTemplates: $it", it) } +} + +@Suppress("UNCHECKED_CAST") +private fun JSONArray.mapCatching( + transform: (T) -> R, onFail: (Throwable) -> Unit +): List { + return List(length()) { i -> get(i) as T }.mapNotNull { element -> + runCatching { + transform(element) + }.onFailure(onFail).getOrNull() + } +} + +private inline fun > getEnumOrdinals( + jsonArray: JSONArray?, enumClass: Class +): List { + return jsonArray?.mapCatching({ name -> + enumValueOf(name.uppercase()) + }, { + Log.e(TAG, "ignore invalid enum ${enumClass.simpleName}: $it", it) + }).orEmpty() +} + +fun getTemplateInfoById(id: String): TemplateViewModel.TemplateInfo? { + return runCatching { + fromJSON(JSONObject(getAppProfileTemplate(id))) + }.onFailure { + Log.e(TAG, "ignore invalid template: $it", it) + }.getOrNull() +} + +private fun getLocaleString(json: JSONObject, key: String): String { + val fallback = json.getString(key) + val locale = Locale.getDefault() + val localeKey = "${locale.language}_${locale.country}" + json.optJSONObject("locales")?.let { + // check locale first + it.optJSONObject(localeKey)?.let { json-> + return json.optString(key, fallback) + } + // fallback to language + it.optJSONObject(locale.language)?.let { json-> + return json.optString(key, fallback) + } + } + return fallback +} + +private fun fromJSON(templateJson: JSONObject): TemplateViewModel.TemplateInfo? { + return runCatching { + val groupsJsonArray = templateJson.optJSONArray("groups") + val capabilitiesJsonArray = templateJson.optJSONArray("capabilities") + val context = templateJson.optString("context").takeIf { it.isNotEmpty() } + ?: Natives.KERNEL_SU_DOMAIN + val namespace = templateJson.optString("namespace").takeIf { it.isNotEmpty() } + ?: Natives.Profile.Namespace.INHERITED.name + + val rulesJsonArray = templateJson.optJSONArray("rules") + val templateInfo = TemplateViewModel.TemplateInfo( + id = templateJson.getString("id"), + name = getLocaleString(templateJson, "name"), + description = getLocaleString(templateJson, "description"), + author = templateJson.optString("author"), + local = templateJson.optBoolean("local"), + namespace = Natives.Profile.Namespace.valueOf( + namespace.uppercase() + ).ordinal, + uid = templateJson.optInt("uid", Natives.ROOT_UID), + gid = templateJson.optInt("gid", Natives.ROOT_GID), + groups = getEnumOrdinals(groupsJsonArray, Groups::class.java).map { it.gid }, + capabilities = getEnumOrdinals( + capabilitiesJsonArray, Capabilities::class.java + ).map { it.cap }, + context = context, + rules = rulesJsonArray?.mapCatching({ it }, { + Log.e(TAG, "ignore invalid rule: $it", it) + }).orEmpty() + ) + templateInfo + }.onFailure { + Log.e(TAG, "ignore invalid template: $it", it) + }.getOrNull() +} + +fun TemplateViewModel.TemplateInfo.toJSON(): JSONObject { + val template = this + return JSONObject().apply { + + put("id", template.id) + put("name", template.name.ifBlank { template.id }) + put("description", template.description.ifBlank { template.id }) + if (template.author.isNotEmpty()) { + put("author", template.author) + } + put("namespace", Natives.Profile.Namespace.entries[template.namespace].name) + put("uid", template.uid) + put("gid", template.gid) + + if (template.groups.isNotEmpty()) { + put("groups", JSONArray( + Groups.entries.filter { + template.groups.contains(it.gid) + }.map { + it.name + } + )) + } + + if (template.capabilities.isNotEmpty()) { + put("capabilities", JSONArray( + Capabilities.entries.filter { + template.capabilities.contains(it.cap) + }.map { + it.name + } + )) + } + + if (template.context.isNotEmpty()) { + put("context", template.context) + } + + if (template.rules.isNotEmpty()) { + put("rules", JSONArray(template.rules)) + } + } +} + +@Suppress("unused") +fun generateTemplates() { + val templateJson = JSONObject() + templateJson.put("id", "com.example") + templateJson.put("name", "Example") + templateJson.put("description", "This is an example template") + templateJson.put("local", true) + templateJson.put("namespace", Natives.Profile.Namespace.INHERITED.name) + templateJson.put("uid", 0) + templateJson.put("gid", 0) + + templateJson.put("groups", JSONArray().apply { put(Groups.INET.name) }) + templateJson.put("capabilities", JSONArray().apply { put(Capabilities.CAP_NET_RAW.name) }) + templateJson.put("context", "u:r:su:s0") + Log.i(TAG, "$templateJson") +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/MimeUtil.java b/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/MimeUtil.java new file mode 100644 index 000000000000..1fc2f4a4ceab --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/MimeUtil.java @@ -0,0 +1,88 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package me.weishu.kernelsu.ui.webui; + +import java.net.URLConnection; + +class MimeUtil { + + public static String getMimeFromFileName(String fileName) { + if (fileName == null) { + return null; + } + + // Copying the logic and mapping that Chromium follows. + // First we check against the OS (this is a limited list by default) + // but app developers can extend this. + // We then check against a list of hardcoded mime types above if the + // OS didn't provide a result. + String mimeType = URLConnection.guessContentTypeFromName(fileName); + + if (mimeType != null) { + return mimeType; + } + + return guessHardcodedMime(fileName); + } + + // We should keep this map in sync with the lists under + // //net/base/mime_util.cc in Chromium. + // A bunch of the mime types don't really apply to Android land + // like word docs so feel free to filter out where necessary. + private static String guessHardcodedMime(String fileName) { + int finalFullStop = fileName.lastIndexOf('.'); + if (finalFullStop == -1) { + return null; + } + + final String extension = fileName.substring(finalFullStop + 1).toLowerCase(); + + return switch (extension) { + case "webm" -> "video/webm"; + case "mpeg", "mpg" -> "video/mpeg"; + case "mp3" -> "audio/mpeg"; + case "wasm" -> "application/wasm"; + case "xhtml", "xht", "xhtm" -> "application/xhtml+xml"; + case "flac" -> "audio/flac"; + case "ogg", "oga", "opus" -> "audio/ogg"; + case "wav" -> "audio/wav"; + case "m4a" -> "audio/x-m4a"; + case "gif" -> "image/gif"; + case "jpeg", "jpg", "jfif", "pjpeg", "pjp" -> "image/jpeg"; + case "png" -> "image/png"; + case "apng" -> "image/apng"; + case "svg", "svgz" -> "image/svg+xml"; + case "webp" -> "image/webp"; + case "mht", "mhtml" -> "multipart/related"; + case "css" -> "text/css"; + case "html", "htm", "shtml", "shtm", "ehtml" -> "text/html"; + case "js", "mjs" -> "application/javascript"; + case "xml" -> "text/xml"; + case "mp4", "m4v" -> "video/mp4"; + case "ogv", "ogm" -> "video/ogg"; + case "ico" -> "image/x-icon"; + case "woff" -> "application/font-woff"; + case "gz", "tgz" -> "application/gzip"; + case "json" -> "application/json"; + case "pdf" -> "application/pdf"; + case "zip" -> "application/zip"; + case "bmp" -> "image/bmp"; + case "tiff", "tif" -> "image/tiff"; + default -> null; + }; + } +} diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/SuFilePathHandler.java b/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/SuFilePathHandler.java new file mode 100644 index 000000000000..ff450e8e9cfe --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/SuFilePathHandler.java @@ -0,0 +1,191 @@ +package me.weishu.kernelsu.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 context {@link Context} that is used to access app's internal storage. + * @param directory the absolute path of the exposed app internal storage directory from + * which files can be loaded. + * @throws IllegalArgumentException if the directory is not allowed. + */ + public SuFilePathHandler(@NonNull Context context, @NonNull File directory, Shell rootShell) { + try { + mDirectory = new File(getCanonicalDirPath(directory)); + if (!isAllowedInternalStorageDir(context)) { + throw new IllegalArgumentException("The given directory \"" + directory + + "\" doesn't exist under an allowed app internal storage directory"); + } + mShell = rootShell; + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to resolve the canonical path for the given directory: " + + directory.getPath(), e); + } + } + + private boolean isAllowedInternalStorageDir(@NonNull Context context) throws IOException { + String dir = getCanonicalDirPath(mDirectory); + + for (String forbiddenPath : FORBIDDEN_DATA_DIRS) { + if (dir.startsWith(forbiddenPath)) { + return false; + } + } + return true; + } + + /** + * Opens the requested file from the exposed data directory. + *

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

+ * The MIME type for the file will be determined from the file's extension using + * {@link java.net.URLConnection#guessContentTypeFromName}. Developers should ensure that + * files are named using standard file extensions. If the file does not have a + * recognised extension, {@code "text/plain"} will be used by default. + * + * @param path the suffix path to be handled. + * @return {@link WebResourceResponse} for the requested file. + */ + @Override + @WorkerThread + @NonNull + public WebResourceResponse handle(@NonNull String path) { + 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/me/weishu/kernelsu/ui/webui/WebUIActivity.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIActivity.kt new file mode 100644 index 000000000000..d926d7eada35 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIActivity.kt @@ -0,0 +1,98 @@ +package me.weishu.kernelsu.ui.webui + +import android.annotation.SuppressLint +import android.app.ActivityManager +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.enableEdgeToEdge +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import androidx.webkit.WebViewAssetLoader +import com.topjohnwu.superuser.Shell +import me.weishu.kernelsu.ui.util.createRootShell +import java.io.File + +@SuppressLint("SetJavaScriptEnabled") +class WebUIActivity : ComponentActivity() { + private lateinit var webviewInterface: WebViewInterface + + private var rootShell: Shell? = null + + override fun onCreate(savedInstanceState: Bundle?) { + + // Enable edge to edge + enableEdgeToEdge() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.isNavigationBarContrastEnforced = false + } + + super.onCreate(savedInstanceState) + + val moduleId = intent.getStringExtra("id")!! + val name = intent.getStringExtra("name")!! + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + @Suppress("DEPRECATION") + setTaskDescription(ActivityManager.TaskDescription("KernelSU - $name")) + } else { + val taskDescription = ActivityManager.TaskDescription.Builder().setLabel("KernelSU - $name").build() + setTaskDescription(taskDescription) + } + + val prefs = getSharedPreferences("settings", MODE_PRIVATE) + WebView.setWebContentsDebuggingEnabled(prefs.getBoolean("enable_web_debugging", false)) + + val moduleDir = "/data/adb/modules/${moduleId}" + val webRoot = File("${moduleDir}/webroot") + val rootShell = createRootShell(true).also { this.rootShell = it } + val webViewAssetLoader = WebViewAssetLoader.Builder() + .setDomain("mui.kernelsu.org") + .addPathHandler( + "/", + SuFilePathHandler(this, webRoot, rootShell) + ) + .build() + + val webViewClient = object : WebViewClient() { + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest + ): WebResourceResponse? { + return webViewAssetLoader.shouldInterceptRequest(request.url) + } + } + + val webView = WebView(this).apply { + 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 + } + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.allowFileAccess = false + webviewInterface = WebViewInterface(this@WebUIActivity, this, moduleDir) + addJavascriptInterface(webviewInterface, "ksu") + setWebViewClient(webViewClient) + loadUrl("https://mui.kernelsu.org/index.html") + } + + setContentView(webView) + } + + override fun onDestroy() { + super.onDestroy() + runCatching { rootShell?.close() } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebViewInterface.kt b/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebViewInterface.kt new file mode 100644 index 000000000000..00fcde653b60 --- /dev/null +++ b/manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebViewInterface.kt @@ -0,0 +1,209 @@ +package me.weishu.kernelsu.ui.webui + +import android.app.Activity +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.text.TextUtils +import android.view.Window +import android.webkit.JavascriptInterface +import android.webkit.WebView +import android.widget.Toast +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import com.topjohnwu.superuser.CallbackList +import com.topjohnwu.superuser.ShellUtils +import com.topjohnwu.superuser.internal.UiThreadHandler +import me.weishu.kernelsu.ui.util.createRootShell +import me.weishu.kernelsu.ui.util.listModules +import me.weishu.kernelsu.ui.util.withNewRootShell +import org.json.JSONArray +import org.json.JSONObject +import java.io.File +import java.util.concurrent.CompletableFuture + +class WebViewInterface( + val context: Context, + private val webView: WebView, + private val modDir: String +) { + + @JavascriptInterface + fun exec(cmd: String): String { + return withNewRootShell(true) { ShellUtils.fastCmd(this, cmd) } + } + + @JavascriptInterface + fun exec(cmd: String, callbackFunc: String) { + exec(cmd, null, callbackFunc) + } + + private fun processOptions(sb: StringBuilder, options: String?) { + val opts = if (options == null) JSONObject() else { + JSONObject(options) + } + + val cwd = opts.optString("cwd") + if (!TextUtils.isEmpty(cwd)) { + sb.append("cd ${cwd};") + } + + opts.optJSONObject("env")?.let { env -> + env.keys().forEach { key -> + sb.append("export ${key}=${env.getString(key)};") + } + } + } + + @JavascriptInterface + fun exec( + cmd: String, + options: String?, + callbackFunc: String + ) { + val finalCommand = StringBuilder() + processOptions(finalCommand, options) + finalCommand.append(cmd) + + val result = withNewRootShell(true) { + newJob().add(finalCommand.toString()).to(ArrayList(), ArrayList()).exec() + } + val stdout = result.out.joinToString(separator = "\n") + val stderr = result.err.joinToString(separator = "\n") + + val jsCode = + "javascript: (function() { try { ${callbackFunc}(${result.code}, ${ + JSONObject.quote( + stdout + ) + }, ${JSONObject.quote(stderr)}); } catch(e) { console.error(e); } })();" + webView.post { + webView.loadUrl(jsCode) + } + } + + @JavascriptInterface + fun spawn(command: String, args: String, options: String?, callbackFunc: String) { + val finalCommand = StringBuilder() + + processOptions(finalCommand, options) + + if (!TextUtils.isEmpty(args)) { + finalCommand.append(command).append(" ") + JSONArray(args).let { argsArray -> + for (i in 0 until argsArray.length()) { + finalCommand.append(argsArray.getString(i)) + finalCommand.append(" ") + } + } + } else { + finalCommand.append(command) + } + + val shell = createRootShell(true) + + val emitData = fun(name: String, data: String) { + val jsCode = + "javascript: (function() { try { ${callbackFunc}.${name}.emit('data', ${ + JSONObject.quote( + data + ) + }); } catch(e) { console.error('emitData', e); } })();" + webView.post { + webView.loadUrl(jsCode) + } + } + + val stdout = object : CallbackList(UiThreadHandler::runAndWait) { + override fun onAddElement(s: String) { + emitData("stdout", s) + } + } + + val stderr = object : CallbackList(UiThreadHandler::runAndWait) { + override fun onAddElement(s: String) { + emitData("stderr", s) + } + } + + val future = shell.newJob().add(finalCommand.toString()).to(stdout, stderr).enqueue() + val completableFuture = CompletableFuture.supplyAsync { + future.get() + } + + completableFuture.thenAccept { result -> + val emitExitCode = + "javascript: (function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();" + webView.post { + webView.loadUrl(emitExitCode) + } + + if (result.code != 0) { + val emitErrCode = + "javascript: (function() { try { var err = new Error(); err.exitCode = ${result.code}; err.message = ${ + JSONObject.quote( + result.err.joinToString( + "\n" + ) + ) + };${callbackFunc}.emit('error', err); } catch(e) { console.error('emitErr', e); } })();" + webView.post { + webView.loadUrl(emitErrCode) + } + } + }.whenComplete { _, _ -> + runCatching { shell.close() } + } + } + + @JavascriptInterface + fun toast(msg: String) { + webView.post { + Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() + } + } + + @JavascriptInterface + fun fullScreen(enable: Boolean) { + if (context is Activity) { + Handler(Looper.getMainLooper()).post { + if (enable) { + hideSystemUI(context.window) + } else { + showSystemUI(context.window) + } + } + } + } + + @JavascriptInterface + fun moduleInfo(): String { + val moduleInfos = JSONArray(listModules()) + var currentModuleInfo = JSONObject() + currentModuleInfo.put("moduleDir", modDir) + val moduleId = File(modDir).getName() + for (i in 0 until moduleInfos.length()) { + val currentInfo = moduleInfos.getJSONObject(i) + + if (currentInfo.getString("id") != moduleId) { + continue + } + + var keys = currentInfo.keys() + for (key in keys) { + currentModuleInfo.put(key, currentInfo.get(key)) + } + break + } + return currentModuleInfo.toString() + } +} + +fun hideSystemUI(window: Window) = + WindowInsetsControllerCompat(window, window.decorView).let { controller -> + controller.hide(WindowInsetsCompat.Type.systemBars()) + controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + +fun showSystemUI(window: Window) = + WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars()) diff --git a/manager/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so b/manager/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so new file mode 100644 index 000000000000..451ca81c42d8 Binary files /dev/null and b/manager/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so differ diff --git a/manager/app/src/main/res/drawable/ic_launcher_background.xml b/manager/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 900e2d8c829e..000000000000 --- a/manager/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/manager/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/manager/app/src/main/res/mipmap-anydpi/ic_launcher.xml similarity index 79% rename from manager/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to manager/app/src/main/res/mipmap-anydpi/ic_launcher.xml index b070c763da32..f30783b21028 100644 --- a/manager/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/manager/app/src/main/res/mipmap-anydpi/ic_launcher.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/manager/app/src/main/res/resources.properties b/manager/app/src/main/res/resources.properties new file mode 100644 index 000000000000..d5a3ddc92a9b --- /dev/null +++ b/manager/app/src/main/res/resources.properties @@ -0,0 +1 @@ +unqualifiedResLocale=en-US \ No newline at end of file diff --git a/manager/app/src/main/res/values-ar/strings.xml b/manager/app/src/main/res/values-ar/strings.xml index 297249d749ed..e47b8e22c053 100644 --- a/manager/app/src/main/res/values-ar/strings.xml +++ b/manager/app/src/main/res/values-ar/strings.xml @@ -5,8 +5,8 @@ إضغط للتثبيت يعمل الإصدار: %d - المستخدمين الخارقين: %d - الوحدات: %d + مستخدمين الجذر: %d + الإضافات: %d غير مدعوم KernelSU يدعم GKI kernels فقط إصدار النواة @@ -18,10 +18,10 @@ متساهل مجهول مستخدم خارق - فشل في تمكين الوحدة: %s - فشل تعطيل الوحدة : %s - لا توجد وحدة مثبتة - الوحدات + فشل في تمكين الإضافة: %s + فشل تعطيل الإضافة : %s + لا توجد إضافات مثبتة + الإضافات إلغاء التثبيت تثبيت الوحدة تثبيت @@ -33,30 +33,30 @@ إعادة تشغيل إلى وضع Download إعادة تشغيل إلى وضع EDL من نحن - هل أنت متأكد أنك تريد إلغاء تثبيت الوحدة %s ? - تم إلغاء التثبيت %s + هل أنت متأكد أنك تريد إلغاء تثبيت الإضافة %s ? + تم إلغاء تثبيتها %s فشل إلغاء التثبيت: %s الإصدار المطور - التراكبات غير متوفرة ، لا يمكن للوحدة أن تعمل! + الوحدات غير متوفرة حيث يتم تعطيل نظام الملفات المتراكب بواسطة النواة. إنعاش إظهار تطبيقات النظام إخفاء تطبيقات النظام إرسال السجلات الوضع الآمن إعادة التشغيل لتطبيق التغييرات - تم تعطيل الوحدات النمطية لأنها تتعارض مع Magisk! + الوحدات غير متاحة بسبب تعارضها مع Magisk! تعلم KernelSU https://kernelsu.org/guide/what-is-kernelsu.html - تعرف على كيفية تثبيت KernelSU واستخدام الوحدات + تعرف على كيفية تثبيت KernelSU واستخدام الإضافات إدعمنا KernelSU سيظل دائماً مجانياً ومفتوح المصدر. مع ذلك، يمكنك أن تظهر لنا أنك تهتم بالتبرع. إنضم إلى قناتنا في %2$s ]]> القدرات تحديث - تحمبل الوحدة : %s + تحميل الإضافة: %s ابدأ التنزيل: %s - الإصدار الجديد: %s متاح ، انقر للتحديث + الإصدار الجديد: %s متاح ، انقر للتحديث. تشغيل الإفتراضي نموذج @@ -66,13 +66,13 @@ مجموعات مُخصّص تركيب مساحة الاسم - الغاء تحميل الوحدات + الغاء تحميل الإضافات فشل تحديث ملف تعريف التطبيق لـ %s سياق SELinux ايقاف إجباري - الغاء تحميل الوحدات بشكل افتراضي - القيمة الافتراضية العامة ل \"إلغاء تحميل الوحدات \" في ملفات تعريف التطبيقات. إذا تم تمكينه ، إزالة جميع تعديلات الوحدة النمطية على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف. - سيسمح تمكين هذا الخيار ل KernelSU باستعادة أي ملفات معدلة بواسطة الوحدات النمطية لهذا التطبيق. + الغاء تحميل الإضافات بشكل افتراضي + القيمة الافتراضية العامة لـ\"إلغاء تحميل الإضافات\" في ملفات تعريف التطبيقات. إذا تم تمكينه، إزالة جميع تعديلات الإضافات على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف. + سيسمح تمكين هذا الخيار لـKernelSU باستعادة أي ملفات معدلة بواسطة الإضافات لهذا التطبيق. المجال القواعد إعادة تشغيل التطبيق @@ -80,4 +80,59 @@ اسم الملف الشخصي إصدار KernelSU الحالي %d منخفض جدًا بحيث لا يعمل المدير بشكل صحيح. الرجاء الترقية إلى الإصدار %d أو أعلى! سجل التغييرات + تم الاستيراد بنجاح + تصدير إلى الحافظة + لا يمكن العثور على القالب المحلي للتصدير! + معرف القالب موجود بالفعل! + استيراد من الحافظة + فشل في جلب سجل التغيير: %s + الاسم + معرف القالب غير صالح + مزامنة القوالب عبر الإنترنت + إنشاء قالب + للقراءة فقط + استيراد / تصدير + فشل في حفظ القالب + تحرير القالب + المعرف + قالب ملف تعريف التطبيق + الوصف + حفظ + إدارة القالب المحلي وعبر الإنترنت لملف تعريف التطبيق + حذف + الحافظة فارغة! + عرض القالب + فشل في منح صلاحية الجذر! + فتح + التحقق تلقائيًا من وجود تحديثات عند فتح التطبيق + التحقق من التحديث + تمكين تصحيح أخطاء WebView + يمكن استخدامه لتصحيح أخطاء WebUI، يرجى تمكينه فقط عند الحاجة. + التالي + اختيار ملف + تثبيت مباشر (موصى به) + التثبيت على فتحة غير نشطة (بعد OTA) + سيتم **إجبار** جهازك على التمهيد إلى الفتحة غير النشطة الحالية بعد إعادة التشغيل! +\nاستخدم هذا الخيار فقط بعد انتهاء التحديث. +\nأستمرار؟ + اختر KMI + يوصى باستخدام صورة القسم %1$s + تصغير الصورة المتفرقة + قم بتغيير حجم الصورة المتفرقة حيث توجد الإضافة إلى حجمها الفعلي. لاحظ أن هذا قد يتسبب في عمل الإضافة بشكل غير طبيعي، لذا يرجى استخدامها فقط عند الضرورة (مثل النسخ الاحتياطي). + إلغاء التثبيت + إلغاء التثبيت مؤقتًا + إلغاء التثبيت بشكل دائم + استعادة الصورة الاصلية + ‬إلغاء تثبيت KernelSU .(الجذر وجميع الوحدات) بشكل كامل ودائم. + تركيب + نجح التركيب + فشل التركيب + LKM المحددة: %s + استعادة صورة المصنع المخزنة (في حالة وجود نسخة احتياطية)، والتي تُستخدم عادة قبل OTA؛ إذا كنت بحاجة إلى إلغاء تثبيت KernelSU، فيرجى استخدام \"إلغاء التثبيت الدائم\". + قم بإلغاء تثبيت KernelSU مؤقتًا، واستعد إلى حالته الأصلية بعد إعادة التشغيل التالية. + حفظ السجلات + إجراء + السجلات محفوظة + فرز (الممكن أولاً) + فرز (الإجراء أولاً) \ No newline at end of file diff --git a/manager/app/src/main/res/values-az/strings.xml b/manager/app/src/main/res/values-az/strings.xml index d6931962972a..21ec189f55c4 100644 --- a/manager/app/src/main/res/values-az/strings.xml +++ b/manager/app/src/main/res/values-az/strings.xml @@ -78,4 +78,5 @@ Məcburi dayandır Yenidən başlat %s görə SELinux qaydalarını güncəlləmək mümkün olmadı + Girişləri Saxla \ No newline at end of file diff --git a/manager/app/src/main/res/values-bn-rBD/strings.xml b/manager/app/src/main/res/values-bn-rBD/strings.xml index 8d5248cfba24..078d88fee7ca 100644 --- a/manager/app/src/main/res/values-bn-rBD/strings.xml +++ b/manager/app/src/main/res/values-bn-rBD/strings.xml @@ -31,7 +31,7 @@ পারমিসিভ মোডিউল ডিসেবল করা যায়নি: %s কোনো মোডিউল ইন্সটল করা নেই - ভারসন: %d + সংস্করণ: %d সুপার ইউজার: %d নেইম স্পেস মাউন্ট ইনহেরিটেড @@ -48,4 +48,5 @@ %s আনইনস্টল করা যায়নি ভার্সন অথার + লগ সংরক্ষণ করুন \ No newline at end of file diff --git a/manager/app/src/main/res/values-bn/strings.xml b/manager/app/src/main/res/values-bn/strings.xml index 676175c99b22..312672c8715a 100644 --- a/manager/app/src/main/res/values-bn/strings.xml +++ b/manager/app/src/main/res/values-bn/strings.xml @@ -8,15 +8,15 @@ সুপার ইউজার: %d মডিউল: %d অসমর্থিত - কার্নেলএসইউ শুধুমাত্র জিকেআই কার্নেল সমর্থন করে + KernelSU শুধুমাত্র GKI কার্নেল সমর্থন করে কার্নেল ম্যানেজার সংস্করণ ফিঙ্গারপ্রিন্ট - সেলিনাক্স স্ট্যাটাস + SELinux স্টেটাস ডিজেবল - এনফোর্সিং + কার্যকর অনুমতিমূলক - অপরিচিত + অজানা সুপার ইউজার মডিউল সক্ষম করতে ব্যর্থ হয়েছে: %s মডিউল নিষ্ক্রিয় করতে ব্যর্থ হয়েছে: %s @@ -63,4 +63,6 @@ গ্লোবাল আলাদাভাবে আনমাউন্ট মোডিউল + ম্যানেজার সঠিকভাবে কাজ করার জন্য বর্তমান KernelSU সংস্করণ %d খুবই কম। অনুগ্রহ করে %d বা উচ্চতর সংস্করণে আপগ্রেড করুন! + লগ সংরক্ষণ করুন \ No newline at end of file diff --git a/manager/app/src/main/res/values-bs/strings.xml b/manager/app/src/main/res/values-bs/strings.xml index d8b86b1090bd..da07863193fe 100644 --- a/manager/app/src/main/res/values-bs/strings.xml +++ b/manager/app/src/main/res/values-bs/strings.xml @@ -79,4 +79,5 @@ Šablon Prilagođeno Naziv profila + Sačuvaj Dnevnike \ No newline at end of file diff --git a/manager/app/src/main/res/values-da/strings.xml b/manager/app/src/main/res/values-da/strings.xml index 4767311c659a..6ab022567f4c 100644 --- a/manager/app/src/main/res/values-da/strings.xml +++ b/manager/app/src/main/res/values-da/strings.xml @@ -79,4 +79,5 @@ Regler Genstart Den nuværende KernelSU version %d er for lav til manageren for at fungere ordentligt. Opgrader til version %d eller højere! + Gem Logfiler \ No newline at end of file diff --git a/manager/app/src/main/res/values-de/strings.xml b/manager/app/src/main/res/values-de/strings.xml index eeafc04a6e58..b7412cc87898 100644 --- a/manager/app/src/main/res/values-de/strings.xml +++ b/manager/app/src/main/res/values-de/strings.xml @@ -6,78 +6,131 @@ Funktioniert Version: %d Superuser - Tippen zum Installieren + Tippe zum Installieren Superuser: %d Unbekannt Erzwingen - Neustart mit Bootloader - Neustart mit Download-Modus - Neustart mit EDL-Modus + In den Bootloader-Modus neustarten + In den Download-Modus neustarten + In den EDL-Modus neustarten Autor - overlayfs nicht verfügbar, Modul kann nicht funktionieren! - Über - Module sind deaktiviert, weil es einen Konflikt mit Magisk gibt! + Module sind nicht verfügbar, da OverlayFS vom Kernel deaktiviert ist. + Über KernelSU + Module sind aufgrund eines Konfliktes mit Magisk nicht verfügbar! https://kernelsu.org/guide/what-is-kernelsu.html - Verstehe, wie du KernelSU installieren und Module verwendest + Erfahre, wie KernelSU installiert wird und wie Module verwendet werden Unterstütze uns - KernelSU ist und wird immer frei und quelloffen sein. Du kannst uns jedoch zeigen, dass du dich für uns interessierst, indem du eine Spende tätigst. + KernelSU ist und wird immer frei und quelloffen sein. Du kannst uns jedoch deine Unterstützung zeigen, indem du eine Spende tätigst. SELinux-Kontext Module standardmäßig aushängen - Globaler Standardwert für \'Module aushängen\' in App-Profilen. Wenn er aktiviert ist, werden alle Moduländerungen im App-System entfernt, für die kein Profil festgelegt ist. + Globaler Standardwert für \"Module aushängen\" im App-Profil. Falls er aktiviert ist, werden alle Moduländerungen im System für alle Apps entfernt, für die kein Profil festgelegt ist. Standard Vorlage Benutzerdefiniert App-Profilaktualisierung für %s fehlgeschlagen - Vererbt + Geerbt Global Individuell - Domain + Domäne Aktualisieren Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen. Regeln - Herunterladen starten: %s - Fehler beim Aktualisieren der SELinux-Regeln für: %s + Starte Download: %s + Aktualisieren der SELinux-Regeln schlug fehl für: %s Starten - Neue Version: %s verfügbar, tippen zum Aktualisieren + Neue Version %s verfügbar, tippen zum Aktualisieren. Stopp erzwingen - Neustart + Neustarten Module: %d - Verwalter-Version - SELinux-Status + Manager-Version + SELinux Status Deaktiviert Modulaktivierung fehlgeschlagen: %s Moduldeaktivierung fehlgeschlagen: %s - Kein Modul installiert + Keine Modul installiert Modul Deinstallieren Installieren - Neustart + Neustarten Einstellungen - Neustart mit Recovery + In den Recovery-Modus neustarten %s deinstalliert Version - Neu laden + Aktualisieren System-Apps anzeigen System-Apps ausblenden Protokoll senden KernelSU verstehen Sicherer Modus - Neu starten, damit die Effekte auftreten - Quellcode unter %1$s ansehen
Unserem %2$s-Kanal beitreten
+ Neustarten, damit Änderungen wirksam werden + Unserem %2$s-Kanal beitreten]]> Profilname Namespace einhängen Gruppen Fähigkeiten Module aushängen - Modul herunterladen: %s + Lädt Modul %s herunter Nicht unterstützt KernelSU unterstützt derzeit nur GKI-Kernel Kernel Fingerabdruck Installieren - Leichter Neustart - Sicher, dass du das Modul %s deinstallieren möchtest\? + Soft-Reboot + Möchtest du wirklich Modul %s deinstallieren? Deinstallation fehlgeschlagen: %s - Die aktuelle Kernel-Version %d ist zu alt für diese Manager-Version. Bitte auf Version %d oder höher upgraden! + Die aktuelle KernelSU-Version %d ist zu alt für diese Manager-Version. Bitte auf Version %d oder höher aktualisieren! Änderungsprotokoll + Erfolgreich importiert + In Zwischenablage exportieren + Kann lokale Vorlage nicht finden! + Vorlagen-ID existiert bereits! + Aus Zwischenablage importieren + Konnte Veränderungs-Protokoll nicht laden: %s + Name + Ungültige Vorlagen-ID + Online-Vorlagen synchronisieren + Vorlage erstellen + Schreibgeschützt + Import/Export + Schlug beim Speichern der Vorlage fehl + Vorlage bearbeiten + ID + App-Profil-Vorlage + Beschreibung + Speichern + Verwalte die lokale und online Vorlage des App-Profils + Löschen + Zwischenablage ist leer! + Vorlage ansehen + WebView-Debugging aktivieren + Kann zum Fehlerbeheben der WebUI verwendet werden, bitte nur im Notfall aktivieren. + %1$s Partitionsabbild empfohlen + KMI auswählen + Weiter + Direkte Installation (empfohlen) + Datei auswählen + In inaktiven Slot installieren (nach OTA) + Nach einem Neustart wird dein Gerät **GEZWUNGEN** in den derzeit inaktiven Slot zu starten! +\nBenutze dies nur nach Fertigstellung des OTA. +\nFortfahren? + Root-Zugriff konnte nicht gewährt werden! + Öffnen + Auf Aktualisierung prüfen + Prüfe automatisch auf Aktualisierungen, wenn die App geöffnet wird + Temporär deinstallieren + Deinstallieren + KernelSU (Root und alle Module) vollständig und dauerhaft deinstallieren. + Ändert die Größe des Sparse-Images, in dem sich das Modul befindet, auf seine tatsächliche Größe. Beachten Sie, dass dies dazu führen kann, dass das Modul nicht ordnungsgemäß funktioniert; verwenden Sie es daher nur, wenn es notwendig ist (Wie für ein Backup). + Protokolle Speichern + Permanent deinstallieren + Standard-Abbild wiederherstellen + KernelSU temporär deinstallieren, originalen Status nach dem nächsten Neustart wiederherstellen. + Das Standard Werksabbild wiederherstellen (falls ein Backup existiert), normalerweise vor einem OTA zu verwenden; falls Sie KernelSU deinstallieren müssen, nutzen Sie bitte \"Permanent deinstallieren\". + Schreibt + Schreiben erfolgreich + Schreiben fehlgeschlagen + Wähle LKM: %s + Spärliches Bild minimieren + Aktion + Protokolle gespeichert \ No newline at end of file diff --git a/manager/app/src/main/res/values-es/strings.xml b/manager/app/src/main/res/values-es/strings.xml index e29c1417e551..a630db99d132 100644 --- a/manager/app/src/main/res/values-es/strings.xml +++ b/manager/app/src/main/res/values-es/strings.xml @@ -2,83 +2,131 @@ Inicio No instalado - Haga clic para instalar - Activo + Haz clic para instalar + Funcionando Versión: %d Superusuarios: %d Módulos: %d - No soportado - Por el momento, KernelSU solo es compatible con kernels genéricos (GKIs) + Sin soporte + KernelSU solo admite kernels GKI por ahora Versión del kernel - Versión del manager + Versión del gestor Huella del dispositivo Estado de SELinux - Disabled - Enforcing - Permissive - Unknown + Desactivado + Estricto + Permisivo + Desconocido Superusuario - No se pudo habilitar el módulo \"%s\" - No se pudo deshabilitar el módulo \"%s\" - No hay ningún módulo instalado + Error al activar el módulo: %s + Error al desactivar el módulo: %s + Ningún módulo instalado Módulo Desinstalar - Instalar módulo + Instalar Instalar Reiniciar Ajustes Reinicio suave - Reiniciar en modo recovery - Reiniciar en modo bootloader - Reiniciar en modo download + Reiniciar en modo de recuperación + Reiniciar en modo de arranque + Reiniciar en modo Download Reiniciar en modo EDL Acerca de - ¿Estás seguro de que quieres desinstalar el módulo \"%s\"? - \"%s\" desinstalado - No se pudo desinstalar \"%s\" + ¿Está seguro de que desea desinstalar el módulo %s? + %s desinstalado + Fallo al desinstalar: %s Versión Autor - El módulo no puede funcionar ya que OverlayFS no está disponible! - Recargar - Mostrar apps del sistema - Ocultar apps del sistema - Enviar registro + Los módulos no están disponibles ya que OverlayFS está desactivado por el kernel. + Refrescar + Mostrar aplicaciones del sistema + Ocultar aplicaciones del sistema + Enviar registros Modo seguro - Reiniciar para aplicar cambios - Se deshabilitaron los módulos ya que entran en conflicto con Magisk! - Descubre KernelSU + Reinicia para aplicar cambios + ¡Los módulos no están disponibles debido a un conflicto con Magisk! + Aprende KernelSU https://kernelsu.org/guide/what-is-kernelsu.html - Descubre cómo instalar KernelSU y utilizar módulos + Aprende a instalar KernelSU y a utilizar módulos Apóyanos - KernelSU es y siempre será, libre y de código abierto. De todas formas, puedes mostrarnos tu apoyo mediante una donación. - Únete a nuestro canal de %2$s]]> + KernelSU es, y siempre será, gratuito y de código abierto. Sin embargo, puedes demostrarnos que te importamos haciendo una donación. + Ver código fuente en %1$s
Únete a nuestro canal %2$s
Predeterminado Plantilla Personalizado Nombre de perfil - Modo de montaje del espacio de nombres + Montaje del espacio de nombres Heredado Global Individual Grupos Capacidades - Contexto de SELinux + Contexto SELinux Desmontar módulos - No se pudo actualizar el perfil de la app para %s + Error al actualizar el perfil de la aplicación para %s Desmontar módulos por defecto - El valor global predeterminado para \"Desmontar módulos\" en los perfiles de las aplicaciones. Si la habilitas, se desharán todas las modificaciones al sistema hechas por el módulo para las apps que no tengan un perfil establecido. - Si habilitas esta opción, KernelSU podrá restaurar cualquier archivo modificado por los módulos para esta app. + El valor global predeterminado para \"Umount modules\" en App Profile. Si está activado, eliminará todas las modificaciones de módulos del sistema para las apps que no tengan un perfil establecido. + Activar esta opción permitirá a KernelSU restaurar cualquier archivo modificado por los módulos para esta aplicación. Dominio Reglas Actualizar - Descargando módulo: \"%s\" + Descargando módulo: %s Iniciar descarga: %s - Nueva versión: %s está disponible, haga clic para actualizar - Lanzar Aplicacion - Forzar cierre de la aplicacion - Reiniciar aplicacion - Falló al actualizar reglas de SEpolicy por: %s - La versión actual de KernelSU %d es demasiado baja para que el gestor funcione correctamente. ¡Por favor actualiza a la versión %d o superior! + La nueva versión %s está disponible, haga clic para actualizar. + Iniciar + Forzar detención + Reiniciar + Error al actualizar las reglas SELinux para: %s + La versión %d actual de KernelSU es demasiado baja para que el gestor funcione correctamente. Por favor, ¡actualice a la versión %d o superior! Registro de cambios + Importado con éxito + Exportar al portapapeles + ¡No se encuentra la plantilla local para exportar! + ¡El ID de plantilla ya existe! + Importar desde el portapapeles + Fallo en la obtención del registro de cambios: %s + Nombre + ID de plantilla no válida + Sincronizar plantillas en línea + Crear plantilla + Sólo lectura + Importar/Exportar + No se ha podido guardar la plantilla + Editar plantilla + ID + Plantilla de perfil de aplicación + Descripción + Guardar + Gestionar la plantilla local y en línea de App Profile + Eliminar + ¡El portapapeles está vacío! + Ver plantilla + Guardar registros + Activar la depuración de WebView + Se recomienda la imagen de partición %1$s + Selecciona KMI + Siguiente + Instalación directa (Recomendada) + ¡Su dispositivo será **FORZADO** a arrancar en la ranura inactiva actual después de un reinicio!\nUtilice esta opción sólo después de que la OTA se haya realizado.\n¿Continuar? + Desinstalar + Restaurar imagen de archivo + Desinstalar temporalmente KernelSU, restaurar al estado original tras el siguiente reinicio. + LKM seleccionado: %s + Flash falló + Éxito de Flash + ¡No se ha podido conceder el acceso root! + Abrir + Seleccione un archivo + Instalar en ranura inactiva (Después de OTA) + Desinstalar temporalmente + Desinstalar permanentemente + Desinstalar KernelSU (Root y todos los módulos) completa y permanentemente. + Redimensiona la imagen dispersa donde se encuentra el módulo a su tamaño real. Tenga en cuenta que esto puede hacer que el módulo funcione de forma anormal, por lo que sólo debe utilizarlo cuando sea necesario (por ejemplo, para realizar copias de seguridad). + Comprobar actualización + Comprobación automática de actualizaciones al abrir la aplicación + Minimizar la imagen dispersa + Puede ser usado para depurar WebUI, por favor habilítalo sólo cuando sea necesario. + Restaurar la imagen de fábrica stock (Si existe una copia de seguridad), por lo general se utiliza antes de OTA; si necesita desinstalar KernelSU, por favor, utilice \"Desinstalar permanentemente\".
\ No newline at end of file diff --git a/manager/app/src/main/res/values-et/strings.xml b/manager/app/src/main/res/values-et/strings.xml new file mode 100644 index 000000000000..4f177ff884a7 --- /dev/null +++ b/manager/app/src/main/res/values-et/strings.xml @@ -0,0 +1,130 @@ + + + Töötamine + Versioon: %d + Mooduleid: %d + Tuum + Manageri versioon + Sõrmejälg + Lubav + Mooduli lubamine ebaõnnestus: %s + Mooduleid pole paigaldatud + Taaskäivita + Taaskäivita taastusesse + Kas soovid kindlasti eemaldada mooduli %s? + %s eemaldatud + Saada logid + Turvarežiim + Muudatuste rakendamiseks taaskäivita + Õpi KernelSUd + https://kernelsu.org/guide/what-is-kernelsu.html + Vaikimisi + Haagi nimeruum + Lahtihaagitud moodulid + Rakenduseprofiili uuendamine %s jaoks ebaõnnestus + Haagi moodulid vaikimisi lahti + Allalaadimise alustamine: %s + SELinux reeglite uuendamine ebaõnnestus: %s + Muuda malli + Rakenduseprofiili mall + ID + Vaid lugemiseks + Malli ID juba eksisteerib! + Ekspordi lõikelauale + Sünkrooni võrgumallid + Muudatuste logi hankimine ebaõnnestus: %s + Kodu + Klõpsa paigaldamiseks + Pole paigaldatud + Mittetoetatud + Superkasutajaid: %d + KernelSU toetab hetkel vaid GSI tuumasid + SELinuxi olek + Keelatud + Jõustav + Teadmata + Superkasutaja + Mooduli keelamine ebaõnnestus: %s + Moodul + Taaskäivita käivituslaadurisse + Eemalda + Paigalda + Teave + Paigalda + Seaded + Pehme taaskäivitus + Taaskäivita allalaadimisrežiimi + Taaskäivita EDL-i + Värskenda + Autor + Eemaldamine ebaõnnestus: %s + Versioon + Moodulid pole saadaval, kuna OverlayFS on kernelis keelatud. + Kuva süsteemirakendused + Peida süsteemirakendused + Moodulid pole saadaval Magiski konflikti tõttu! + Õpi KernelSUd paigaldama ja mooduleid kasutama + Toeta meid + Grupid + KernelSU on, ja alati jääb, tasuta ning avatud lähtekoodiga kättesaadavaks. Sellegipoolest võid sa näidata, et hoolid, ning teha annetuse. + Mall + Vaata lähtekoodi %1$sis
Liitu meie %2$si kanaliga
+ Profiili nimi + Kohandatud + Päritud + Globaalne + Individuaalne + Võimekused + Sobimatu malli ID + SELinux kontekst + Praegune KernelSU versioon %d on liiga madal, haldur ei saa korrektselt töötada. Palun täienda versioonile %d või kõrgem! + Domeen + Käivita + Sundpeata + Reeglid + Uuenda + Mooduli allalaadimine: %s + Uus versioon %s on saadaval, klõpsa täiendamiseks. + Taaskäivita + Muudatuste logi + Nimi + Kirjeldus + Edukalt imporditud + Salvesta + Lõikelaud on tühi! + Kustuta + Vaata malli + Impordi/ekspordi + Impordi lõikelaualt + Malli salvestamine ebaõnnestus + Loo mall + Halda kohalikke ja võrgusolevaid rakenduseprofiili malle + Selle valiku lubamine lubab KernelSU-l taastada selle rakenduse moodulite poolt mistahes muudetud faile. + Ei saa eksportida, kohalikku malli ei leitud! + Globaalne vaikeväärtus \"Lahtihaagitud moodulitele\" rakenduseprofiilis. Lubamisel eemaldab see kõik moodulite süsteemimuudatused rakendustele, millel ei ole profiili määratud. + Saab kasutada WebUI silumiseks, palun luba ainult vajadusel. + Juurkasutaja andmine ebaõnnestus! + Kontrolli uuendusi + Rakenduse avamisel kontrolli automaatselt uuendusi + Ava + Luba WebView silumine + Salvesta Logid + Vali KMI + %1$s partitsioonitõmmis on soovitatud + Edasi + Sinu seade **SUNNITAKSE** pärast taaskäivitust ebaaktiivsesse lahtrisse käivituma!\nKasuta seda valikut vaid siis, kui tegid üle-õhu uuenduse.\nJätkad? + Eemalda + Eemalda KernelSU ajutiselt, taasta pärast taaskäivitust algseisu. + KernelSU eemaldamine (juurkasutaja ja kõik moodulid) täielikult ja püsivalt. + Taasta tehase-vaiketõmmis (kui varundus eksisteerib), tavaliselt kasutatakse enne üle-õhu uuendust; kui soovid KernelSU-d eemaldada, palun kasuta \"Eemalda püsivalt\". + Välgutamine + Välgutamine õnnestus + Välgutamine ebaõnnestus + Valitud LKM: %s + Otsene paigaldus (soovitatud) + Vali fail + Paigalda ebaaktiivsesse lahtrisse (pärast üle-õhu uuendust) + Eemalda ajutiselt + Eemalda püsivalt + Taasta vaikimisi tõmmis +
\ No newline at end of file diff --git a/manager/app/src/main/res/values-fa/strings.xml b/manager/app/src/main/res/values-fa/strings.xml index 6a6cf2a4b5a4..17469cc8a892 100644 --- a/manager/app/src/main/res/values-fa/strings.xml +++ b/manager/app/src/main/res/values-fa/strings.xml @@ -1,66 +1,68 @@ + -خانه -نصب نشده است -برای نصب ضربه بزنید -به درستی کار می‌کند -نسخه: %d -برنامه های با دسترسی روت: %d -ماژول‌ها: %d -پشتیبانی نشده -کرنل اس یو فقط هسته های gki را پشتیبانی میکند + خانه + نصب نشده است + برای نصب ضربه بزنید + به درستی کار می‌کند + نسخه: %d + برنامه های با دسترسی روت: %d + ماژول‌ها: %d + پشتیبانی نشده + کرنل اس یو فقط هسته های gki را پشتیبانی میکند هسته نسخه برنامه اثرانگشت وضعیت SELinux -غیرفعال -قانونمند -آزاد -ناشناخته -دسترسی روت + غیرفعال + قانونمند + آزاد + ناشناخته + دسترسی روت فعال کردن ماژول ناموفق بود: %s -غیرفعال کردن ماژول ناموفق بود: %s -هیچ ماژولی نصب نشده است -ماژول -لغو نصب -نصب -نصب -راه اندازی دوباره -تنظیمات -راه اندازی نرم -راه اندازی به ریکاوری -راه اندازی به بوتلودر -راه اندازی به حالت دانلود -راه اندازی به EDL -درباره + غیرفعال کردن ماژول ناموفق بود: %s + هیچ ماژولی نصب نشده است + ماژول + لغو نصب + نصب + نصب + راه اندازی دوباره + تنظیمات + راه اندازی نرم + راه اندازی به ریکاوری + راه اندازی به بوتلودر + راه اندازی به حالت دانلود + راه اندازی به EDL + درباره آیا مطمئنید که میخواهید ماژول %s را پاک کنید؟ -%s پاک شد -پاک کردن ناموفق بود: %s -نسخه -سازنده -overlayfs موجود نیست. مازول کار نمیکند!! -تازه‌سازی -نمایش برنامه های سیستمی -مخفی کردن برنامه های سیستمی -ارسال وقایع -حالت امن -راه‌اندازی مجدد برای تاثیرگذاری -مازول به دلیل تعارض با مجیسک غیرفعال شده اند\'s! -یادگیری کرنل اس یو -https://kernelsu.org/guide/what-is-kernelsu.html -یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید -از ما حمایت کنید -KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است. - + %s پاک شد + پاک کردن ناموفق بود: %s + نسخه + سازنده + overlayfs موجود نیست. مازول کار نمیکند!! + تازه‌سازی + نمایش برنامه های سیستمی + مخفی کردن برنامه های سیستمی + ارسال وقایع + حالت امن + راه‌اندازی مجدد برای تاثیرگذاری + مازول به دلیل تعارض با مجیسک غیرفعال شده اند\'s! + یادگیری کرنل اس یو + https://kernelsu.org/guide/what-is-kernelsu.html + یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید + از ما حمایت کنید + KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است. + Join our %2$s channel ]]> -پروفایل برنامه -پیش‌فرض -قالب -شخصی سازی شده -اسم پروفایل -Mount namespace -اثر گرفته -گلوبال -تکی -جداکردن ماژول ها - + پروفایل برنامه + پیش‌فرض + قالب + شخصی سازی شده + اسم پروفایل + Mount namespace + اثر گرفته + گلوبال + تکی + جداکردن ماژول ها + ذخیره گزارش‌ها + \ No newline at end of file diff --git a/manager/app/src/main/res/values-fil/strings.xml b/manager/app/src/main/res/values-fil/strings.xml index f708f0b71ca1..fc527bf30627 100644 --- a/manager/app/src/main/res/values-fil/strings.xml +++ b/manager/app/src/main/res/values-fil/strings.xml @@ -66,4 +66,5 @@ Pangalan ng profile Minana Ang pangkalahatang default na halaga para sa \"Umount modules\" sa Mga Profile ng App. Kung pinagana, aalisin nito ang lahat ng mga pagbabago sa modyul sa system para sa mga aplikasyon na walang hanay ng Profile. + I-save ang mga Log \ No newline at end of file diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index a2d63632fb2b..93668235ce99 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -5,10 +5,10 @@ Version : %d Super-utilisateurs : %d Modules : %d - Actuellement, KernelSU ne supporte que les noyaux GKI + KernelSU ne prend désormais en charge que les noyaux GKI Noyau Empreinte digitale - Statut de SELinux + Mode SELinux Désactivé Permissive Inconnu @@ -16,12 +16,12 @@ Aucun module installé Accueil Appuyez ici pour installer - Non supporté - Échec de la désinstallation : %s + Non pris en charge + Échec de la désinstallation : %s Version Version du gestionnaire Enforcing - Échec de l\'activation du module : %s + Échec de l\'activation du module : %s Modules Désinstaller Installer @@ -29,30 +29,29 @@ Redémarrer Installer Paramètres - Redémarrer vers le bootloader - Redémarrage logiciel - Redémarrer en mode récupération + Redémarrer en mode bootloader + Redémarrage progressif + Redémarrer en mode de récupération Redémarrer en mode EDL À propos - %s désinstallé - Redémarrer en mode téléchargement + %s a été désinstallé + Redémarrer en mode de téléchargement Auteur Êtes-vous sûr(e) de vouloir désinstaller le module %s \? Découvrir KernelSU - OverlayFS n\'est pas disponible, impossible de faire fonctionner le module ! + Les modules sont indisponibles car OverlayFS est désactivé par le noyau. Rafraîchir Afficher les applications système Masquer les applications système Mode sans échec - Envoyer les logs + Envoyer les journaux Redémarrez pour appliquer les modifications - Les modules sont désactivés car ils sont en conflit avec ceux de Magisk ! + Les modules sont indisponibles en raison d\'un conflit avec Magisk ! https://kernelsu.org/guide/what-is-kernelsu.html Soutenez-nous Découvrez comment installer KernelSU et utiliser les modules - KernelSU est et restera toujours gratuit et open source. Vous pouvez cependant nous témoigner de votre soutien en nous faisant un don. - Voir le code source sur %1$s
-\nRejoindre notre canal %2$s
+ KernelSU est, et restera toujours, gratuit et open source. Vous pouvez cependant nous témoigner de votre soutien en nous faisant un don. + Voir le code source sur %1$s
\nRejoindre notre canal %2$s
Modèle Par défaut Personnalisé @@ -66,18 +65,70 @@ Capacités Démonter les modules Échec de la modification du profil d\'application de %s - L\'activation de cette option permettra à KernelSU de restaurer tous les fichiers modifiés par les modules de cette application. + L\'activation de cette option permettra à KernelSU de restaurer tous les fichiers modifiés par les modules pour cette application. Démonter les modules par défaut - Valeur globale par défaut pour « Démonter les modules » dans les profils d\'application. Si l\'option est activée, les modifications apportées au système par les modules seront supprimées pour les applications qui n\'ont pas de profil défini. + Valeur globale par défaut pour l\'option « Démonter les modules » dans les profils d\'application. Lorsqu\'elle est activée, les modifications apportées au système par les modules seront supprimées pour les applications qui n\'ont pas de profil défini. Domaine Règles Mettre à jour Téléchargement du module : %s Lancer - Nouvelle version : la %s est disponible, appuyez ici pour la télécharger + La nouvelle version %s est disponible, appuyez ici pour mettre à jour. Début du téléchargement de : %s Forcer l\'arrêt Relancer l\'application - Échec de la mise à jour des règles de SELinux pour : %s - La version actuelle (%d) de KernelSU est trop ancienne pour que le gestionnaire fonctionne correctement. Veuillez passer à la version %d ou à une version supérieure ! + Échec de la mise à jour des règles SELinux pour : %s + La version actuelle de KernelSU (%d) est trop ancienne pour que le gestionnaire fonctionne correctement. Veuillez passer à la version %d ou à une version supérieure ! + Importation réussie + Exporter vers le presse-papiers + Impossible de trouver un modèle local à exporter ! + L\'ID du modèle existe déjà ! + Journal des modifications + Importer à partir du presse-papiers + Échec de récupération du journal des modifications : %s + Nom + ID de modèle invalide + Synchroniser les modèles en ligne + Créer un modèle + Lecture seule + Importer/exporter + Échec de l\'enregistrement du modèle + Modifier le modèle + ID + Modèles de profils d\'application + Description + Enregistrer + Gérer les modèles de profils d\'application locaux et en ligne + Supprimer + Le presse-papiers est vide ! + Voir le modèle + Vérifier automatiquement les mises à jour à l\'ouverture de l\'application + Vérifier les mises à jour + Activer le débogage WebView + Peut être utilisé pour déboguer WebUI, n\'activez cette option que si nécessaire. + Échec de l\'octroi des privilèges root ! + Ouvrir + Installation directe (recommandé) + Sélectionner un fichier + Installer dans l\'emplacement inactif (après OTA) + Votre appareil sera **FORCÉ** à démarrer sur l\'emplacement inactif actuel après un redémarrage ! +\nN\'utilisez cette option qu\'une fois la mise à jour OTA terminée. +\nContinuer ? + Suivant + L\'image de la partition %1$s est recommandée + Sélectionner une KMI + Minimiser la taille de l\'image creuse + Redimensionne l\'image creuse où le module est situé à sa taille réelle. Notez que cela peut entraîner un dysfonctionnement du module, veuillez utiliser cette option uniquement lorsque cela est nécessaire (par exemple pour la sauvegarde de l\'appareil). + Désinstaller + Désinstaller temporairement + Désinstaller définitivement + Restaurer l\'image d\'origine + Restaurer l\'image d\'origine d\'usine (s\'il en existe une sauvegarde), option généralement utilisée avant une mise à jour OTA ; si vous avez besoin de désinstaller KernelSU, utilisez plutôt l\'option « Désinstaller définitivement ». + Flash en cours + Flash réussi + Échec du flash + LKM sélectionné : %s + Désinstallation complète et permanente de KernelSU (root et tous les modules). + Désinstaller KernelSU temporairement et rétablir l\'état original au redémarrage suivant. + Enregistrer les journaux \ No newline at end of file diff --git a/manager/app/src/main/res/values-gl/strings.xml b/manager/app/src/main/res/values-gl/strings.xml new file mode 100644 index 000000000000..89956f2306d6 --- /dev/null +++ b/manager/app/src/main/res/values-gl/strings.xml @@ -0,0 +1,4 @@ + + + Inicio + \ No newline at end of file diff --git a/manager/app/src/main/res/values-hi/strings.xml b/manager/app/src/main/res/values-hi/strings.xml index 899b327cf265..f930446936f3 100644 --- a/manager/app/src/main/res/values-hi/strings.xml +++ b/manager/app/src/main/res/values-hi/strings.xml @@ -80,4 +80,5 @@ %1$s पर स्रोत कोड देखें
हमारे %2$s चैनल से जुड़ें
मैनेजर वर्जन नया वर्जन: %s उपलब्ध है,अपग्रेड के लिए क्लिक करें + लॉग सहेजें \ No newline at end of file diff --git a/manager/app/src/main/res/values-hr/strings.xml b/manager/app/src/main/res/values-hr/strings.xml index e57b3a201df5..9fb1e4c67460 100644 --- a/manager/app/src/main/res/values-hr/strings.xml +++ b/manager/app/src/main/res/values-hr/strings.xml @@ -79,4 +79,5 @@ Pokrenite Prisilno Zaustavite Resetujte + Spremi Zapise \ No newline at end of file diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index e4e85643f177..765cf9ebca17 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -3,80 +3,132 @@ Működik Verzió: %d Modulok: %d - KernelSU csak GKI kerneleket támogat jelenleg + A KernelSU jelenleg csak GKI kerneleket támogat Kernel - App verziója - Build Fingerprint - Kikapcsolt + Alkalmazás verziója + Ujjlenyomat + Letiltva Újraindítás letöltő módba Újraindítás EDL-be Névjegy - Biztos vagy benne hogy eltávolítod a következő modult: %s\? + Biztos benne hogy eltávolítja a következő modult: %s? Nem sikerült eltávolítani: %s Készítő - Overlayfs nem elérhető, a modul nem tud enélkül működni! - Újratöltés - Mutasd a rendszer alkalmazásokat - Rejtsd el a rendszer alkalmazásokat + A modulok nem érhetők el, mivel az OverlayFS-t a kernel letiltotta. + Frissítés + Rendszeralkalmazások megjelenítése + Rendszeralkalmazások elrejtése Biztonságos mód - A modul letiltva mert ütközik a Magisk verziójával! - Tudj meg többet a KernelSU-ról - Tudd meg hogyan telepítsd a KernelSU-t és használd moduljait - Támogass minket - Tekintsd meg a forráskódot a %1$s-n
Csatlakozz a %2$s csatornánkhoz
+ A modulok nem érhetők el a Magiskkel való ütközés miatt! + Tudjon meg többet a KernelSU-ról + Ismerje meg a KernelSU telepítését és a modulok használatát + Támogasson minket + Tekintse meg a forráskódot a %1$s-on
Csatlakozzon a %2$s csatornánkhoz
Alapértelmezett Sablon Egyedi Profil neve - Mountold a névteret + Névtér csatlakoztatása Örökölt https://kernelsu.org/guide/what-is-kernelsu.html Különálló Csoportok Jogosultságok - SElinux kontextus - Umountold a modulokat alpértelmezés szerint - Ennek az opciónak az engedélyezése lehetővé teszi, hogy a KernelSU visszaállítsa az alkalmazás moduljai által módosított fájlokat. + SELinux kontextus + Modulok leválasztása alapértelmezetten + Ha engedélyezi ezt az opciót, a KernelSU visszaállíthatja az alkalmazás moduljai által módosított fájlokat. Tartomány Szabályok Frissítés - A %s modul letöltése folyamatban - Indítsd el a letöltést: %s + Modul letöltése: %s + Letöltés indítása: %s Indítás Kényszerített leállítás újraindítás Kezdőlap Nincs telepítve - Kattints a telepítéshez + Kattintson a telepítéshez Engedélyezett alkalmazások: %d Nem támogatott - SELinux státusz - Érvényesítés - Megengedő + SELinux állapot + Kényszerített + Engedélyezett Ismeretlen Superuser Nem sikerült engedélyezni a következő modult: %s - Nem sikerült letiltani a következő modulokat: %s - Nincs modul telepítve + Nem sikerült letiltani a következő modult: %s + Nincs telepített modul Modulok Eltávolítás Telepítés Telepítés Újraindítás Beállítások - Android felület újraindítása + Rendszerfelület újraindítása Újraindítás recovery-módba Újraindítás bootloader-módba %s eltávolítva Verzió - Fejlesztői napló küldése - Indítsd újra a készüléket hogy érvényesítsd a változást - A KernelSU ingyenes és nyílt forráskódú és mindig is az lesz. Te viszont meg tudod mutatni azt, hogy törődsz ennek a projektnek a sorsával egy adomány formájában. + Naplók küldése + Indítsa újra a készüléket a változások érvényesítéséhez + A KernelSU ingyenes, nyílt forráskódú és mindig is az lesz. Ön azonban adományozással megmutathatja, hogy törődik a projekttel. Globális - Unmountold a modulokat - Nem sikerült frissíteni az App Profilt ehhez %s - A „Modulok csatlakoztatása” globális alapértelmezett értéke az alkalmazásprofilokban. Ha engedélyezve van, eltávolítja a rendszer összes modul-módosítását azoknál az alkalmazásoknál, amelyeknek nincs beállított profilja. - Új verzió: %s elérhető, kattints a letöltéséhez - Nem sikerült frissíteni a SELinux szabályait a következőhöz: %s - A jelenlegi KernelSU verzió %d túlságosan elavult. Kérlek frissíts a %d verzióra vagy újabbra! + Modulok leválasztása + Nem sikerült frissíteni az App Profilt ehhez: %s + A \"Modulok leválasztása\" globális alapértelmezett értéke az App Profile-ban. Ha engedélyezve van, eltávolít minden modulmódosítást a rendszerből azon alkalmazások esetében, amelyeknek nincs profilja beállítva. + Elérhető az új, %s verzió, kattintson a frissítéshez. + Nem sikerült frissíteni az SELinux szabályokat a következőhöz: %s + A jelenlegi KernelSU verzió %d túlságosan elavult a megfelelő működéshez. Kérjük frissítsen a %d verzióra vagy újabbra! + Sikeresen importálva + Exportálás a vágólapról + Nem található helyi sablon az exportáláshoz! + A sablon ID már létezik! + Változások + Importálás a vágólapról + A változásnapló lekérése nem sikerült: %s + Név + Hibás sablon ID + Online sablonok szinkronizálása + Sablon készítése + Csak olvasható + Import/Export + A sablon mentése sikertelen + Sablon szerkesztése + ID + App Profile sablon + Leírás + Mentés + Az App Profile helyi és online sablonjának kezelése + Törlés + A vágólap üres! + Sablon megtekintése + Naplók mentése + A WebUI hibakeresésére használható, csak szükség esetén engedélyezze. + WebView hibakeresés engedélyezése + Megnyitás + Végleges eltávolítás + %1$s partíció képfájl ajánlott + KMI kiválasztása + Következő + Ideiglenes eltávolítás + A KernelSU ideiglenes eltávolítása, az eredeti állapot visszaállítása a következő újraindítás után. + Eltávolítás + Telepítés + Sikeres telepítés + Kiválasztott LKM: %s + Sikertelen telepítés + A root jog megadása sikertelen! + Telepítés inaktív helyre (OTA után) + Fájl kiválasztása + A KernelSU eltávolítása (root és az összes modul) teljesen és véglegesen. + Eredeti képfájl visszaállítása + Művelet + Közvetlen telepítés (Ajánlott) + Az eszköze **KÉNYSZERÍTETTEN** a jelenleg inaktív helyről fog indulni újraindítás után!\nCsak az OTA befejezése után használja.\nFolytatja? + Átméretezi a sparse képfájlt, ahol a modul található, a tényleges méretére. Vegye figyelembe, hogy ez a modul rendellenes működését okozhatja, ezért kérjük, hogy csak akkor használja, ha szükséges (például biztonsági mentéshez). + Állítsa vissza a gyári képfájlt (ha létezik biztonsági mentés). Általában OTA előtt használják. Ha a KernelSU-t szeretné eltávolítani, használja a végleges eltávolítás opciót. + Frissítés ellenőrzése + Automatikusan keressen frissítéseket az alkalmazás megnyitásakor + Mentett naplók + Sparse képfájl minimalizálása \ No newline at end of file diff --git a/manager/app/src/main/res/values-in/strings.xml b/manager/app/src/main/res/values-in/strings.xml index 8eead48dc813..b2291e9280ba 100644 --- a/manager/app/src/main/res/values-in/strings.xml +++ b/manager/app/src/main/res/values-in/strings.xml @@ -1,84 +1,139 @@ Beranda - Tidak terpasang - Klik untuk memasang - Bekerja + Tidak terinstal + Klik untuk menginstal + Berfungsi Versi: %d - Superusers: %d + SuperUser: %d Modul: %d Tidak didukung - KernelSU hanya mendukung kernel GKI saat ini + KernelSU saat ini hanya mendukung kernel GKI Kernel - Versi Manager - Sidik jari + Versi manager + Identitas Status SELinux - Dinonaktifkan + Nonaktif Enforcing - Permasif - Tidak dikenal - Superuser + Permisif + Tidak diketahui + SuperUser Gagal mengaktifkan modul: %s Gagal menonaktifkan modul: %s Tidak ada modul yang terpasang Modul - Copot - Pasang - Pasang - Mulai ulang + Hapus + Instal + Instal + Reboot Pengaturan Soft Reboot - Mulai ulang ke Recovery - Mulai ulang ke Bootloader - Mulai ulang ke Download - Mulai ulang ke EDL + Reboot ke Recovery + Reboot ke Bootloader + Reboot ke Download + Reboot ke EDL Tentang - Apakah Anda yakin ingin mencopot modul %s? - %s Tercopot - Gagal untuk mencopot: %s + Yakin menghapus modul %s? + %s berhasil dihapus + Gagal menghapus: %s Versi - Pembuat - overlayfs tidak tersedia, modul tidak dapat bekerja! - Segarkan - Tampilkan apl sistem - Sembunyikan apl sistem + Oleh + Kernel tidak mendukung OverlayFS, modul tidak akan berfungsi. + Muat ulang + Tampilkan aplikasi sistem + Sembunyikan aplikasi sistem Kirim Log Mode aman - Mulai ulang untuk menerapkan - Modul dinonaktifkan karena bertentangan dengan Magisk! + Reboot agar berfungsi + Konflik dengan Magisk, fungsi modul ditiadakan! Pelajari KernelSU https://kernelsu.org/id_ID/guide/what-is-kernelsu.html - Pelajari cara memasang KernelSU dan menggunakan modul + Pelajari cara instal KernelSU dan menggunakan modul Dukung Kami - KernelSU gratis dan bersumber terbuka, dan akan selalu seperti itu. Bagaimanapun juga Anda dapat menunjukan kepedulian Anda kepada kami dengan mengirimkan sedikit donasi. - Gabung kanal %2$s kami]]> + KernelSU akan selalu menjadi aplikasi gratis dan terbuka. Anda dapat memberikan donasi sebagai bentuk dukungan. + Gabung kanal %2$s kami]]> Profil Apl Bawaan Templat Khusus Nama profil - Ikat ruang-nama + Mount Namespace Diwariskan Universal - Personal + Individual Kelompok Kemampuan Konteks SELinux - Lepas modul - Gagal memperbarui Profil Apl untuk %s - Lepas modul secara bawaan - Nilai bawaan universal untuk \"Lepas modul\" di Profil-profil Apl. Jika diaktifkan, ini akan menghapus semua modifikasi modul pada sistem untuk aplikasi yang tidak memiliki set Profil. - Mengaktifkan opsi ini akan mengizinkan KernelSU memulihkan file-file yang dimodifikasi oleh modul untuk aplikasi ini. + Umount Modul + Gagal membarui Profil pada %s + 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. + Aktifkan opsi ini agar KernelSU dapat memulihkan kembali berkas termodifikasi oleh modul pada aplikasi ini. Domain Aturan - Perbarui - Mengunduh module: %s + Membarui + Mengunduh modul: %s Mulai mengunduh: %s - Versi baru: %s sudah tersedia, tap untuk mengunduh + Tersedia versi terbaru %s, Klik untuk membarui. Jalankan - Paksa Berhenti + Paksa berhenti Mulai ulang - Gagal memperbarui aturan SELinux untuk: %s - Versi KernelSU saat ini %d terlalu rendah bagi manajer untuk dapat berfungsi dengan baik. Harap tingkatkan ke versi %d atau yang lebih tinggi! + Gagal membarui aturan SELinux pada: %s + Versi KernelSU %d terlalu rendah agar manajer berfungsi normal. Harap membarui ke versi %d atau di atasnya! Catatan Perubahan + Berhasil diimpor + Ekspor ke papan klip + Tidak ditemukan templat lokal untuk diekspor! + ID templat sudah ada! + Impor dari papan klip + Gagal mengambil Changelog: %s + Nama + ID template tidak valid + Sinkronkan templat daring + Buat templat + Impor/Ekspor + Gagal menyimpan templat + Edit templat + ID + Templat Profil Aplikasi + Deskripsi + Simpan + Atur templat Profil yang lokal dan daring + Hapus + Papan klip kosong! + Lihat templat + readonly + Pengawakutuan WebView + Dapat digunakan untuk men-debug WebUI. Harap aktifkan hanya bila diperlukan. + %1$s image partisi terekomendasi + Pilih KMI + Selanjutnya + Gawai akan **DIPAKSA** untuk but ke slot nonaktif! +\nHANYA gunakan setelah proses OTA selesai. +\nLanjutkan? + Instal langsung (rekomendasi) + Pilih berkas + Instal ke slot nonaktif (setelah OTA) + Gagal memberikan akses root! + Buka + Cek terbaru + Cek terbaru setiap membuka aplikasi + Minimalkan ukuran sparse image + Mengembalikan ukuran sparse image, lokasi modul disimpan, ke ukuran sebenarnya. Dapat menyebabkan modul bekerja abnormal, gunakan saat dibutuhkan saja (mis. untuk pencadangan). + Hapus permanen KernelSU (root dan modul). + Hapus sementara + Pulihkan image bawaan + Hapus + Sementara menghapus KernelSU, memulihkan ke kondisi asal setelah reboot berikutnya. + Hapus permanen + Pulihkan image bawaan ROM (jika cadangan tersedia), umumnya dilakukan sebelum OTA; jika ingin menghapus KernelSU, gunakan fungsi \"Hapus permanen\". + Pemasangan Berhasil + LKM dipilih: %s + Pasang + Pemasangan Gagal + Simpan Log + Action + Log disimpan + Urut (Diaktifkan terlebih dahulu) + Urut (Tindakan pertama) \ No newline at end of file diff --git a/manager/app/src/main/res/values-it/strings.xml b/manager/app/src/main/res/values-it/strings.xml index 436023aecead..642640b3cff4 100644 --- a/manager/app/src/main/res/values-it/strings.xml +++ b/manager/app/src/main/res/values-it/strings.xml @@ -5,14 +5,14 @@ Clicca per installare In esecuzione Versione: %d - Superuser: %d - Moduli: %d + Applicazioni con accesso root: %d + Moduli installati: %d Non supportato KernelSU ora supporta solo i kernel GKI Kernel Versione del manager - Fingerprint - Stato SELinux + Impronta della build di Android + Stato di SELinux Disabilitato Enforcing Permissive @@ -21,14 +21,14 @@ Impossibile abilitare il modulo: %s Impossibile disabilitare il modulo: %s Nessun modulo installato - Moduli + Modulo Disinstalla Installa Installa Riavvia Impostazioni - Riavvio veloce - Riavvia in Recovery + Riavvio rapido + Riavvia in modalità Recovery Riavvia in modalità Bootloader Riavvia in modalità Download Riavvia in modalità EDL @@ -38,14 +38,14 @@ Impossibile disinstallare: %s Versione Autore - overlayfs non è disponibile, il modulo non può funzionare! - Aggiorna + overlayfs non è disponibile, i moduli non possono funzionare! + Ricarica Mostra app di sistema Nascondi app di sistema Invia log Modalità provvisoria Riavvia per applicare la modifica - I moduli sono disabilitati perché in conflitto con quelli di Magisk! + I moduli sono disabilitati perché in conflitto con Magisk! Scopri KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Scopri come installare KernelSU e utilizzare i moduli @@ -53,17 +53,17 @@ KernelSU è, e sempre sarà, gratuito e open source. Puoi comunque mostrarci il tuo apprezzamento facendo una donazione. Unisciti al nostro canale %2$s]]> Nome profilo - Namespace di mount + Spazio dei nomi del mount Globale Gruppi Ereditato Individuale Predefinito Personalizzato - Template + Modello Scollega moduli Contesto SELinux - Aggiornamento Profilo per %s fallito + Aggiornamento App Profile per %s fallito Aggiorna Apri Capacità @@ -71,12 +71,64 @@ Regole Sto scaricando il modulo: %s Inizia a scaricare:%s - Nuova versione: %s disponibile, tocca per scaricare + Nuova versione: %s disponibile, tocca per aggiornare Arresto forzato Riavvia Aggiornamento regole SELinux per %s fallito Attivando questa opzione permetterai a KernelSU di ripristinare ogni file modificato dai moduli per questa app. Dominio - Il valore predefinito per \"Scollega moduli\" in Profili App. Se attivato, rimuoverà tutte le modifiche al sistema da parte dei moduli per le applicazioni che non hanno un profilo impostato. + Il valore predefinito per \"Scollega moduli\" in App Profile. Se attivato, rimuoverà tutte le modifiche al sistema da parte dei moduli per le applicazioni che non hanno un profilo impostato. La versione attualmente installata di KernelSU (%d) è troppo vecchia ed il gestore non può funzionare correttamente. Si prega di aggiornare alla versione %d o successiva! + Registro aggiornamenti + Crea modello + Modifica modello + identificatore + Identificativo modello non valido + Nome + Visualizza modello + Sola lettura + L\'identificatore del modello è già in uso! + Importa/Esporta + Importa dagli appunti + Esporta negli appunti + Impossibile trovare un modello locale da esportare! + Importato con successo + Sincronizza i modelli remoti + Gli appunti sono vuoti! + Impossibile ottenere l\'accesso root! + Modelli App Profile + Gestisci i modelli locali e remoti di App Profile + Elimina + Descrizione + Salva + Impossibile salvare il modello + Apri + Impossibile reperire il changelog: %s + Controlla aggiornamenti + Controlla automaticamente la disponibilità di aggiornamenti all\'apertura dell\'applicazione + Abilita il debug di WebView + Può essere usato per svolgere il debug di WebUI, è consigliato attivarlo solo quando necessario. + È consigliato usare immagine della partizione %1$s + Scegli il KMI + Avanti + Installazione diretta (Raccomandata) + Scegli un file + Installa nello slot inattivo (dopo OTA) + Il tuo dispositivo sarà **FORZATO** ad avviarsi nello slot inattivo dopo il riavvio! +\nUsa questa opzione solo quando l\'applicazione dell\'aggiornamento OTA è terminata. +\nProcedere? + Riduci la dimensione dell\'immagine moduli sparse al minimo + Riduci la dimensione dell\'immagine sparse dei moduli alla sua reale dimensione. Nota che questo potrebbe causare malfunzionamenti dei moduli quindi utilizzala solo quando necessario (ad esempio in caso di backup) + Disinstalla + Disinstalla temporaneamente + Disinstalla permanentemente + Ripristina immagine originale del produttore + Disinstalla temporaneamente KernelSU, ripristina lo stato originale dopo il prossimo riavvio. + Disinstalla KernelSU (root e tutti i moduli) completamente e permanentemente. + Installazione + Installazione completata + Installazione fallita + LKM selezionato: %s + Ripristina l\'immagine di fabbrica del produttore (se il backup è presente), solitamente usato prima di applicare l\'OTA; se devi disinstallare KernelSU, utilizza invece \"Disinstalla permanentemente\". + Salva Registri \ No newline at end of file diff --git a/manager/app/src/main/res/values-iw/strings.xml b/manager/app/src/main/res/values-iw/strings.xml index eee70a9bbccf..18cfc638eb9d 100644 --- a/manager/app/src/main/res/values-iw/strings.xml +++ b/manager/app/src/main/res/values-iw/strings.xml @@ -80,4 +80,5 @@ ראה את קוד המקור ב%1$s
הצטרף אלינו %2$s בערוץ
גרסת מנהל גרסה חדשה עבור: %s זמינה, לחץ כדי לשדרג + שמור יומנים \ No newline at end of file diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index 40c4a545a71c..beb0dac0e9c0 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -10,7 +10,7 @@ 非対応 現在、 KernelSU は GKI カーネルにのみ対応しています カーネル - バージョン + アプリのバージョン Fingerprint SELinux の状態 Disabled @@ -20,7 +20,7 @@ スーパーユーザー モジュールの有効化に失敗: %s モジュールの無効化に失敗: %s - モジュールをインストールしていません + モジュールがインストールされていません モジュール アンインストール インストール @@ -31,26 +31,26 @@ リカバリーへ再起動 ブートローダー へ再起動 ダウンロードモードへ再起動 - EDLへ再起動 + EDL へ再起動 アプリについて - モジュール %s をアンインストールしますか? - %sをアンインストールしました + モジュール %s をアンインストールしますか? + %s はアンインストールされました アンインストールに失敗: %s バージョン 制作者 - OverlayFS が有効でないためモジュールは動作しません! + カーネルによって OverlayFS が無効になっているため、モジュールが利用できません。 更新 システムアプリを表示 システムアプリを非表示 ログを送信 セーフモード 再起動すると有効化されます - Magisk と競合しているためモジュールは無効になっています! - KernelSU の詳細 + モジュールが Magisk との競合により利用できません! + KernelSU について https://kernelsu.org/ja_JP/guide/what-is-kernelsu.html KernelSU のインストール方法やモジュールの使い方はこちら 支援する - KernelSU はこれからもずっとフリーでオープンソースです。寄付をすることで私たちを気にかけていることを示せます。 + KernelSU はこれからもずっと無料でオープンソースです。寄付をして頂くことで、開発を支援していただけます。 %2$s チャンネルに参加]]> アプリのプロファイル 既定 @@ -64,12 +64,12 @@ モジュールのアンマウント グループ SELinux コンテキスト - %sのアプリのプロファイルの更新をできませでした + %s のアプリのプロファイルの更新をできませでした ドメイン ルール - 新しいバージョン: %s が利用可能です。タップしてダウンロード + 新しいバージョン %s が利用可能です。タップしてダウンロード。 アップデート - ダウンロードを開始:%s + ダウンロードを開始: %s 起動 強制停止 再起動 @@ -78,7 +78,62 @@ モジュールをダウンロード中: %s このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。 既定でモジュールのマウントを解除 - アプリプロファイルの「モジュールのマウント解除」の共通既定値です。 有効にすると、プロファイル セットを持たないアプリでのシステムに対するすべてのモジュール変更が削除されます。 + アプリプロファイルの「モジュールのアンマウント」の共通のデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。 現在の KernelSU バージョン %d はマネージャーが適切に機能するには低すぎます。 バージョン %d 以降にアップグレードしてください! 変更履歴 + インポート成功 + クリップボードからエクスポート + エクスポートするローカル テンプレートが見つかりません! + テンプレート ID はすでに存在します! + クリップボードからインポート + 変更ログの取得に失敗しました: %s + 名前 + 無効なテンプレート ID + オンラインテンプレートの同期 + テンプレートの作成 + 読み取り専用 + インポート/エクスポート + テンプレートの保存に失敗しました + テンプレートの編集 + ID + アプリプロファイルのテンプレート + 説明 + 保存 + アプリプロファイルのローカルおよびオンラインテンプレートを管理する + 消去 + クリップボードが空です! + テンプレートを表示 + アップデートを確認 + アプリを開いたときにアップデートを自動的に確認する + root の付与に失敗しました! + 開く + WebView デバッグを有効にする + WebUI のデバッグに使用できます。必要な場合にのみ有効にしてください。 + %1$s パーティション イメージが推奨されます + KMI を選択してください + 次に + 非アクティブなスロットにインストール (OTA 後) + 再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。 +\nこのオプションは、OTA が完了した後にのみ使用してください。 +\n続く? + 直接インストール (推奨) + ファイルを選択してください + スパースイメージを最小化 + モジュールが配置されているスパースイメージのサイズを実際のサイズに変更します。 モジュールが正常に動作しなくなる可能性がありますので、必要な場合にのみご使用ください。 + 完全にアンインストールする + ストックイメージを復元 + 一時的にアンインストールする + アンインストール + KernelSU を一時的にアンインストールし、次回の再起動後に元の状態に戻します。 + KernelSU (ルートおよびすべてのモジュール) を完全かつ永久にアンインストールします。 + バックアップが存在する場合、工場出荷時のイメージを復元できます (OTA の前に使用してください)。KernelSU をアンインストールする必要がある場合は、「完全にアンインストールする」を使用してください。 + フラッシュ + フラッシュ成功 + フラッシュ失敗 + 選択された LKM: %s + ログを保存 + アクション + 保存されたログ + 並べ替え(最初に有効) + 並べ替え(アクション優先) \ No newline at end of file diff --git a/manager/app/src/main/res/values-kn/strings.xml b/manager/app/src/main/res/values-kn/strings.xml index 564ceb544c49..3c4e79fe639f 100644 --- a/manager/app/src/main/res/values-kn/strings.xml +++ b/manager/app/src/main/res/values-kn/strings.xml @@ -66,4 +66,5 @@ %1$s ನಲ್ಲಿ ಮೂಲ ಕೋಡ್ ಅನ್ನು ವೀಕ್ಷಿಸಿ
ನಮ್ಮ %2$s ಚಾನಲ್‌ಗೆ ಸೇರಿ
ಮ್ಯಾನೇಜರ್ ವರ್ಷನ್ ಹೊಸ ಆವೃತ್ತಿ: %s ಲಭ್ಯವಿದೆ, ಅಪ್‌ಗ್ರೇಡ್ ಮಾಡಲು ಕ್ಲಿಕ್ ಮಾಡಿ + ಲಾಗ್ಗಳನ್ನು ಉಳಿಸಿ \ No newline at end of file diff --git a/manager/app/src/main/res/values-ko/strings.xml b/manager/app/src/main/res/values-ko/strings.xml index 75de3a0f2c9c..3cd6d90f7702 100644 --- a/manager/app/src/main/res/values-ko/strings.xml +++ b/manager/app/src/main/res/values-ko/strings.xml @@ -38,24 +38,24 @@ 모듈 삭제 실패: %s 버전 제작자 - overlayfs 사용 불가, 모듈을 사용할 수 없습니다! + 커널에서 OverlayFS를 비활성화하여 모듈을 사용할 수 없습니다. 새로고침 시스템 앱 보이기 시스템 앱 숨기기 로그 보내기 안전 모드 다시 시작하여 변경 사항 적용 - Magisk와의 충돌로 인해 모듈을 사용할 수 없습니다! + Magisk와 충돌로 모듈을 사용할 수 없습니다! KernelSU 알아보기 KernelSU 설치 방법과 모듈 사용 방법을 확인합니다 지원이 필요합니다 KernelSU는 지금도, 앞으로도 항상 무료이며 오픈 소스로 유지됩니다. 기부를 통해 여러분의 관심을 보여주세요. %2$s 채널 참가하기]]> https://kernelsu.org/guide/what-is-kernelsu.html - 앱 프로필 메뉴의 \"모듈 사용 해제\" 설정에 대한 전역 기본값을 설정합니다. 활성화 시, 개별 프로필이 설정되지 않은 앱은 시스템에 대한 모듈의 모든 수정사항이 적용되지 않습니다. + 앱 프로필 메뉴의 \"모듈 마운트 해제\" 설정에 대한 전역 기본값을 설정합니다. 활성화 시, 개별 프로필이 설정되지 않은 앱은 시스템에 대한 모듈의 모든 수정사항이 적용되지 않습니다. 다시 시작 규칙 - 새 버전: %s 사용 가능, 여기를 눌러서 받기 + 새 버전: %s이 사용 가능합니다, 여기를 눌러 업그레이드하세요. 다운로드 시작: %s 강제 중지 기본값 @@ -72,10 +72,65 @@ 권한 %s에 대한 앱 프로필 업데이트 실패 기본값으로 모듈 사용 해제 - 이 옵션이 활성화되면, KernelSU는 이 애플리케이션에 대한 모듈의 모든 수정사항을 복구합니다. + 이 옵션이 활성화되면, KernelSU는 이 앱에 대한 모듈의 모든 수정사항을 복구합니다. 업데이트 모듈 받는 중: %s 도메인 실행 다음 앱에 대한 SELinux 규칙 업데이트 실패: %s + 로그 저장 + 업데이트 내역 + WebUI 디버깅에 사용 가능, 필요할 때만 활성화해주세요. + 스파스 이미지 최소화 + 플래시 중 + 선택된 LKM: %s + %1$s 파티션 이미지 권장됨 + KMI 선택 + 다음 + 완전히, 그리고 영구히 KernelSU (루트 및 모든 모듈)를 삭제합니다. + WebView 디버깅 활성화 + 현재 KernelSU 버전 %d는 매니저가 올바르게 작동하기에 너무 낮습니다. 버전 %d 이상으로 업그레이드해 주세요! + 모듈이 위치한 스파스 이미지의 크기를 실제 크기로 조정합니다. 모듈이 비정상적으로 작동할 수 있으니, 필요할 때만 (예: 백업) 사용해 주세요. + 동작 + 임시적 삭제 + 업데이트 내역 가져오기 실패: %s + 열기 + 재부팅 후 기기는 **강제로** 비활성 슬롯으로 부팅합니다!\nOTA를 진행한 후에만 이 옵션을 사용하세요.\n진행할까요? + 플래시 성공 + 플래시 실패 + 삭제 + 영구적 삭제 + 임시적으로 KernelSU를 삭제하고, 다음 재부팅에 원래대로 복구합니다. + 앱 프로필 템플레이트 + 앱 프로필의 로컬 및 온라인 템플레이트 관리 + ID + 올바르지 않은 템플레이트 id + 이름 + 설명 + 저장 + 삭제 + 읽기 전용 + 템플레이트 ID가 이미 존재합니다! + 불러오기/내보내기 + 클립보드에서 불러오기 + 클립보드로 내보내기 + 불러오기 성공 + 온라인 템플레이트 동기화 + 템플레이트 저장 실패 + 클립보드가 비었습니다! + 루트 부여 실패! + 템플레이트 생성 + 템플레이트 편집 + 템플레이트 보기 + 내보낼 로컬 템플레이트가 없습니다! + 파일 선택 + 직접 설치 (권장) + 비활성 슬롯에 설치 (OTA 이후) + 순정 이미지 복구 + 순정 이미지 복구 (백업이 존재한다면), OTA 전에 사용합니다; KernelSU를 삭제해야 한다면, \"영구적 삭제\"를 사용해 주세요. + 업데이트 확인 + 앱 실행시 자동으로 업데이트 확인 + 로그 저장됨 + 정렬 (활성화됨 우선) + 정렬 (동작이 있는 것 우선) \ No newline at end of file diff --git a/manager/app/src/main/res/values-lt/strings.xml b/manager/app/src/main/res/values-lt/strings.xml index a5ba9efca62f..cba5f5b18c25 100644 --- a/manager/app/src/main/res/values-lt/strings.xml +++ b/manager/app/src/main/res/values-lt/strings.xml @@ -80,4 +80,5 @@ Visuotinė numatytoji „Modulių atjungimo“ reikšmė programų profiliuose. Jei įjungta, ji pašalins visus sistemos modulio pakeitimus programoms, kurios neturi profilio. Keitimų žurnalas Ši KernelSU versija %d yra per žema, kad šis vadybininkas galėtų tinkamai funkcionuoti. Prašome atsinaujinti į versiją %d ar aukščiau! + Saglabāt Žurnālus \ No newline at end of file diff --git a/manager/app/src/main/res/values-lv/strings.xml b/manager/app/src/main/res/values-lv/strings.xml new file mode 100644 index 000000000000..c264b76a650d --- /dev/null +++ b/manager/app/src/main/res/values-lv/strings.xml @@ -0,0 +1,134 @@ + + + Iespējojot šo opciju, KernelSU varēs atjaunot visus moduļos šīs lietojumprogrammas modificētos failus. + Neizdevās atjaunināt SELinux noteikumus: %s + Pārvaldiet vietējo un tiešsaistes lietotņu profila veidni + Nederīgs veidnes id + veidnes id jau pastāv! + Eksportēt starpliktuvē + Importēt no starpliktuves + Importēts veiksmīgi + Sinhronizēt tiešsaistes veidnes + Sākums + Nav ieinstalēts + Noklikšķiniet, lai instalētu + Darbojas + Versija: %d + Superlietotāji: %d + Moduļi: %d + Neatbalstīts + KernelSU atbalsta tikai GKI kodolus + Kodols + Pārvaldnieka versija + Pirkstu nospiedums + SELinux statuss + Izpildīšana + Atspējots + Nezināms + SuperLietotājs + Neizdevās atspējot moduli: %s + Nav instalētu moduļu + Moduļi + Atinstalēt + Instalēt + Restartēt + Iestatījumi + Ātri restartēt + Restartēt uz Bootloaderu + Restartēt uz Recovery + Restartēt uz Download + Restartēt uz EDL + Par + %s ir atinstalēts + Neizdevās atinstalēt: %s + Autors + Atjaunot + Rādīt sistēmas lietotnes + Slēpt sistēmas lietotnes + Ziņot žurnālu + Restartējiet, lai stātos spēkā + Uzzināt par KernelSU + https://kernelsu.org/guide/what-is-kernelsu.html + Uzzināt, kā instalēt KernelSU un izmantot moduļus + Atbalsti mūs + Skatiet avota kodu vietnē %1$s
Pievienojies mūsu %2$s kanālam
+ Noklusējums + Veidne + Pielāgots + Profila vārds + Mount nosaukumvieta + Individuāls + Iespējas + SELinux konteksts + Atvienot moduļus + Neizdevās atjaunināt lietotnes profilu %s + Pēc noklusējuma atvienot moduļus + Globālā noklusējuma vērtība vienumam “Atvienot moduļus” lietotņu profilos. Ja tas ir iespējots, lietojumprogrammām, kurām nav iestatīts profils, tiks noņemtas visas sistēmas moduļu modifikācijas. + Domēns + Noteikumi + Atjaunināt + Lejupielādē moduli: %s + Sākt lejupielādi: %s + Jaunā versija: %s ir pieejama, noklikšķiniet, lai atjauninātu + Palaist + Piespiedu apstāšana + Restartēt aplikāciju + Izmaiņu žurnāls + Lietotnes profila veidne + Izveidot veidni + Rediģēt veidni + id + Vārds + Apraksts + Saglabāt + Dzēst + Skatīt veidni + tikai lasīt + Importēt/Eksportēt + Nevar atrast vietējo eksportējamo veidni! + Neizdevās saglabāt veidni + Starpliktuve ir tukša! + Izmaiņu žurnāla iegūšana neizdevās: %s + Visatļautība + Neizdevās iespējot moduli: %s + Instalēt + Vai tiešām vēlaties atinstalēt moduli %s? + Versija + overlayfs nav pieejams, modulis nevar darboties! + Drošais režīms + Moduļi ir atspējoti, jo tie konfliktē ar Magisk! + KernelSU ir un vienmēr būs bezmaksas un atvērtā koda. Tomēr jūs varat parādīt mums, ka jums rūp, veicot ziedojumu. + Grupas + Globāli + Pašreizējā KernelSU versija %d ir pārāk zema, lai pārvaldnieks darbotos pareizi. Lūdzu, atjauniniet uz versiju %d vai jaunāku! + Iespējot WebView atkļūdošanu + Ieteicams %1$s nodalījuma attēls + Nākamais + Mantots + Izvēlieties failu + Instalēt neaktīvajā slotā (pēc OTA) + Pēc restartēšanas jūsu ierīce tiks **PIESPIESTI** palaista pašreizējā neaktīvajā slotā! +\nIzmantojiet šo opciju tikai pēc OTA pabeigšanas +\nTurpināt? + Tiešā instalēšana (Ieteicams) + Atinstalēt + Pagaidu atinstalēšana + Atjaunot oriģinālo attēlu + Īslaicīgi atinstalēt KernelSU, pēc nākamās restartēšanas atjaunot sākotnējo stāvokli. + KernelSU (saknes un visu moduļu) pilnīga atinstalēšana. + Atjaunojot rūpnīcas attēlu (ja ir dublējums), ko parasti izmanto pirms OTA; ja nepieciešams atinstalēt KernelSU, lūdzu, izmantojiet \"Neatgriezeniski atinstalēt\". + Izvēlētais lkm: %s + Neizdevās piešķirt sakni! + Atvērt + Pārbaudīt atjauninājumus + Automātiski pārbaudīt atjauninājumus atverot aplikāciju + Var izmantot WebUI atkļūdošanai, lūdzu, izmantot tikai tad, kad tas ir nepieciešams. + Izvēlieties KMI + Neatgriezeniski atinstalēt + Instalē + Instalēts veiksmīgi + Instalēšana neizdevās + Samazināt reto attēlu + Mainīt retā attēla izmēru, kurā atrodas modulis, līdz tā faktiskajam izmēram. Ņemiet vērā, ka tas var izraisīt moduļa neparastu darbību, tāpēc, lūdzu, izmantojiet tikai nepieciešamības gadījumā (piemēram, dublēšanai) + Išsaugoti Žurnalus +
\ No newline at end of file diff --git a/manager/app/src/main/res/values-mr/strings.xml b/manager/app/src/main/res/values-mr/strings.xml index fad70307d03a..eaaf2a255b44 100644 --- a/manager/app/src/main/res/values-mr/strings.xml +++ b/manager/app/src/main/res/values-mr/strings.xml @@ -78,4 +78,5 @@ सक्तीने थांबा लाँच करा पुन्हा सुरू करा + लॉग जतन करा \ No newline at end of file diff --git a/manager/app/src/main/res/values-ms/strings.xml b/manager/app/src/main/res/values-ms/strings.xml new file mode 100644 index 000000000000..4a8d2ba4c748 --- /dev/null +++ b/manager/app/src/main/res/values-ms/strings.xml @@ -0,0 +1,38 @@ + + + Tidak Diketahui + Lumpuhkan + Permisif + Reboot ke Download + Modul tidak berjaya diaktifkan: %s + Reboot ke EDL + Superusers: %d + Modul: %d + Enforcing + Cap Jari + Reboot ke Recovery + Soft Reboot + Padam + Pasang + Tekan untuk memasang + Modul + Tentang + Versi: %d + Reboot + KernelSU ketika ini hanya menyokong kernel GKI + Status SELinux + Tidak Disokong + Layar Utama + Apakah anda pasti ingin membuang modul %s\? + Superuser + Tetapan + Berjalan + Gagal mematikan modul: %s + Tiada modul dipasang + Pasang + Kernel + Tidak terpasang + Reboot ke Bootloader + Versi manager + Simpan Log + \ No newline at end of file diff --git a/manager/app/src/main/res/values-night/themes.xml b/manager/app/src/main/res/values-night/themes.xml new file mode 100644 index 000000000000..d76ba8e6852b --- /dev/null +++ b/manager/app/src/main/res/values-night/themes.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/manager/app/src/main/res/xml/network_security_config.xml b/manager/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 000000000000..6dd26ccf046a --- /dev/null +++ b/manager/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,8 @@ + + + + 127.0.0.1 + 0.0.0.0 + ::1 + + diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index 030ac9fda115..3bb25ee907cc 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -7,6 +7,7 @@ plugins { alias(libs.plugins.agp.app) apply false alias(libs.plugins.agp.lib) apply false alias(libs.plugins.kotlin) apply false + alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.lsplugin.cmaker) } @@ -14,38 +15,28 @@ cmaker { default { arguments.addAll( arrayOf( - "-DANDROID_STL=c++_static", + "-DANDROID_STL=none", ) ) - val flags = arrayOf( - "-Wno-gnu-string-literal-operator-template", - "-Wno-c++2b-extensions", - ) - cFlags.addAll(flags) - cppFlags.addAll(flags) - abiFilters("arm64-v8a", "x86_64") + abiFilters("arm64-v8a", "x86_64", "riscv64") } buildTypes { if (it.name == "release") { - arguments += "-DDEBUG_SYMBOLS_PATH=${buildDir.absolutePath}/symbols" + arguments += "-DDEBUG_SYMBOLS_PATH=${layout.buildDirectory.asFile.get().absolutePath}/symbols" } } } val androidMinSdkVersion = 26 -val androidTargetSdkVersion = 33 -val androidCompileSdkVersion = 33 -val androidBuildToolsVersion = "33.0.2" -val androidCompileNdkVersion = "25.2.9519653" -val androidSourceCompatibility = JavaVersion.VERSION_17 -val androidTargetCompatibility = JavaVersion.VERSION_17 +val androidTargetSdkVersion = 35 +val androidCompileSdkVersion = 35 +val androidBuildToolsVersion = "35.0.0" +val androidCompileNdkVersion = "27.0.12077973" +val androidSourceCompatibility = JavaVersion.VERSION_21 +val androidTargetCompatibility = JavaVersion.VERSION_21 val managerVersionCode by extra(getVersionCode()) val managerVersionName by extra(getVersionName()) -tasks.register("clean") { - delete(rootProject.buildDir) -} - fun getGitCommitCount(): Int { val out = ByteArrayOutputStream() exec { @@ -88,6 +79,9 @@ subprojects { versionCode = managerVersionCode versionName = managerVersionName } + ndk { + abiFilters += listOf("arm64-v8a", "x86_64", "riscv64") + } } lint { @@ -101,4 +95,4 @@ subprojects { } } } -} +} \ No newline at end of file diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index 12b2f2204903..41384e397b79 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -1,26 +1,38 @@ [versions] -agp = "8.1.0" -kotlin = "1.8.10" -ksp = "1.8.10-1.0.9" -compose-bom = "2023.06.01" -lifecycle = "2.6.1" -accompanist = "0.30.1" -navigation = "2.6.0" -compose-destination = "1.9.42-beta" -libsu = "5.0.5" -sheets-compose-dialogs = "1.2.0" +agp = "8.8.0" +kotlin = "2.1.0" +ksp = "2.1.0-1.0.29" +compose-bom = "2025.01.00" +lifecycle = "2.8.7" +navigation = "2.8.5" +activity-compose = "1.10.0" +kotlinx-coroutines = "1.10.1" +coil-compose = "2.7.0" +compose-destination = "2.1.0-beta15" +sheets-compose-dialogs = "1.3.0" markdown = "4.6.2" +webkit = "1.12.1" +appiconloader-coil = "1.5.0" +parcelablelist = "2.0.1" +libsu = "6.0.0" +apksign = "1.4" +cmaker = "1.2" [plugins] agp-app = { id = "com.android.application", version.ref = "agp" } agp-lib = { id = "com.android.library", version.ref = "agp" } + kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } + ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } -lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version = "1.1" } -lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version = "1.1" } + +lsplugin-apksign = { id = "org.lsposed.lsplugin.apksign", version.ref = "apksign" } +lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version.ref = "cmaker" } [libraries] -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version = "1.7.1" } +androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } + androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } @@ -36,26 +48,27 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } -com-google-accompanist-drawablepainter = { group = "com.google.accompanist", name = "accompanist-drawablepainter", version.ref = "accompanist" } -com-google-accompanist-navigation-animation = { group = "com.google.accompanist", name = "accompanist-navigation-animation", version.ref = "accompanist" } -com-google-accompanist-systemuicontroller = { group = "com.google.accompanist", name = "accompanist-systemuicontroller", version.ref = "accompanist" } +androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } com-github-topjohnwu-libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" } com-github-topjohnwu-libsu-service = { group = "com.github.topjohnwu.libsu", name = "service", version.ref = "libsu" } +com-github-topjohnwu-libsu-io = { group = "com.github.topjohnwu.libsu", name = "io", version.ref = "libsu" } -dev-rikka-rikkax-parcelablelist = { module = "dev.rikka.rikkax.parcelablelist:parcelablelist", version = "2.0.1" } +dev-rikka-rikkax-parcelablelist = { module = "dev.rikka.rikkax.parcelablelist:parcelablelist", version.ref = "parcelablelist" } -io-coil-kt-coil-compose = { group = "io.coil-kt", name = "coil-compose", version = "2.3.0" } +io-coil-kt-coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil-compose" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version = "1.7.1" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } -me-zhanghai-android-appiconloader-coil = { group = "me.zhanghai.android.appiconloader", name = "appiconloader-coil", version = "1.5.0" } +me-zhanghai-android-appiconloader-coil = { group = "me.zhanghai.android.appiconloader", name = "appiconloader-coil", version.ref = "appiconloader-coil" } -compose-destinations-animations-core = { group = "io.github.raamcosta.compose-destinations", name = "animations-core", version.ref = "compose-destination" } +compose-destinations-core = { group = "io.github.raamcosta.compose-destinations", name = "core", version.ref = "compose-destination" } compose-destinations-ksp = { group = "io.github.raamcosta.compose-destinations", name = "ksp", version.ref = "compose-destination" } -sheet-compose-dialogs-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "sheets-compose-dialogs"} -sheet-compose-dialogs-list = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "list", version.ref = "sheets-compose-dialogs"} -sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "input", version.ref = "sheets-compose-dialogs"} +sheet-compose-dialogs-core = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "core", version.ref = "sheets-compose-dialogs" } +sheet-compose-dialogs-list = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "list", version.ref = "sheets-compose-dialogs" } +sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs", name = "input", version.ref = "sheets-compose-dialogs" } + +markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } -markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } \ No newline at end of file +lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" } \ No newline at end of file diff --git a/manager/gradle/wrapper/gradle-wrapper.jar b/manager/gradle/wrapper/gradle-wrapper.jar index 7f93135c49b7..2c3521197d7c 100644 Binary files a/manager/gradle/wrapper/gradle-wrapper.jar and b/manager/gradle/wrapper/gradle-wrapper.jar differ diff --git a/manager/gradle/wrapper/gradle-wrapper.properties b/manager/gradle/wrapper/gradle-wrapper.properties index ac72c34e8acc..df97d72b8b91 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-8.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/manager/gradlew b/manager/gradlew index 0adc8e1a5321..f5feea6d6b11 100755 --- a/manager/gradlew +++ b/manager/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,8 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -145,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -153,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -202,11 +205,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/manager/gradlew.bat b/manager/gradlew.bat index 6689b85beecd..9d21a21834d5 100644 --- a/manager/gradlew.bat +++ b/manager/gradlew.bat @@ -1,92 +1,94 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/manager/settings.gradle.kts b/manager/settings.gradle.kts index b2c940969924..2230bf48671a 100644 --- a/manager/settings.gradle.kts +++ b/manager/settings.gradle.kts @@ -1,15 +1,15 @@ +@file:Suppress("UnstableApiUsage") + enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { - gradlePluginPortal() google() mavenCentral() } } dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { google() mavenCentral() diff --git a/scripts/allowlist.bt b/scripts/allowlist.bt new file mode 100644 index 000000000000..d95bb5ac49a9 --- /dev/null +++ b/scripts/allowlist.bt @@ -0,0 +1,89 @@ +// Define constants as per the provided structure. +#define KSU_MAX_PACKAGE_NAME 256 +#define KSU_MAX_GROUPS 32 +#define KSU_SELINUX_DOMAIN 64 + +// Define the root_profile structure with padding for 64-bit alignment. +struct root_profile { + uint32 uid; + uint32 gid; + + uint32 groups_count; + uint32 groups[KSU_MAX_GROUPS]; + char padding1[4]; // Padding for 64-bit alignment. + + struct { + uint64 effective; + uint64 permitted; + uint64 inheritable; + } capabilities; + + char selinux_domain[KSU_SELINUX_DOMAIN]; + + uint32 namespaces; + char padding2[4]; // Padding for 64-bit alignment. +}; + +// Define the non_root_profile structure with padding for 64-bit alignment. +struct non_root_profile { + byte umount_modules; + char padding[7]; // Padding to make the total size a multiple of 8. +}; + +// Define the rp_config structure with padding for 64-bit alignment. +struct rp_config_t { + byte use_default; + + char template_name[KSU_MAX_PACKAGE_NAME]; + char padding[7]; // Padding to make the total size a multiple of 8. + + struct root_profile profile; +}; + +// Define the nrp_config structure with padding for 64-bit alignment. +struct nrp_config_t { + byte use_default; + char padding1[7]; // Padding to make the total size a multiple of 8. + + struct non_root_profile profile; + char padding2[488]; // Padding to align the union +}; + +// Define the main app_profile structure +typedef struct { + uint32 version; + char key[KSU_MAX_PACKAGE_NAME]; + int32 current_uid; + int64 allow_su; + + // Based on allow_su, decide which profile to use + if (allow_su != 0) { + rp_config_t rp_config; + } else { + nrp_config_t nrp_config; + } + +} app_profile; + +// Define the file header with magic number and version +typedef struct { + uint32 magic; + uint32 version; +} file_header; + +// Main entry for parsing the file +file_header header; + +if (header.magic != 0x7f4b5355) { + Printf("Invalid file magic number.\n"); + return; +} + +FSeek(8); // Skip the header + + +// Continually read app_profile instances until end of file +while (!FEof()) { + app_profile profile; +} + diff --git a/scripts/ksubot.py b/scripts/ksubot.py index 2839de7f62df..c3b3672772ed 100644 --- a/scripts/ksubot.py +++ b/scripts/ksubot.py @@ -1,101 +1,101 @@ -import os -import sys -import asyncio -import telegram -from telegram import helpers - - -BOT_TOKEN = os.environ.get("BOT_TOKEN") -CHAT_ID = os.environ.get("CHAT_ID") -CACHE_CHAT_ID = os.environ.get("CACHE_CHAT_ID") -MESSAGE_THREAD_ID = os.environ.get("MESSAGE_THREAD_ID") -COMMIT_URL = os.environ.get("COMMIT_URL") -COMMIT_MESSAGE = os.environ.get("COMMIT_MESSAGE") -RUN_URL = os.environ.get("RUN_URL") -TITLE = os.environ.get("TITLE") -VERSION = os.environ.get("VERSION") -MSG_TEMPLATE = """ -*{title}* -\#ci\_{version} -``` -{commit_message} -``` -[Commit]({commit_url}) -[Workflow run]({run_url}) -""".strip() - - -def get_caption(): - msg = MSG_TEMPLATE.format( - title=helpers.escape_markdown(TITLE, 2), - version=helpers.escape_markdown(VERSION, 2), - commit_message=helpers.escape_markdown(COMMIT_MESSAGE, 2, telegram.MessageEntity.PRE), - commit_url=helpers.escape_markdown(COMMIT_URL, 2, telegram.MessageEntity.TEXT_LINK), - run_url=helpers.escape_markdown(RUN_URL, 2, telegram.MessageEntity.TEXT_LINK) - ) - if len(msg) > telegram.constants.MessageLimit.CAPTION_LENGTH: - return COMMIT_URL - return msg - - -def check_environ(): - if BOT_TOKEN is None: - print("[-] Invalid BOT_TOKEN") - exit(1) - if CHAT_ID is None: - print("[-] Invalid CHAT_ID") - exit(1) - if CACHE_CHAT_ID is None: - print("[-] Invalid CACHE_CHAT_ID") - exit(1) - if COMMIT_URL is None: - print("[-] Invalid COMMIT_URL") - exit(1) - if COMMIT_MESSAGE is None: - print("[-] Invalid COMMIT_MESSAGE") - exit(1) - if RUN_URL is None: - print("[-] Invalid RUN_URL") - exit(1) - if TITLE is None: - print("[-] Invalid TITLE") - exit(1) - if VERSION is None: - print("[-] Invalid VERSION") - exit(1) - - -async def main(): - print("[+] Uploading to telegram") - check_environ() - print("[+] Files:", sys.argv[1:]) - bot = telegram.Bot(BOT_TOKEN) - files = [] - paths = sys.argv[1:] - caption = get_caption() - print("[+] Caption: ") - print("---") - print(caption) - print("---") - for one in paths: - if not os.path.exists(one): - print("[-] File not exist: " + one) - continue - print("[+] Upload: " + one) - msg = await bot.send_document(CACHE_CHAT_ID, one, write_timeout=60, connect_timeout=30) - if one == paths[-1]: - files.append(telegram.InputMediaDocument(msg.document, - caption=caption, - parse_mode=telegram.constants.ParseMode.MARKDOWN_V2)) - else: - files.append(telegram.InputMediaDocument(msg.document)) - await bot.delete_message(CACHE_CHAT_ID, msg.message_id) - print("[+] Sending") - await bot.send_media_group(CHAT_ID, files, message_thread_id=MESSAGE_THREAD_ID) - print("[+] Done!") - - -if __name__ == "__main__": - loops = asyncio.new_event_loop() - loops.run_until_complete(asyncio.wait([main()])) - +import asyncio +import os +import sys +from telethon import TelegramClient +from telethon.tl.functions.help import GetConfigRequest + +API_ID = 611335 +API_HASH = "d524b414d21f4d37f08684c1df41ac9c" + + +BOT_TOKEN = os.environ.get("BOT_TOKEN") +CHAT_ID = os.environ.get("CHAT_ID") +MESSAGE_THREAD_ID = os.environ.get("MESSAGE_THREAD_ID") +COMMIT_URL = os.environ.get("COMMIT_URL") +COMMIT_MESSAGE = os.environ.get("COMMIT_MESSAGE") +RUN_URL = os.environ.get("RUN_URL") +TITLE = os.environ.get("TITLE") +VERSION = os.environ.get("VERSION") +MSG_TEMPLATE = """ +**{title}** +#ci_{version} +``` +{commit_message} +``` +[Commit]({commit_url}) +[Workflow run]({run_url}) +""".strip() + + +def get_caption(): + msg = MSG_TEMPLATE.format( + title=TITLE, + version=VERSION, + commit_message=COMMIT_MESSAGE, + commit_url=COMMIT_URL, + run_url=RUN_URL, + ) + if len(msg) > 1024: + return COMMIT_URL + return msg + + +def check_environ(): + global CHAT_ID, MESSAGE_THREAD_ID + if BOT_TOKEN is None: + print("[-] Invalid BOT_TOKEN") + exit(1) + if CHAT_ID is None: + print("[-] Invalid CHAT_ID") + exit(1) + else: + CHAT_ID = int(CHAT_ID) + if COMMIT_URL is None: + print("[-] Invalid COMMIT_URL") + exit(1) + if COMMIT_MESSAGE is None: + print("[-] Invalid COMMIT_MESSAGE") + exit(1) + if RUN_URL is None: + print("[-] Invalid RUN_URL") + exit(1) + if TITLE is None: + print("[-] Invalid TITLE") + exit(1) + if VERSION is None: + print("[-] Invalid VERSION") + exit(1) + if MESSAGE_THREAD_ID is None: + print("[-] Invaild MESSAGE_THREAD_ID") + exit(1) + else: + MESSAGE_THREAD_ID = int(MESSAGE_THREAD_ID) + + +async def main(): + print("[+] Uploading to telegram") + check_environ() + files = sys.argv[1:] + print("[+] Files:", files) + if len(files) <= 0: + print("[-] No files to upload") + exit(1) + print("[+] Logging in Telegram with bot") + script_dir = os.path.dirname(os.path.abspath(sys.argv[0])) + session_dir = os.path.join(script_dir, "ksubot") + async with await TelegramClient(session=session_dir, api_id=API_ID, api_hash=API_HASH).start(bot_token=BOT_TOKEN) as bot: + caption = [""] * len(files) + caption[-1] = get_caption() + print("[+] Caption: ") + print("---") + print(caption) + print("---") + print("[+] Sending") + await bot.send_file(entity=CHAT_ID, file=files, caption=caption, reply_to=MESSAGE_THREAD_ID, parse_mode="markdown") + print("[+] Done!") + +if __name__ == "__main__": + try: + asyncio.run(main()) + except Exception as e: + print(f"[-] An error occurred: {e}") diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock index 9bc59ccc88e8..906b44f2a3ba 100644 --- a/userspace/ksud/Cargo.lock +++ b/userspace/ksud/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "addr2line" -version = "0.19.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "adler32" @@ -24,25 +24,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" [[package]] -name = "aes" -version = "0.7.5" +name = "ahash" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", - "opaque-debug", + "cfg-if 1.0.0", + "once_cell", + "version_check", + "zerocopy", ] [[package]] -name = "aho-corasick" -version = "0.7.20" +name = "allocator-api2" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" -dependencies = [ - "memchr", -] +checksum = "45862d1c77f2228b9e10bc609d5bc203d86ebc9b87ad8d5d5167a6c9abf739d9" [[package]] name = "android-properties" @@ -50,22 +47,27 @@ 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.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f0fc03f560e1aebde41c2398b691cb98b5ea5996a6184a7a67bbbb77448969" +checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" [[package]] name = "android_logger" -version = "0.13.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d70a974711fabdc7555de36765bbc237f093f5992d62cab3dcaa0b0bfc7d756" +checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" dependencies = [ "android_log-sys", - "env_logger", + "env_filter", "log", - "once_cell", ] [[package]] @@ -77,140 +79,155 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +dependencies = [ + "anstyle", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" -version = "1.0.68" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775" + +[[package]] +name = "arbitrary" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] [[package]] name = "async-trait" -version = "0.1.73" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.38", + "syn", ] [[package]] name = "autocfg" -version = "1.1.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", - "cfg-if", + "cfg-if 1.0.0", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets", ] [[package]] -name = "base64ct" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" - -[[package]] -name = "bindgen" -version = "0.60.1" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "062dddbc1ba4aca46de6338e2bf87771414c335f7b2f2036e8f3e9befebf88e6" -dependencies = [ - "bitflags", - "cexpr", - "clang-sys", - "lazy_static", - "lazycell", - "peeking_take_while", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", -] +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "1.3.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "byteorder" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" - -[[package]] -name = "bytes" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" - -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] -name = "bzip2-sys" -version = "0.1.11+1.0.8" +name = "bytes" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" -dependencies = [ - "cc", - "libc", - "pkg-config", -] +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" -version = "1.0.79" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" dependencies = [ - "jobserver", + "shlex", ] [[package]] -name = "cexpr" -version = "0.6.0" +name = "cfg-if" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" @@ -220,97 +237,78 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" dependencies = [ + "android-tzdata", "iana-time-zone", - "num-integer", + "js-sys", "num-traits", - "winapi", -] - -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", + "wasm-bindgen", + "windows-targets", ] [[package]] -name = "clang-sys" -version = "1.4.0" +name = "clap" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" dependencies = [ - "glob", - "libc", - "libloading", + "clap_builder", + "clap_derive", ] [[package]] -name = "clap" -version = "4.1.4" +name = "clap_builder" +version = "4.5.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f13b9c79b5d1dd500d20ef541215a6423c75829ef43117e1b4d17fd8af0b5d76" +checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" dependencies = [ - "bitflags", - "clap_derive", + "anstream", + "anstyle", "clap_lex", - "is-terminal", - "once_cell", "strsim", - "termcolor", ] [[package]] name = "clap_derive" -version = "4.1.0" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684a277d672e91966334af371f1a7b5833f9aa00b07c84e92fbce95e00208ce8" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck", - "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.107", + "syn", ] [[package]] name = "clap_lex" -version = "0.3.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "783fe232adfca04f90f56201b26d79682d4cd2625e0bc7290b95123afe558ade" -dependencies = [ - "os_str_bytes", -] +checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "colorchoice" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" -dependencies = [ - "termcolor", - "unicode-width", -] +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "const_format" -version = "0.2.30" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7309d9b4d3d2c0641e018d449232f2e28f1b22933c137f157d3dbc14228b8c0e" +checksum = "50c655d81ff1114fb0dcdea9225ea9f0cc712a6f8d189378e82bdf62a473a64b" dependencies = [ "const_format_proc_macros", ] [[package]] name = "const_format_proc_macros" -version = "0.2.29" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f47bf7270cf70d370f8f98c1abb6d2d4cf60a6845d30e05bfb90c6568650" +checksum = "eff1a44b93f47b1bac19a27932f5c591e43d1ba357ee4f61526c8a25603f0eb1" dependencies = [ "proc-macro2", "quote", @@ -318,42 +316,59 @@ dependencies = [ ] [[package]] -name = "constant_time_eq" -version = "0.1.5" +name = "core-foundation-sys" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "core-foundation-sys" -version = "0.8.3" +name = "core2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "0ca741a962e1b0bff6d724a1a0958b686406e853bb14061f218562e1896f95e6" dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] name = "crossbeam" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" dependencies = [ - "cfg-if", "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", @@ -363,56 +378,46 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.6" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" -version = "0.8.2" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" dependencies = [ - "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" -version = "0.9.13" +version = "0.9.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" dependencies = [ - "autocfg", - "cfg-if", "crossbeam-utils", - "memoffset", - "scopeguard", ] [[package]] name = "crossbeam-queue" -version = "0.3.8" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.14" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" -dependencies = [ - "cfg-if", -] +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "crypto-common" @@ -425,163 +430,128 @@ dependencies = [ ] [[package]] -name = "cxx" -version = "1.0.89" +name = "dary_heap" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc831ee6a32dd495436e317595e639a587aa9907bef96fe6e6abc290ab6204e9" -dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", -] +checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" [[package]] -name = "cxx-build" -version = "1.0.89" +name = "deflate64" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94331d54f1b1a8895cd81049f7eaaaef9d05a7dcb4d1fd08bf3ff0806246789d" -dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 1.0.107", -] +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" [[package]] -name = "cxxbridge-flags" -version = "1.0.89" +name = "deranged" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dcd35ba14ca9b40d6e4b4b39961f23d835dbb8eed74565ded361d93e1feb8a" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] [[package]] -name = "cxxbridge-macro" -version = "1.0.89" +name = "derive-new" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bbeb29798b407ccd82a3324ade1a7286e0d29851475990b612670f6f5124d2" +checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn", ] [[package]] -name = "derive-new" -version = "0.5.9" +name = "derive_arbitrary" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3418329ca0ad70234b9735dc4ceed10af4df60eff9c8e7b06cb5e520d92c3535" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn", ] [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", - "subtle", -] - -[[package]] -name = "either" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" - -[[package]] -name = "encoding" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" -dependencies = [ - "encoding-index-japanese", - "encoding-index-korean", - "encoding-index-simpchinese", - "encoding-index-singlebyte", - "encoding-index-tradchinese", ] [[package]] -name = "encoding-index-japanese" -version = "1.20141219.5" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04e8b2ff42e9a05335dbf8b5c6f7567e5591d0d916ccef4e0b1710d32a0d0c91" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "encoding_index_tests", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "encoding-index-korean" -version = "1.20141219.5" +name = "either" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dc33fb8e6bcba213fe2f14275f0963fd16f0a02c878e3095ecfdf5bee529d81" -dependencies = [ - "encoding_index_tests", -] +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] -name = "encoding-index-simpchinese" -version = "1.20141219.5" +name = "encoding_rs" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87a7194909b9118fc707194baa434a4e3b0fb6a5a757c73c3adb07aa25031f7" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "encoding_index_tests", + "cfg-if 1.0.0", ] [[package]] -name = "encoding-index-singlebyte" -version = "1.20141219.5" +name = "env_filter" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3351d5acffb224af9ca265f435b859c7c01537c0849754d3db3fdf2bfe2ae84a" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ - "encoding_index_tests", + "log", ] [[package]] -name = "encoding-index-tradchinese" -version = "1.20141219.5" +name = "env_logger" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd0e20d5688ce3cab59eb3ef3a2083a5c77bf496cb798dc6fcdb75f323890c18" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ - "encoding_index_tests", + "env_filter", + "log", ] [[package]] -name = "encoding_index_tests" -version = "0.1.4" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246d82be1c9d791c5dfde9a2bd045fc3cbba3fa2b11ad558f27d01712f00569" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "env_logger" -version = "0.10.0" +name = "errno" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", + "errno-dragonfly", + "libc", + "winapi", ] [[package]] name = "errno" -version = "0.2.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ - "errno-dragonfly", "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -600,16 +570,22 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b59f8a77817ff1b795adafc535941bdf664184f5f95e0b6d1d77dd6d12815dc" dependencies = [ - "bitflags", - "errno", + "bitflags 1.3.2", + "errno 0.2.8", "libc", ] +[[package]] +name = "fastrand" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" + [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -617,9 +593,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -636,41 +612,42 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "gimli" -version = "0.27.3" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] -name = "glob" -version = "0.3.1" +name = "hashbrown" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", +] [[package]] -name = "heck" -version = "0.4.1" +name = "hashbrown" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] -name = "hermit-abi" -version = "0.2.6" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hex" @@ -679,12 +656,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] -name = "hmac" -version = "0.12.1" +name = "hole-punch" +version = "0.0.4-alpha.0" +source = "git+https://github.com/tiann/hole-punch#11ab7a61bfb98682b72fd7f58a47d8e5d997328e" +dependencies = [ + "cfg-if 0.1.10", + "errno 0.2.8", + "libc", + "memmap", + "thiserror", + "winapi", +] + +[[package]] +name = "home" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "digest", + "windows-sys 0.52.0", ] [[package]] @@ -696,132 +686,98 @@ dependencies = [ "libm", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "iana-time-zone" -version = "0.1.53" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "winapi", + "windows-core", ] [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] name = "include-flate" -version = "0.1.4" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfdcb449c721557c1cf89bbd3412bf33fa963289e26e9badbd824a960912e148" +checksum = "df49c16750695486c1f34de05da5b7438096156466e7f76c38fcdf285cf0113e" dependencies = [ - "include-flate-codegen-exports", + "include-flate-codegen", "lazy_static", "libflate", ] [[package]] name = "include-flate-codegen" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a7d6e1419fa3129eb0802b4c99603c0d425c79fb5d76191d5a20d0ab0d664e8" +checksum = "8c5b246c6261be723b85c61ecf87804e8ea4a35cb68be0ff282ed84b95ffe7d7" dependencies = [ "libflate", - "proc-macro-hack", "proc-macro2", "quote", - "syn 1.0.107", + "syn", ] [[package]] -name = "include-flate-codegen-exports" -version = "0.1.4" +name = "indexmap" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75657043ffe3d8280f1cb8aef0f505532b392ed7758e0baeac22edadcee31a03" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ - "include-flate-codegen", - "proc-macro-hack", + "equivalent", + "hashbrown 0.15.0", ] [[package]] -name = "io-lifetimes" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1abeb7a0dd0f8181267ff8adc397075586500b81b28a73e8a0208b00fc170fb3" -dependencies = [ - "libc", - "windows-sys 0.45.0", -] - -[[package]] -name = "is-terminal" -version = "0.4.2" +name = "is_executable" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" dependencies = [ - "hermit-abi", - "io-lifetimes", - "rustix", - "windows-sys 0.42.0", + "winapi", ] [[package]] -name = "is_executable" -version = "1.0.1" +name = "is_terminal_polyfill" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" -dependencies = [ - "winapi", -] +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itoa" -version = "1.0.5" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" [[package]] name = "java-properties" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1904d8654a1ef51034d02d5a9411b50bf91bea15b0ab644ae179d1325976263" +version = "2.0.0" +source = "git+https://github.com/Kernel-SU/java-properties.git?branch=master#42a4aa941b70ded2dd3be9e9f892471023e70229" dependencies = [ - "encoding", + "encoding_rs", "lazy_static", - "regex", -] - -[[package]] -name = "jobserver" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" -dependencies = [ - "libc", + "regex-lite", ] [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -843,134 +799,131 @@ dependencies = [ "android-properties", "android_logger", "anyhow", + "chrono", "clap", "const_format", "derive-new", - "encoding", + "encoding_rs", "env_logger", "extattr", "getopts", + "hole-punch", "humansize", "is_executable", "java-properties", "jwalk", "libc", "log", + "loopdev", "nom", "procfs", - "regex", + "regex-lite", "retry", "rust-embed", + "rustix 0.38.34", "serde", "serde_json", + "sha1", "sha256", - "sys-mount", + "tempfile", "which", - "zip 0.6.4", + "zip", "zip-extensions", ] [[package]] name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "lazycell" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" [[package]] name = "libflate" -version = "1.2.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05605ab2bce11bcfc0e9c635ff29ef8b2ea83f29be257ee7d730cac3ee373093" +checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" dependencies = [ "adler32", + "core2", "crc32fast", + "dary_heap", "libflate_lz77", ] [[package]] name = "libflate_lz77" -version = "1.1.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a734c0493409afcd49deee13c006a04e3586b9761a03543c6272c9c51f2f5a" +checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" dependencies = [ + "core2", + "hashbrown 0.14.5", "rle-decode-fast", ] -[[package]] -name = "libloading" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" -dependencies = [ - "cfg-if", - "winapi", -] - [[package]] name = "libm" -version = "0.2.6" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] -name = "link-cplusplus" -version = "1.0.8" +name = "linux-raw-sys" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] -name = "linux-raw-sys" -version = "0.1.4" +name = "lockfree-object-pool" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" [[package]] name = "log" -version = "0.4.17" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "loopdev" version = "0.5.0" -source = "git+https://github.com/tiann/loopdev?branch=loopfix#b6ca5e3ea163f66239f6a835874fe231b2a9286f" +source = "git+https://github.com/Kernel-SU/loopdev#7a921f8d966477a645b1188732fac486c71a68ef" dependencies = [ - "bindgen", - "errno", + "errno 0.2.8", "libc", ] +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + [[package]] name = "memchr" -version = "2.5.0" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] -name = "memoffset" -version = "0.7.1" +name = "memmap" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "6585fd95e7bb50d6cc31e20d4cf9afb4e2ba16c5846fc76793f11218da9c475b" dependencies = [ - "autocfg", + "libc", + "winapi", ] [[package]] @@ -981,11 +934,11 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] @@ -999,167 +952,95 @@ dependencies = [ ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" -version = "0.30.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "once_cell" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - -[[package]] -name = "password-hash" -version = "0.4.2" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core", - "subtle", -] - -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest", - "hmac", - "password-hash", - "sha2", -] - -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] -name = "pkg-config" -version = "0.3.26" +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.107", - "version_check", + "zerocopy", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "proc-macro2" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "307e3004becf10f5a6e0d59d20f3cd28231b0e0827a96cd3e0ce6d14bc1e4bb3" dependencies = [ - "proc-macro2", - "quote", - "version_check", + "unicode-ident", ] [[package]] -name = "proc-macro-hack" -version = "0.5.20+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" - -[[package]] -name = "proc-macro2" -version = "1.0.69" +name = "procfs" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ - "unicode-ident", + "bitflags 2.6.0", + "chrono", + "flate2", + "hex", + "procfs-core", + "rustix 0.38.41", ] [[package]] -name = "procfs" -version = "0.15.1" +name = "procfs-core" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ca7f9f29bab5844ecd8fdb3992c5969b6622bb9609b9502fef9b4310e3f1f" +checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags", - "byteorder", + "bitflags 2.6.0", "chrono", - "flate2", "hex", - "lazy_static", - "rustix", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -1196,9 +1077,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.6.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -1206,32 +1087,19 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.10.2" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" dependencies = [ - "crossbeam-channel", "crossbeam-deque", "crossbeam-utils", - "num_cpus", ] [[package]] -name = "regex" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.6.28" +name = "regex-lite" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" [[package]] name = "retry" @@ -1250,9 +1118,9 @@ checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" [[package]] name = "rust-embed" -version = "6.4.2" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "283ffe2f866869428c92e0d61c2f35dfb4355293cdfdc48f49e895c15f1333d1" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" dependencies = [ "include-flate", "rust-embed-impl", @@ -1262,22 +1130,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "6.3.1" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31ab23d42d71fb9be1b643fe6765d292c5e14d46912d13f3ae2815ca048ea04d" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 1.0.107", + "syn", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "7.3.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1669d81dfabd1b5f8e2856b8bbe146c6192b0ba22162edc738ac0a5de18f054" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" dependencies = [ "sha2", "walkdir", @@ -1285,35 +1153,42 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +name = "rustix" +version = "0.38.34" +source = "git+https://github.com/Kernel-SU/rustix.git?branch=main#4a53fbc7cb7a07cabe87125cc21dbc27db316259" +dependencies = [ + "bitflags 2.6.0", + "errno 0.3.9", + "itoa", + "libc", + "linux-raw-sys", + "once_cell", + "windows-sys 0.52.0", +] [[package]] name = "rustix" -version = "0.36.7" +version = "0.38.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" +checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" dependencies = [ - "bitflags", - "errno", - "io-lifetimes", + "bitflags 2.6.0", + "errno 0.3.9", "libc", "linux-raw-sys", - "windows-sys 0.42.0", + "windows-sys 0.52.0", ] [[package]] name = "ryu" -version = "1.0.12" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -1325,61 +1200,64 @@ dependencies = [ ] [[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "scratch" -version = "1.0.3" +name = "serde" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f" +dependencies = [ + "serde_derive", +] [[package]] -name = "serde" -version = "1.0.152" +name = "serde_derive" +version = "1.0.215" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "serde_json" -version = "1.0.91" +version = "1.0.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest", ] [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest", ] [[package]] name = "sha256" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7895c8ae88588ccead14ff438b939b0c569cd619116f14b4d13fdff7b8333386" +checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" dependencies = [ "async-trait", "bytes", @@ -1390,49 +1268,27 @@ dependencies = [ [[package]] name = "shlex" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] -name = "smart-default" -version = "0.6.0" +name = "simd-adler32" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "133659a15339456eeeb07572eb02a91c91e9815e9cbc89566944d2c8d3efdbf6" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "syn" -version = "1.0.107" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.38" +version = "2.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" dependencies = [ "proc-macro2", "quote", @@ -1440,165 +1296,114 @@ dependencies = [ ] [[package]] -name = "sys-mount" -version = "2.0.2" -source = "git+https://github.com/tiann/sys-mount?branch=loopfix#c7c4048e4a4ffdf8b108a85956363a75f2c554f0" -dependencies = [ - "bitflags", - "libc", - "loopdev", - "smart-default", - "thiserror", - "tracing", -] - -[[package]] -name = "termcolor" -version = "1.2.0" +name = "tempfile" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" dependencies = [ - "winapi-util", + "cfg-if 1.0.0", + "fastrand", + "once_cell", + "rustix 0.38.41", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", -] - -[[package]] -name = "time" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", + "syn", ] [[package]] name = "time" -version = "0.3.20" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ + "deranged", + "num-conv", + "powerfmt", "serde", "time-core", ] [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "tokio" -version = "1.29.1" +version = "1.41.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "22cfb5bee7a6a52939ca9224d6ac897bb669134078daa8735560897f69de4d33" dependencies = [ - "autocfg", "backtrace", "bytes", "pin-project-lite", ] -[[package]] -name = "tracing" -version = "0.1.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.107", -] - -[[package]] -name = "tracing-core" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" -dependencies = [ - "once_cell", -] - [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", - "winapi", "winapi-util", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1607,34 +1412,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.107", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1642,32 +1448,33 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "which" -version = "4.4.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" dependencies = [ "either", - "libc", - "once_cell", + "home", + "rustix 0.38.41", + "winsafe", ] [[package]] @@ -1688,11 +1495,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1701,39 +1508,43 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" -version = "0.42.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.45.0" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", + "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", @@ -1742,115 +1553,118 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "zip" -version = "0.5.13" +name = "winsafe" +version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" -dependencies = [ - "byteorder", - "bzip2", - "crc32fast", - "flate2", - "thiserror", - "time 0.1.45", -] +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" [[package]] -name = "zip" -version = "0.6.4" +name = "zerocopy" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0445d0fbc924bb93539b4316c11afb121ea39296f99a3c4c9edad09e3658cdef" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ - "aes", "byteorder", - "bzip2", - "constant_time_eq", - "crc32fast", - "crossbeam-utils", - "flate2", - "hmac", - "pbkdf2", - "sha1", - "time 0.3.20", - "zstd", + "zerocopy-derive", ] [[package]] -name = "zip-extensions" -version = "0.6.1" +name = "zerocopy-derive" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a64c3c977bc3434ce2d4bcea8ad3c644672de0f2c402b72b9171ca80a8885d14" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ - "zip 0.5.13", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" +name = "zip" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +checksum = "dc5e4288ea4057ae23afc69a4472434a87a2495cafce6632fd1c4ec9f5cf3494" dependencies = [ - "zstd-safe", + "arbitrary", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "indexmap", + "lzma-rs", + "memchr", + "thiserror", + "time", + "zopfli", ] [[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" +name = "zip-extensions" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +checksum = "386508a00aae1d8218b9252a41f59bba739ccee3f8e420bb90bcb1c30d960d4a" dependencies = [ - "libc", - "zstd-sys", + "zip", ] [[package]] -name = "zstd-sys" -version = "2.0.7+zstd.1.5.4" +name = "zopfli" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94509c3ba2fe55294d752b79842c530ccfab760192521df74a081a78d2b3c7f5" +checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" dependencies = [ - "cc", - "libc", - "pkg-config", + "bumpalo", + "crc32fast", + "lockfree-object-pool", + "log", + "once_cell", + "simd-adler32", ] diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index a6e102d9ac3c..33786366b270 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -2,49 +2,62 @@ name = "ksud" version = "0.1.0" edition = "2021" -rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -anyhow = "1.0.68" -clap = { version = "4.0.32", features = ["derive"] } -const_format = "0.2.30" -zip = "0.6.3" -zip-extensions = "0.6.1" -java-properties = "1.4.1" -log = "0.4.17" -env_logger = "0.10.0" +anyhow = "1.0" +clap = { version = "4.5", features = ["derive"] } +const_format = "0.2" +zip = { version = "2.2", default-features = false } +zip-extensions = { version = "0.8", features = [ + "deflate", + "deflate64", + "time", + "lzma", + "xz", +], default-features = false } +java-properties = { git = "https://github.com/Kernel-SU/java-properties.git", branch = "master", default-features = false } +log = "0.4" +env_logger = { version = "0.11", default-features = false } serde = { version = "1.0" } serde_json = "1.0" -regex = "1.5.4" -encoding = "0.2.33" -retry = "2.0.0" -humansize = "2.0.0" +encoding_rs = "0.8" +retry = "2.0" +humansize = "2.1" libc = "0.2" -extattr = "1.0.0" -jwalk = "0.8.1" -is_executable = "1.0.1" -nom = "7" -derive-new = "0.5" -rust-embed = { version = "6.4.2", features = [ +extattr = "1.0" +jwalk = "0.8" +is_executable = "1.0" +nom = "7.1" +derive-new = "0.7" +rust-embed = { version = "8.5", features = [ "debug-embed", "compression", # must clean build after updating binaries ] } -which = "4.2.2" -getopts = "0.2.21" -sha256 = "1.4.0" +which = "7.0" +getopts = "0.2" +sha256 = "1" +sha1 = "0.10" +tempfile = "3.14" +chrono = "0.4" +hole-punch = { git = "https://github.com/tiann/hole-punch" } +regex-lite = "0.1" [target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies] -sys-mount = { git = "https://github.com/tiann/sys-mount", branch = "loopfix" } +rustix = { git = "https://github.com/Kernel-SU/rustix.git", branch = "main", features = [ + "all-apis", +] } # some android specific dependencies which compiles under unix are also listed here for convenience of coding -android-properties = { version = "0.2.2", features = ["bionic-deprecated"] } -procfs = "0.15" +android-properties = { version = "0.2", features = ["bionic-deprecated"] } +procfs = "0.17" +loopdev = { git = "https://github.com/Kernel-SU/loopdev" } [target.'cfg(target_os = "android")'.dependencies] -android_logger = "0.13" +android_logger = { version = "0.14", default-features = false } [profile.release] strip = true opt-level = "z" lto = true +codegen-units = 1 diff --git a/userspace/ksud/bin/.gitignore b/userspace/ksud/bin/.gitignore new file mode 100644 index 000000000000..1464b7ed27b3 --- /dev/null +++ b/userspace/ksud/bin/.gitignore @@ -0,0 +1 @@ +**/*.ko \ No newline at end of file diff --git a/userspace/ksud/bin/aarch64/bootctl b/userspace/ksud/bin/aarch64/bootctl new file mode 100644 index 000000000000..cf5c61368467 Binary files /dev/null and b/userspace/ksud/bin/aarch64/bootctl differ diff --git a/userspace/ksud/bin/aarch64/busybox b/userspace/ksud/bin/aarch64/busybox index 61b2ec160421..2fd8bcfa56cf 100755 Binary files a/userspace/ksud/bin/aarch64/busybox and b/userspace/ksud/bin/aarch64/busybox differ diff --git a/userspace/ksud/bin/aarch64/ksuinit b/userspace/ksud/bin/aarch64/ksuinit new file mode 100755 index 000000000000..8d3ba057f9de Binary files /dev/null and b/userspace/ksud/bin/aarch64/ksuinit differ diff --git a/userspace/ksud/bin/aarch64/resetprop b/userspace/ksud/bin/aarch64/resetprop index b667d2ea7972..305032c90dc9 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/busybox b/userspace/ksud/bin/x86_64/busybox old mode 100644 new mode 100755 index 87a4d95fce94..3603e741b8e8 Binary files a/userspace/ksud/bin/x86_64/busybox and b/userspace/ksud/bin/x86_64/busybox differ diff --git a/userspace/ksud/bin/x86_64/ksuinit b/userspace/ksud/bin/x86_64/ksuinit new file mode 100755 index 000000000000..9517baf915bf Binary files /dev/null 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 0997f25158a8..80030612649b 100644 Binary files a/userspace/ksud/bin/x86_64/resetprop and b/userspace/ksud/bin/x86_64/resetprop differ diff --git a/userspace/ksud/src/apk_sign.rs b/userspace/ksud/src/apk_sign.rs index 9e8dad3fedf7..6d047ccad9be 100644 --- a/userspace/ksud/src/apk_sign.rs +++ b/userspace/ksud/src/apk_sign.rs @@ -81,13 +81,10 @@ pub fn get_apk_signature(apk: &str) -> Result<(u32, String)> { } if v3_signing_exist || v3_1_signing_exist { - return Err(anyhow::anyhow!( - "Unexpected v3 signature found!", - )); + return Err(anyhow::anyhow!("Unexpected v3 signature found!",)); } v2_signing.ok_or(anyhow::anyhow!("No signature found!")) - } fn calc_cert_sha256( diff --git a/userspace/ksud/src/assets.rs b/userspace/ksud/src/assets.rs index 55ffe4c17888..e8255c8204a7 100644 --- a/userspace/ksud/src/assets.rs +++ b/userspace/ksud/src/assets.rs @@ -1,28 +1,50 @@ use anyhow::Result; use const_format::concatcp; use rust_embed::RustEmbed; +use std::path::Path; use crate::{defs::BINARY_DIR, utils}; pub const RESETPROP_PATH: &str = concatcp!(BINARY_DIR, "resetprop"); pub const BUSYBOX_PATH: &str = concatcp!(BINARY_DIR, "busybox"); +pub const BOOTCTL_PATH: &str = concatcp!(BINARY_DIR, "bootctl"); -#[cfg(target_arch = "aarch64")] +#[cfg(all(target_arch = "x86_64", target_os = "android"))] #[derive(RustEmbed)] -#[folder = "bin/aarch64"] +#[folder = "bin/x86_64"] struct Asset; -#[cfg(target_arch = "x86_64")] +// IF NOT x86_64 ANDROID, ie. macos, linux, windows, always use aarch64 +#[cfg(not(all(target_arch = "x86_64", target_os = "android")))] #[derive(RustEmbed)] -#[folder = "bin/x86_64"] +#[folder = "bin/aarch64"] struct Asset; -pub fn ensure_binaries() -> Result<()> { +pub fn ensure_binaries(ignore_if_exist: bool) -> Result<()> { for file in Asset::iter() { - utils::ensure_binary( - format!("{BINARY_DIR}{file}"), - &Asset::get(&file).unwrap().data, - )? + if file == "ksuinit" || file.ends_with(".ko") { + // don't extract ksuinit and kernel modules + continue; + } + let asset = Asset::get(&file).ok_or(anyhow::anyhow!("asset not found: {}", file))?; + utils::ensure_binary(format!("{BINARY_DIR}{file}"), &asset.data, ignore_if_exist)? } Ok(()) } + +pub fn copy_assets_to_file(name: &str, dst: impl AsRef) -> Result<()> { + let asset = Asset::get(name).ok_or(anyhow::anyhow!("asset not found: {}", name))?; + std::fs::write(dst, asset.data)?; + Ok(()) +} + +pub fn list_supported_kmi() -> Result> { + let mut list = Vec::new(); + for file in Asset::iter() { + // kmi_name = "xxx_kernelsu.ko" + if let Some(kmi) = file.strip_suffix("_kernelsu.ko") { + list.push(kmi.to_string()); + } + } + Ok(list) +} diff --git a/userspace/ksud/src/boot_patch.rs b/userspace/ksud/src/boot_patch.rs new file mode 100644 index 000000000000..8ce6ee73a186 --- /dev/null +++ b/userspace/ksud/src/boot_patch.rs @@ -0,0 +1,714 @@ +#[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 anyhow::anyhow; +use anyhow::bail; +use anyhow::ensure; +use anyhow::Context; +use anyhow::Result; +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}; + +#[cfg(target_os = "android")] +fn ensure_gki_kernel() -> Result<()> { + let version = get_kernel_version()?; + let is_gki = version.0 == 5 && version.1 >= 10 || version.2 > 5; + ensure!(is_gki, "only support GKI kernel"); + Ok(()) +} + +#[cfg(target_os = "android")] +pub 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+)")?; + if let Some(captures) = re.captures(&version) { + let major = captures + .get(1) + .and_then(|m| m.as_str().parse::().ok()) + .ok_or_else(|| anyhow!("Major version parse error"))?; + let minor = captures + .get(2) + .and_then(|m| m.as_str().parse::().ok()) + .ok_or_else(|| anyhow!("Minor version parse error"))?; + let patch = captures + .get(3) + .and_then(|m| m.as_str().parse::().ok()) + .ok_or_else(|| anyhow!("Patch version parse error"))?; + Ok((major, minor, patch)) + } else { + Err(anyhow!("Invalid kernel version string")) + } +} + +#[cfg(target_os = "android")] +fn parse_kmi(version: &str) -> Result { + let re = Regex::new(r"(.* )?(\d+\.\d+)(\S+)?(android\d+)(.*)")?; + let cap = re + .captures(version) + .ok_or_else(|| anyhow::anyhow!("Failed to get KMI from boot/modules"))?; + let android_version = cap.get(4).map_or("", |m| m.as_str()); + let kernel_version = cap.get(2).map_or("", |m| m.as_str()); + Ok(format!("{android_version}-{kernel_version}")) +} + +#[cfg(target_os = "android")] +fn parse_kmi_from_uname() -> Result { + let uname = rustix::system::uname(); + let version = uname.release().to_string_lossy(); + parse_kmi(&version) +} + +#[cfg(target_os = "android")] +fn parse_kmi_from_modules() -> Result { + use std::io::BufRead; + // find a *.ko in /vendor/lib/modules + let modfile = std::fs::read_dir("/vendor/lib/modules")? + .filter_map(Result::ok) + .find(|entry| entry.path().extension().map_or(false, |ext| ext == "ko")) + .map(|entry| entry.path()) + .ok_or_else(|| anyhow!("No kernel module found"))?; + let output = Command::new("modinfo").arg(modfile).output()?; + for line in output.stdout.lines().map_while(Result::ok) { + if line.starts_with("vermagic") { + return parse_kmi(&line); + } + } + anyhow::bail!("Parse KMI from modules failed") +} + +#[cfg(target_os = "android")] +pub fn get_current_kmi() -> Result { + parse_kmi_from_uname().or_else(|_| parse_kmi_from_modules()) +} + +#[cfg(not(target_os = "android"))] +pub fn get_current_kmi() -> Result { + bail!("Unsupported platform") +} + +fn parse_kmi_from_kernel(kernel: &PathBuf, workdir: &Path) -> Result { + use std::fs::{copy, File}; + use std::io::{BufReader, Read}; + let kernel_path = workdir.join("kernel"); + copy(kernel, &kernel_path).context("Failed to copy kernel")?; + + let file = File::open(&kernel_path).context("Failed to open kernel file")?; + let mut reader = BufReader::new(file); + let mut buffer = Vec::new(); + reader + .read_to_end(&mut buffer) + .context("Failed to read kernel file")?; + + let printable_strings: Vec<&str> = buffer + .split(|&b| b == 0) + .filter_map(|slice| std::str::from_utf8(slice).ok()) + .filter(|s| s.chars().all(|c| c.is_ascii_graphic() || c == ' ')) + .collect(); + + 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); + } + } + } + println!("- Failed to get KMI version"); + bail!("Try to choose LKM manually") +} + +fn parse_kmi_from_boot(magiskboot: &Path, image: &PathBuf, workdir: &Path) -> Result { + let image_path = workdir.join("image"); + + std::fs::copy(image, &image_path).context("Failed to copy image")?; + + let status = Command::new(magiskboot) + .current_dir(workdir) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg("unpack") + .arg(&image_path) + .status() + .context("Failed to execute magiskboot command")?; + + if !status.success() { + bail!( + "magiskboot unpack failed with status: {:?}", + status.code().unwrap() + ); + } + + parse_kmi_from_kernel(&image_path, workdir) +} + +fn do_cpio_cmd(magiskboot: &Path, workdir: &Path, cmd: &str) -> Result<()> { + let status = Command::new(magiskboot) + .current_dir(workdir) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg("cpio") + .arg("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_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 dd, Q: AsRef>(ifile: P, ofile: Q) -> Result<()> { + let status = Command::new("dd") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg(format!("if={}", ifile.as_ref().display())) + .arg(format!("of={}", ofile.as_ref().display())) + .status()?; + ensure!( + status.success(), + "dd if={:?} of={:?} failed", + ifile.as_ref(), + ofile.as_ref() + ); + Ok(()) +} + +pub fn restore( + image: Option, + magiskboot_path: Option, + flash: bool, +) -> Result<()> { + let tmpdir = tempfile::Builder::new() + .prefix("KernelSU") + .tempdir() + .context("create temp dir failed")?; + let workdir = tmpdir.path(); + let magiskboot = find_magiskboot(magiskboot_path, workdir)?; + + 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)?; + + println!("- Unpacking boot image"); + let status = Command::new(&magiskboot) + .current_dir(workdir) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg("unpack") + .arg(bootimage.display().to_string()) + .status()?; + ensure!(status.success(), "magiskboot unpack failed"); + + let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir)?; + 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() { + do_cpio_cmd( + &magiskboot, + workdir, + &format!("extract {0} {0}", BACKUP_FILENAME), + )?; + let sha = std::fs::read(workdir.join(BACKUP_FILENAME))?; + let sha = String::from_utf8(sha)?; + let sha = sha.trim(); + let backup_path = + PathBuf::from(KSU_BACKUP_DIR).join(format!("{KSU_BACKUP_FILE_PREFIX}{sha}")); + if backup_path.is_file() { + new_boot = Some(backup_path); + from_backup = true; + } else { + println!("- Warning: no backup {backup_path:?} found!"); + } + + if let Err(e) = clean_backup(sha) { + println!("- Warning: Cleanup backup image failed: {e}"); + } + } else { + println!("- Backup info is absent!"); + } + + if new_boot.is_none() { + // remove kernelsu.ko + do_cpio_cmd(&magiskboot, workdir, "rm kernelsu.ko")?; + + // if init.real exists, restore it + let status = do_cpio_cmd(&magiskboot, workdir, "exists init.real").is_ok(); + if status { + do_cpio_cmd(&magiskboot, workdir, "mv init.real init")?; + } else { + let ramdisk = workdir.join("ramdisk.cpio"); + std::fs::remove_file(ramdisk)?; + } + + println!("- Repacking boot image"); + let status = Command::new(&magiskboot) + .current_dir(workdir) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg("repack") + .arg(bootimage.display().to_string()) + .status()?; + ensure!(status.success(), "magiskboot repack failed"); + new_boot = Some(workdir.join("new-boot.img")); + } + + let new_boot = new_boot.unwrap(); + + if image.is_some() { + // if image is specified, write to output file + let output_dir = std::env::current_dir()?; + let now = chrono::Utc::now(); + let output_image = output_dir.join(format!( + "kernelsu_restore_{}.img", + now.format("%Y%m%d_%H%M%S") + )); + + if from_backup || std::fs::rename(&new_boot, &output_image).is_err() { + std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?; + } + println!("- Output file is written to"); + println!("- {}", output_image.display().to_string().trim_matches('"')); + } + if flash { + if from_backup { + println!("- Flashing new boot image from {}", new_boot.display()); + } else { + println!("- Flashing new boot image"); + } + flash_boot(&bootdevice, new_boot)?; + } + println!("- Done!"); + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +pub fn patch( + image: Option, + kernel: Option, + kmod: Option, + init: Option, + ota: bool, + flash: bool, + out: Option, + magiskboot: Option, + kmi: Option, +) -> Result<()> { + let result = do_patch(image, kernel, kmod, init, ota, flash, out, magiskboot, kmi); + if let Err(ref e) = result { + println!("- Install Error: {e}"); + } + result +} + +#[allow(clippy::too_many_arguments)] +fn do_patch( + image: Option, + kernel: Option, + kmod: Option, + init: Option, + ota: bool, + flash: bool, + out: Option, + magiskboot_path: Option, + kmi: Option, +) -> Result<()> { + println!(include_str!("banner")); + + let patch_file = image.is_some(); + + #[cfg(target_os = "android")] + if !patch_file { + ensure_gki_kernel()?; + } + + let is_replace_kernel = kernel.is_some(); + + if is_replace_kernel { + ensure!( + init.is_none() && kmod.is_none(), + "init and module must not be specified." + ); + } + + let tmpdir = tempfile::Builder::new() + .prefix("KernelSU") + .tempdir() + .context("create temp dir failed")?; + let workdir = tmpdir.path(); + + // extract magiskboot + let magiskboot = find_magiskboot(magiskboot_path, workdir)?; + + let kmi = if let Some(kmi) = kmi { + kmi + } else { + match get_current_kmi() { + Ok(value) => value, + Err(e) => { + println!("- {}", e); + if let Some(image_path) = &image { + println!( + "- Trying to auto detect KMI version for {}", + image_path.to_str().unwrap() + ); + parse_kmi_from_boot(&magiskboot, image_path, tmpdir.path())? + } else if let Some(kernel_path) = &kernel { + println!( + "- Trying to auto detect KMI version for {}", + kernel_path.to_str().unwrap() + ); + parse_kmi_from_kernel(kernel_path, tmpdir.path())? + } else { + "".to_string() + } + } + } + }; + + let skip_init = kmi.starts_with("android12-"); + + let (bootimage, bootdevice) = + find_boot_image(&image, skip_init, ota, is_replace_kernel, workdir)?; + + let bootimage = bootimage.display().to_string(); + + // try extract magiskboot/bootctl + let _ = assets::ensure_binaries(false); + + if let Some(kernel) = kernel { + std::fs::copy(kernel, workdir.join("kernel")).context("copy kernel from failed")?; + } + + println!("- Preparing assets"); + + let kmod_file = workdir.join("kernelsu.ko"); + if let Some(kmod) = kmod { + std::fs::copy(kmod, kmod_file).context("copy kernel module failed")?; + } else { + // If kmod is not specified, extract from assets + println!("- KMI: {kmi}"); + let name = format!("{kmi}_kernelsu.ko"); + assets::copy_assets_to_file(&name, kmod_file) + .with_context(|| format!("Failed to copy {name}"))?; + }; + + let init_file = workdir.join("init"); + if let Some(init) = init { + std::fs::copy(init, init_file).context("copy init failed")?; + } else { + assets::copy_assets_to_file("ksuinit", init_file).context("copy ksuinit failed")?; + } + + // magiskboot unpack boot.img + // magiskboot cpio ramdisk.cpio 'cp init init.real' + // magiskboot cpio ramdisk.cpio 'add 0755 ksuinit init' + // magiskboot cpio ramdisk.cpio 'add 0755 kernelsu.ko' + + println!("- Unpacking boot image"); + let status = Command::new(&magiskboot) + .current_dir(workdir) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg("unpack") + .arg(&bootimage) + .status()?; + ensure!(status.success(), "magiskboot unpack failed"); + + let no_ramdisk = !workdir.join("ramdisk.cpio").exists(); + let is_magisk_patched = is_magisk_patched(&magiskboot, workdir)?; + ensure!( + no_ramdisk || !is_magisk_patched, + "Cannot work with Magisk patched image" + ); + + println!("- Adding KernelSU LKM"); + let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir)?; + + let mut need_backup = false; + if !is_kernelsu_patched { + // kernelsu.ko is not exist, backup init if necessary + let status = do_cpio_cmd(&magiskboot, workdir, "exists init"); + if status.is_ok() { + do_cpio_cmd(&magiskboot, workdir, "mv init init.real")?; + } + + need_backup = flash; + } + + do_cpio_cmd(&magiskboot, workdir, "add 0755 init init")?; + do_cpio_cmd(&magiskboot, workdir, "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}"); + } + } + + println!("- Repacking boot image"); + // magiskboot repack boot.img + let status = Command::new(&magiskboot) + .current_dir(workdir) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .arg("repack") + .arg(&bootimage) + .status()?; + ensure!(status.success(), "magiskboot repack failed"); + let new_boot = workdir.join("new-boot.img"); + + if patch_file { + // if image is specified, write to output file + let output_dir = out.unwrap_or(std::env::current_dir()?); + let now = chrono::Utc::now(); + let output_image = output_dir.join(format!( + "kernelsu_patched_{}.img", + now.format("%Y%m%d_%H%M%S") + )); + + if std::fs::rename(&new_boot, &output_image).is_err() { + std::fs::copy(&new_boot, &output_image).context("copy out new boot failed")?; + } + println!("- Output file is written to"); + println!("- {}", output_image.display().to_string().trim_matches('"')); + } + + if flash { + println!("- Flashing new boot image"); + flash_boot(&bootdevice, new_boot)?; + + if ota { + post_ota()?; + } + } + + println!("- Done!"); + Ok(()) +} + +#[cfg(target_os = "android")] +fn calculate_sha1(file_path: impl AsRef) -> Result { + use sha1::Digest; + use std::io::Read; + let mut file = std::fs::File::open(file_path.as_ref())?; + let mut hasher = sha1::Sha1::new(); + let mut buffer = [0; 1024]; + + loop { + let n = file.read(&mut buffer)?; + if n == 0 { + break; + } + hasher.update(&buffer[..n]); + } + + let result = hasher.finalize(); + Ok(format!("{:x}", result)) +} + +#[cfg(target_os = "android")] +fn do_backup(magiskboot: &Path, workdir: &Path, image: &str) -> Result<()> { + let sha1 = calculate_sha1(image)?; + let filename = format!("{KSU_BACKUP_FILE_PREFIX}{sha1}"); + + println!("- Backup stock boot image"); + // magiskboot cpio ramdisk.cpio 'add 0755 $BACKUP_FILENAME' + let target = format!("{KSU_BACKUP_DIR}{filename}"); + std::fs::copy(image, &target).with_context(|| format!("backup to {target}"))?; + std::fs::write(workdir.join(BACKUP_FILENAME), sha1.as_bytes()).context("write sha1")?; + do_cpio_cmd( + magiskboot, + workdir, + &format!("add 0755 {0} {0}", BACKUP_FILENAME), + )?; + println!("- Stock image has been backup to"); + println!("- {target}"); + Ok(()) +} + +#[cfg(target_os = "android")] +fn clean_backup(sha1: &str) -> Result<()> { + println!("- Clean up backup"); + let backup_name = format!("{}{}", KSU_BACKUP_FILE_PREFIX, sha1); + let dir = std::fs::read_dir(defs::KSU_BACKUP_DIR)?; + for entry in dir.flatten() { + let path = entry.path(); + if !path.is_file() { + continue; + } + if let Some(name) = path.file_name() { + let name = name.to_string_lossy().to_string(); + if name != backup_name + && name.starts_with(KSU_BACKUP_FILE_PREFIX) + && std::fs::remove_file(path).is_ok() + { + println!("- removed {name}"); + } + } + } + Ok(()) +} + +fn flash_boot(bootdevice: &Option, new_boot: PathBuf) -> Result<()> { + let Some(bootdevice) = bootdevice else { + bail!("boot device not found") + }; + let status = Command::new("blockdev") + .arg("--setrw") + .arg(bootdevice) + .status()?; + ensure!(status.success(), "set boot device rw failed"); + dd(new_boot, bootdevice).context("flash boot failed")?; + Ok(()) +} + +fn find_magiskboot(magiskboot_path: Option, workdir: &Path) -> Result { + let magiskboot = { + if which("magiskboot").is_ok() { + let _ = assets::ensure_binaries(true); + "magiskboot".into() + } else { + // magiskboot is not in $PATH, use builtin or specified one + let magiskboot = if let Some(magiskboot_path) = magiskboot_path { + std::fs::canonicalize(magiskboot_path)? + } else { + let magiskboot_path = workdir.join("magiskboot"); + assets::copy_assets_to_file("magiskboot", &magiskboot_path) + .context("copy magiskboot failed")?; + magiskboot_path + }; + ensure!(magiskboot.exists(), "{magiskboot:?} is not exist"); + #[cfg(unix)] + let _ = std::fs::set_permissions(&magiskboot, std::fs::Permissions::from_mode(0o755)); + magiskboot + } + }; + Ok(magiskboot) +} + +fn find_boot_image( + image: &Option, + skip_init: bool, + ota: bool, + is_replace_kernel: bool, + workdir: &Path, +) -> Result<(PathBuf, Option)> { + let bootimage; + let mut bootdevice = None; + if let Some(ref image) = *image { + 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_exist = + Path::new(&format!("/dev/block/by-name/init_boot{slot_suffix}")).exists(); + let boot_partition = if !is_replace_kernel && init_boot_exist && !skip_init { + format!("/dev/block/by-name/init_boot{slot_suffix}") + } else { + format!("/dev/block/by-name/boot{slot_suffix}") + }; + + println!("- Bootdevice: {boot_partition}"); + let tmp_boot_path = workdir.join("boot.img"); + + dd(&boot_partition, &tmp_boot_path)?; + + ensure!(tmp_boot_path.exists(), "boot image not found"); + + bootimage = tmp_boot_path; + bootdevice = Some(boot_partition); + }; + Ok((bootimage, bootdevice)) +} + +fn post_ota() -> Result<()> { + use crate::defs::ADB_DIR; + use assets::BOOTCTL_PATH; + let status = Command::new(BOOTCTL_PATH).arg("hal-info").status()?; + if !status.success() { + return Ok(()); + } + + let current_slot = Command::new(BOOTCTL_PATH) + .arg("get-current-slot") + .output()? + .stdout; + let current_slot = String::from_utf8(current_slot)?; + let current_slot = current_slot.trim(); + let target_slot = if current_slot == "0" { 1 } else { 0 }; + + Command::new(BOOTCTL_PATH) + .arg(format!("set-active-boot-slot {target_slot}")) + .status()?; + + let post_fs_data = std::path::Path::new(ADB_DIR).join("post-fs-data.d"); + utils::ensure_dir_exists(&post_fs_data)?; + let post_ota_sh = post_fs_data.join("post_ota.sh"); + + let sh_content = format!( + r###" +{BOOTCTL_PATH} mark-boot-successful +rm -f {BOOTCTL_PATH} +rm -f /data/adb/post-fs-data.d/post_ota.sh +"### + ); + + std::fs::write(&post_ota_sh, sh_content)?; + #[cfg(unix)] + std::fs::set_permissions(post_ota_sh, std::fs::Permissions::from_mode(0o755))?; + + Ok(()) +} diff --git a/userspace/ksud/src/cli.rs b/userspace/ksud/src/cli.rs index 410195a9e05a..5f0b8769b508 100644 --- a/userspace/ksud/src/cli.rs +++ b/userspace/ksud/src/cli.rs @@ -1,12 +1,13 @@ use anyhow::{Ok, Result}; use clap::Parser; +use std::path::PathBuf; #[cfg(target_os = "android")] use android_logger::Config; #[cfg(target_os = "android")] use log::LevelFilter; -use crate::{apk_sign, debug, defs, event, module, utils}; +use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils}; /// KernelSU userspace cli #[derive(Parser, Debug)] @@ -34,7 +35,17 @@ enum Commands { BootCompleted, /// Install KernelSU userspace component to system - Install, + Install { + #[arg(long, default_value = None)] + magiskboot: Option, + }, + + /// Uninstall KernelSU modules and itself(LKM Only) + Uninstall { + /// magiskboot path, if not specified, will search from $PATH + #[arg(long, default_value = None)] + magiskboot: Option, + }, /// SELinux policy Patch tool Sepolicy { @@ -48,12 +59,81 @@ enum Commands { command: Profile, }, + /// Patch boot or init_boot images to apply KernelSU + BootPatch { + /// boot image path, if not specified, will try to find the boot image automatically + #[arg(short, long)] + boot: Option, + + /// kernel image path to replace + #[arg(short, long)] + kernel: Option, + + /// LKM module path to replace, if not specified, will use the builtin one + #[arg(short, long)] + module: Option, + + /// init to be replaced + #[arg(short, long, requires("module"))] + init: Option, + + /// will use another slot when boot image is not specified + #[arg(short = 'u', long, default_value = "false")] + ota: bool, + + /// Flash it to boot partition after patch + #[arg(short, long, default_value = "false")] + flash: bool, + + /// output path, if not specified, will use current directory + #[arg(short, long, default_value = None)] + out: Option, + + /// magiskboot path, if not specified, will search from $PATH + #[arg(long, default_value = None)] + magiskboot: Option, + + /// KMI version, if specified, will use the specified KMI + #[arg(long, default_value = None)] + kmi: Option, + }, + + /// Restore boot or init_boot images patched by KernelSU + BootRestore { + /// boot image path, if not specified, will try to find the boot image automatically + #[arg(short, long)] + boot: Option, + + /// Flash it to boot partition after patch + #[arg(short, long, default_value = "false")] + flash: bool, + + /// magiskboot path, if not specified, will search from $PATH + #[arg(long, default_value = None)] + magiskboot: Option, + }, + + /// Show boot information + BootInfo { + #[command(subcommand)] + command: BootInfo, + }, /// For developers Debug { #[command(subcommand)] command: Debug, }, } + +#[derive(clap::Subcommand, Debug)] +enum BootInfo { + /// show current kmi version + CurrentKmi, + + /// show supported kmi versions + SupportedKmi, +} + #[derive(clap::Subcommand, Debug)] enum Debug { /// Set the manager app, kernel CONFIG_KSU_DEBUG should be enabled. @@ -70,13 +150,28 @@ enum Debug { }, /// Root Shell - Su, + Su { + /// switch to gloabl mount namespace + #[arg(short, long, default_value = "false")] + global_mnt: bool, + }, /// Get kernel version Version, Mount, + /// Copy sparse file + Xcp { + /// source file + src: String, + /// destination file + dst: String, + /// punch hole + #[arg(short, long, default_value = "false")] + punch_hole: bool, + }, + /// For testing Test, } @@ -128,8 +223,17 @@ enum Module { id: String, }, + /// run action for module + Action { + // module id + id: String, + }, + /// list all modules List, + + /// Shrink module image size + Shrink, } #[derive(clap::Subcommand, Debug)] @@ -148,20 +252,26 @@ enum Profile { policy: String, }, - /// get template of + /// get template of GetTemplate { - /// package name - package: String, + /// template id + id: String, }, - /// set template of to