diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 95ff27aa..00000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.bat eol=crlf \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 12031898..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1,5 +0,0 @@ -# These are supported funding model platforms - -github: tiann -patreon: weishu -custom: https://vxposed.com/donate.html diff --git a/.github/ISSUE_TEMPLATE/add_device.yml b/.github/ISSUE_TEMPLATE/add_device.yml deleted file mode 100644 index 756fd3d1..00000000 --- a/.github/ISSUE_TEMPLATE/add_device.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Contribute to Unofficially Supported Device -description: Add your device kernel source to KernelSU's Unofficially Supported Device List -title: "[Add Device]: " -labels: ["add-device"] -body: - - type: markdown - attributes: - value: | - Thanks for supporting KernelSU! - - type: input - id: repo-url - attributes: - label: Repository URL - description: Your repository URL - placeholder: https://github.com/tiann/KernelSU - validations: - required: true - - type: input - id: device - attributes: - label: Device - description: Please describe the device maintained by you. - placeholder: GKI 2.0 Device - validations: - required: true - - type: checkboxes - id: terms - attributes: - label: Code of Conduct - description: By submitting this issue, you should be the maintainer of the repository. - options: - - label: I'm the maintainer of this repository - required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml deleted file mode 100644 index 11a1f961..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ /dev/null @@ -1,72 +0,0 @@ -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 deleted file mode 100644 index d4f07b61..00000000 --- a/.github/ISSUE_TEMPLATE/config.yml +++ /dev/null @@ -1,5 +0,0 @@ -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/custom.yml b/.github/ISSUE_TEMPLATE/custom.yml deleted file mode 100644 index 5b17fe90..00000000 --- a/.github/ISSUE_TEMPLATE/custom.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Custom issue template -description: WARNING! If you are reporting a bug but use this template, the issue will be closed directly. -title: '[Custom]' -body: - - type: textarea - id: description - attributes: - label: "Describe your problem." - validations: - required: true - diff --git a/.github/scripts/build_a12.sh b/.github/scripts/build_a12.sh deleted file mode 100644 index d6abf750..00000000 --- a/.github/scripts/build_a12.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/bin/bash -set -euo pipefail - -build_from_image() { - export TITLE - TITLE=kernel-aarch64-${1//Image-/} - echo "[+] title: $TITLE" - - export PATCH_LEVEL - PATCH_LEVEL=$(echo "$1" | awk -F_ '{ print $2}') - echo "[+] patch level: $PATCH_LEVEL" - - echo '[+] Download prebuilt ramdisk' - GKI_URL=https://dl.google.com/android/gki/gki-certified-boot-android12-5.10-"${PATCH_LEVEL}"_r1.zip - FALLBACK_URL=https://dl.google.com/android/gki/gki-certified-boot-android12-5.10-2023-01_r1.zip - status=$(curl -sL -w "%{http_code}" "$GKI_URL" -o /dev/null) - if [ "$status" = "200" ]; then - curl -Lo gki-kernel.zip "$GKI_URL" - else - echo "[+] $GKI_URL not found, using $FALLBACK_URL" - curl -Lo gki-kernel.zip "$FALLBACK_URL" - fi - unzip gki-kernel.zip && rm gki-kernel.zip - - echo '[+] Unpack prebuilt boot.img' - BOOT_IMG=$(find . -maxdepth 1 -name "boot*.img") - $UNPACK_BOOTIMG --boot_img="$BOOT_IMG" - rm "$BOOT_IMG" - - echo '[+] Building Image.gz' - $GZIP -n -k -f -9 Image >Image.gz - - echo '[+] Building boot.img' - $MKBOOTIMG --header_version 4 --kernel Image --output boot.img --ramdisk out/ramdisk --os_version 12.0.0 --os_patch_level "${PATCH_LEVEL}" - $AVBTOOL add_hash_footer --partition_name boot --partition_size $((64 * 1024 * 1024)) --image boot.img --algorithm SHA256_RSA2048 --key ../kernel-build-tools/linux-x86/share/avb/testkey_rsa2048.pem - - echo '[+] Building boot-gz.img' - $MKBOOTIMG --header_version 4 --kernel Image.gz --output boot-gz.img --ramdisk out/ramdisk --os_version 12.0.0 --os_patch_level "${PATCH_LEVEL}" - $AVBTOOL add_hash_footer --partition_name boot --partition_size $((64 * 1024 * 1024)) --image boot-gz.img --algorithm SHA256_RSA2048 --key ../kernel-build-tools/linux-x86/share/avb/testkey_rsa2048.pem - - echo '[+] Building boot-lz4.img' - $MKBOOTIMG --header_version 4 --kernel Image.lz4 --output boot-lz4.img --ramdisk out/ramdisk --os_version 12.0.0 --os_patch_level "${PATCH_LEVEL}" - $AVBTOOL add_hash_footer --partition_name boot --partition_size $((64 * 1024 * 1024)) --image boot-lz4.img --algorithm SHA256_RSA2048 --key ../kernel-build-tools/linux-x86/share/avb/testkey_rsa2048.pem - - echo '[+] Compress images' - for image in boot*.img; do - $GZIP -n -f -9 "$image" - mv "$image".gz "${1//Image-/}"-"$image".gz - done - - echo "[+] Images to upload" - find . -type f -name "*.gz" - - # find . -type f -name "*.gz" -exec python3 "$GITHUB_WORKSPACE"/KernelSU/scripts/ksubot.py {} + -} - -for dir in Image*; do - if [ -d "$dir" ]; then - echo "----- Building $dir -----" - cd "$dir" - build_from_image "$dir" - cd .. - fi -done diff --git a/.github/scripts/build_a13.sh b/.github/scripts/build_a13.sh deleted file mode 100644 index 929e1be0..00000000 --- a/.github/scripts/build_a13.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -euo pipefail - -build_from_image() { - export TITLE - TITLE=kernel-aarch64-${1//Image-/} - - echo "[+] title: $TITLE" - echo '[+] Building Image.gz' - $GZIP -n -k -f -9 Image >Image.gz - - echo '[+] Building boot.img' - $MKBOOTIMG --header_version 4 --kernel Image --output boot.img - $AVBTOOL add_hash_footer --partition_name boot --partition_size $((64 * 1024 * 1024)) --image boot.img --algorithm SHA256_RSA2048 --key ../kernel-build-tools/linux-x86/share/avb/testkey_rsa2048.pem - - echo '[+] Building boot-gz.img' - $MKBOOTIMG --header_version 4 --kernel Image.gz --output boot-gz.img - $AVBTOOL add_hash_footer --partition_name boot --partition_size $((64 * 1024 * 1024)) --image boot-gz.img --algorithm SHA256_RSA2048 --key ../kernel-build-tools/linux-x86/share/avb/testkey_rsa2048.pem - - echo '[+] Building boot-lz4.img' - $MKBOOTIMG --header_version 4 --kernel Image.lz4 --output boot-lz4.img - $AVBTOOL add_hash_footer --partition_name boot --partition_size $((64 * 1024 * 1024)) --image boot-lz4.img --algorithm SHA256_RSA2048 --key ../kernel-build-tools/linux-x86/share/avb/testkey_rsa2048.pem - - echo '[+] Compress images' - for image in boot*.img; do - $GZIP -n -f -9 "$image" - mv "$image".gz "${1//Image-/}"-"$image".gz - done - - echo '[+] Images to upload' - find . -type f -name "*.gz" - - # find . -type f -name "*.gz" -exec python3 "$GITHUB_WORKSPACE"/KernelSU/scripts/ksubot.py {} + -} - -for dir in Image*; do - if [ -d "$dir" ]; then - echo "----- Building $dir -----" - cd "$dir" - build_from_image "$dir" - cd .. - fi -done diff --git a/.github/workflows/add-device.yml b/.github/workflows/add-device.yml deleted file mode 100644 index c98c2ac4..00000000 --- a/.github/workflows/add-device.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: handle-add-device-issue - -on: - issues: - types: [labeled] - -jobs: - handle-add-device: - if: github.event.label.name == 'add-device' - runs-on: ubuntu-latest - env: - ISSUE_CONTENT: ${{ github.event.issue.body }} - steps: - - uses: actions/checkout@v4 - - name: Parse issue body - id: handle-add-device - run: | - python3 scripts/add_device_handler.py website/docs/repos.json || true - - name: Commit - if: steps.handle-add-device.outputs.success == 'true' - run: | - git config --local user.name "GitHub Actions" - git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add website/docs/repos.json - git commit -m "add device: ${{ steps.handle-add-device.outputs.device }}" - - name: Make pull request - if: steps.handle-add-device.outputs.success == 'true' - id: cpr - uses: peter-evans/create-pull-request@v7 - with: - token: ${{ secrets.GITHUB_TOKEN }} - commit-message: "[add device]: ${{ steps.handle-add-device.outputs.device }}" - title: "[add device]: ${{ steps.handle-add-device.outputs.device }}" - body: | - ${{ steps.handle-add-device.outputs.device }} has been added to the website. - Related issue: ${{ github.event.issue.html_url }} - 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: | - echo "Pull Request Number - ${{ steps.cpr.outputs.pull-request-number }}" - echo "Pull Request URL - ${{ steps.cpr.outputs.pull-request-url }}" - - uses: Kernel-SU/actions-comment-on-issue@master - if: ${{ steps.cpr.outputs.pull-request-number }} - with: - message: "Automatically created pull request: ${{ steps.cpr.outputs.pull-request-url }}" - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - uses: Kernel-SU/actions-comment-on-issue@master - if: steps.handle-add-device.outputs.success != 'true' - with: - 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@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 deleted file mode 100644 index e211c1e0..00000000 --- a/.github/workflows/avd-kernel.yml +++ /dev/null @@ -1,137 +0,0 @@ -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-lkm.yml b/.github/workflows/build-lkm.yml deleted file mode 100644 index 515e7cba..00000000 --- a/.github/workflows/build-lkm.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Build LKM for KernelSU -on: - workflow_call: - inputs: - upload: - required: true - type: boolean - default: true - description: "Whether to upload to branch" - secrets: - # username:github_pat - TOKEN: - required: true - workflow_dispatch: - inputs: - upload: - required: true - type: boolean - default: true - description: "Whether to upload to branch" -jobs: - build-lkm: - strategy: - matrix: - include: - - version: "android12-5.10" - sub_level: 233 - os_patch_level: 2025-02 - - version: "android13-5.10" - sub_level: 234 - os_patch_level: 2025-03 - - version: "android13-5.15" - sub_level: 178 - os_patch_level: 2025-03 - - version: "android14-5.15" - sub_level: 178 - os_patch_level: 2025-03 - - version: "android14-6.1" - sub_level: 129 - os_patch_level: 2025-04 - - version: "android15-6.6" - sub_level: 82 - os_patch_level: 2025-04 - # uses: ./.github/workflows/gki-kernel-mock.yml when debugging - 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 - - push-to-branch: - needs: [build-lkm] - runs-on: ubuntu-latest - if: ${{ inputs.upload }} - steps: - - name: Download all workflow run artifacts - uses: actions/download-artifact@v4 - with: - path: bin/ - merge-multiple: true - - name: Push to branch LKM - run: | - cd bin - git config --global init.defaultBranch lkm - git init - git remote add origin https://${{ secrets.TOKEN }}@github.com/${{ github.repository }} - git config --local user.name "github-actions[bot]" - git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com" - find . -type f - git add . - git commit -m "Upload LKM from ${{ github.sha }}" -m "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - git push --force --set-upstream origin lkm diff --git a/.github/workflows/build-manager.yml b/.github/workflows/build-manager.yml deleted file mode 100644 index c7747d83..00000000 --- a/.github/workflows/build-manager.yml +++ /dev/null @@ -1,266 +0,0 @@ -name: Build Manager - -on: - push: - branches: [ "main", "ci" ] - paths: - - '.github/workflows/build-manager.yml' - - 'manager/**' - - 'kernel/**' - - 'userspace/ksud/**' - - 'userspace/susfs/**' - - 'userspace/kpmmgr/**' - pull_request: - branches: [ "main" ] - paths: - - 'manager/**' - workflow_call: - workflow_dispatch: - inputs: - build_lkm: - required: true - type: choice - default: "auto" - options: - - "true" - - "false" - - "auto" - description: "Whether to build lkm" - upload_lkm: - required: true - type: boolean - default: true - description: "Whether to upload lkm" -jobs: - check-build-lkm: - runs-on: ubuntu-latest - outputs: - build_lkm: ${{ steps.check-build.outputs.build_lkm }} - upload_lkm: ${{ steps.check-build.outputs.upload_lkm }} - steps: - - name: check build - id: check-build - run: | - if [ "${{ github.event_name }}" == "workflow_dispatch" ] && [ "${{ inputs.build_lkm }}" != "auto" ]; then - kernel_changed="${{ inputs.build_lkm }}" - else - kernel_changed=true - mkdir tmp - cd tmp - git config --global init.defaultBranch bot - git config --global user.name 'Bot' - git config --global user.email 'bot@github.shirkneko.io' - git init . - git remote add origin https://github.com/${{ github.repository }} - CURRENT_COMMIT="${{ github.event.head_commit.id }}" - git fetch origin $CURRENT_COMMIT --depth=1 - git fetch origin lkm --depth=1 - LKM_COMMIT="$(git log --format=%B -n 1 origin/lkm | head -n 1)" - LKM_COMMIT="${LKM_COMMIT#Upload LKM from }" - LKM_COMMIT=$(echo "$LKM_COMMIT" | tr -d '[:space:]') - echo "LKM_COMMIT=$LKM_COMMIT" - git fetch origin "$LKM_COMMIT" --depth=1 - git diff --quiet "$LKM_COMMIT" "$CURRENT_COMMIT" -- kernel :!kernel/setup.sh .github/workflows/build-lkm.yml .github/workflows/build-kernel-*.yml && kernel_changed=false - cd .. - rm -rf tmp - fi - if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == 'refs/heads/main' ]; then - need_upload=true - elif [ "${{ github.event_name }}" == "workflow_dispatch" ]; then - need_upload="${{ inputs.upload_lkm }}" - else - need_upload=false - fi - echo "kernel changed: $kernel_changed" - echo "need upload: $need_upload" - echo "build_lkm=$kernel_changed" >> "$GITHUB_OUTPUT" - echo "upload_lkm=$need_upload" >> "$GITHUB_OUTPUT" - - build-lkm: - needs: check-build-lkm - uses: ./.github/workflows/build-lkm.yml - if: ${{ needs.check-build-lkm.outputs.build_lkm == 'true' }} - with: - upload: ${{ needs.check-build-lkm.outputs.upload_lkm == 'true' }} - secrets: inherit - build-susfs: - if: ${{ always() }} - needs: [ check-build-lkm, build-lkm ] - strategy: - matrix: - include: - - target: aarch64-linux-android - os: ubuntu-latest - uses: ./.github/workflows/susfs.yml - with: - target: ${{ matrix.target }} - os: ${{ matrix.os }} - - build-kpmmgr: - if: ${{ always() }} - needs: [ check-build-lkm, build-lkm ] - strategy: - matrix: - include: - - target: aarch64-linux-android - os: ubuntu-latest - uses: ./.github/workflows/kpmmgr.yml - with: - target: ${{ matrix.target }} - os: ${{ matrix.os }} - - build-ksud: - if: ${{ always() }} - needs: [ check-build-lkm, build-lkm ] - strategy: - matrix: - include: - - target: aarch64-linux-android - os: ubuntu-latest - - target: x86_64-linux-android - os: ubuntu-latest - uses: ./.github/workflows/ksud.yml - with: - target: ${{ matrix.target }} - os: ${{ matrix.os }} - pack_lkm: true - pull_lkm: ${{ needs.check-build-lkm.outputs.build_lkm != 'true' }} - - build-manager: - if: ${{ always() }} - needs: build-ksud - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./manager - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup need_upload - id: need_upload - run: | - if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then - echo "UPLOAD=true" >> $GITHUB_OUTPUT - else - echo "UPLOAD=false" >> $GITHUB_OUTPUT - fi - - - name: Write key - if: ${{ ( github.event_name != 'pull_request' && github.ref == 'refs/heads/main' ) || github.ref == 'refs/heads/susfs' || github.ref_type == 'tag' }} - run: | - if [ ! -z "${{ secrets.KEYSTORE }}" ]; then - { - echo KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' - echo KEY_ALIAS='${{ secrets.KEY_ALIAS }}' - echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' - echo KEYSTORE_FILE='key.jks' - } >> gradle.properties - echo "${{ secrets.KEYSTORE }}" | base64 -d > key.jks - fi - - - name: Setup Java - uses: actions/setup-java@v4 - with: - distribution: temurin - java-version: 21 - - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v4 - - - name: Setup Android SDK - uses: android-actions/setup-android@v3 - - - name: Download arm64 susfs - uses: actions/download-artifact@v4 - with: - name: susfs-aarch64-linux-android - path: . - - - name: Download arm64 kpmmgr - uses: actions/download-artifact@v4 - with: - name: kpmmgr-aarch64-linux-android - path: . - - - name: Download arm64 ksud - uses: actions/download-artifact@v4 - with: - name: ksud-aarch64-linux-android - path: . - - - name: Download x86_64 ksud - uses: actions/download-artifact@v4 - with: - name: ksud-x86_64-linux-android - path: . - - - name: Copy ksud to app jniLibs - run: | - mkdir -p app/src/main/jniLibs/arm64-v8a - mkdir -p app/src/main/jniLibs/x86_64 - cp -f ../aarch64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozako.so - cp -f ../x86_64-linux-android/release/zakozako ../manager/app/src/main/jniLibs/x86_64/libzakozako.so - - - name: Copy kpmmgr to app jniLibs - run: | - mkdir -p app/src/main/jniLibs/arm64-v8a - cp -f ../arm64-v8a/kpmmgr ../manager/app/src/main/jniLibs/arm64-v8a/libkpmmgr.so - - - name: Copy susfs to app jniLibs - run: | - mkdir -p app/src/main/jniLibs/arm64-v8a - cp -f ../arm64-v8a/zakozakozako ../manager/app/src/main/jniLibs/arm64-v8a/libzakozakozako.so - - - name: Build with Gradle - run: | - { - 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@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: 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: ${{ vars.CHAT_ID }} - BOT_TOKEN: ${{ secrets.BOT_TOKEN }} - MESSAGE_THREAD_ID: ${{ vars.MESSAGE_THREAD_ID }} - COMMIT_MESSAGE: ${{ github.event.head_commit.message }} - COMMIT_URL: ${{ github.event.head_commit.url }} - RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} - TITLE: Manager - run: | - if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then - export VERSION=$(git rev-list --count HEAD) - APK=$(find ./app/build/outputs/apk/release -name "*.apk") - pip3 install telethon - python3 $GITHUB_WORKSPACE/scripts/ksubot.py $APK - fi diff --git a/.github/workflows/build-su.yml b/.github/workflows/build-su.yml deleted file mode 100644 index 22fd58af..00000000 --- a/.github/workflows/build-su.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Build SU -on: - push: - branches: [ "main", "ci" ] - paths: - - '.github/workflows/build-su.yml' - - 'userspace/su/**' - - 'scripts/ksubot.py' - pull_request: - branches: [ "main" ] - paths: - - 'userspace/su/**' -jobs: - build-su: - name: Build userspace su - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Setup need_upload - id: need_upload - run: | - if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then - echo "UPLOAD=true" >> $GITHUB_OUTPUT - else - echo "UPLOAD=false" >> $GITHUB_OUTPUT - fi - - name: Build su - working-directory: ./userspace/su - run: $ANDROID_NDK/ndk-build - - name: Upload a Build Artifact - uses: actions/upload-artifact@v4 - with: - name: su - path: ./userspace/su/libs diff --git a/.github/workflows/clippy.yml b/.github/workflows/clippy.yml deleted file mode 100644 index bafb41a1..00000000 --- a/.github/workflows/clippy.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Clippy check - -on: - push: - branches: - - main - paths: - - '.github/workflows/clippy.yml' - - 'userspace/ksud/**' - pull_request: - branches: - - main - paths: - - '.github/workflows/clippy.yml' - - 'userspace/ksud/**' - -env: - RUSTFLAGS: '-Dwarnings' - -jobs: - clippy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - run: rustup update stable - - uses: Swatinem/rust-cache@v2 - with: - workspaces: userspace/ksud - - - name: Install cross - run: | - RUSTFLAGS="" cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1 - - - name: Run clippy - run: | - cross clippy --manifest-path userspace/ksud/Cargo.toml --target aarch64-linux-android --release - cross clippy --manifest-path userspace/ksud/Cargo.toml --target x86_64-linux-android --release diff --git a/.github/workflows/deploy-website.yml b/.github/workflows/deploy-website.yml deleted file mode 100644 index 34961052..00000000 --- a/.github/workflows/deploy-website.yml +++ /dev/null @@ -1,67 +0,0 @@ -name: Deploy Website - -on: - push: - branches: - - main - - website - paths: - - '.github/workflows/deploy-website.yml' - - 'website/**' - workflow_dispatch: - -# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages -permissions: - contents: read - pages: write - id-token: write - -# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. -# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. -concurrency: - group: pages - cancel-in-progress: false - -jobs: - # Build job - build: - runs-on: ubuntu-latest - defaults: - run: - working-directory: ./website - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Not needed if lastUpdated is not enabled - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: latest - cache: yarn # or pnpm / yarn - cache-dependency-path: website/yarn.lock - - name: Setup Pages - uses: actions/configure-pages@v5 - - name: Install dependencies - run: yarn install --frozen-lockfile - - name: Build with VitePress - run: | - yarn docs:build - touch docs/.vitepress/dist/.nojekyll - - name: Upload artifact - uses: actions/upload-pages-artifact@v3 - with: - path: website/docs/.vitepress/dist - - # Deployment job - deploy: - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - needs: build - runs-on: ubuntu-latest - name: Deploy - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/gki-kernel-mock.yml b/.github/workflows/gki-kernel-mock.yml deleted file mode 100644 index 205d1d0e..00000000 --- a/.github/workflows/gki-kernel-mock.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: GKI Kernel Build - -on: - workflow_call: - inputs: - version: - required: true - type: string - description: > - Output directory of gki, - for example: android12-5.10 - version_name: - required: true - type: string - description: > - With SUBLEVEL of kernel, - for example: android12-5.10.66 - tag: - required: true - type: string - description: > - Part of branch name of common kernel manifest, - for example: android12-5.10-2021-11 - os_patch_level: - required: false - type: string - description: > - Patch level of common kernel manifest, - for example: 2021-11 - default: 2022-05 - patch_path: - required: false - type: string - description: > - Directory name of .github/patches/ - for example: 5.10 - use_cache: - required: false - type: boolean - default: true - embed_ksud: - required: false - type: string - default: ksud-aarch64-linux-android - description: > - Artifact name of prebuilt ksud to be embedded - for example: ksud-aarch64-linux-android - debug: - required: false - type: boolean - default: false - build_lkm: - required: false - type: boolean - default: false - secrets: - BOOT_SIGN_KEY: - required: false - CHAT_ID: - required: false - BOT_TOKEN: - required: false - MESSAGE_THREAD_ID: - required: false - -jobs: - mock_build: - name: Mock build ${{ inputs.version_name }} - runs-on: ubuntu-latest - steps: - - name: Create mocking ko - run: | - echo "${{ inputs.version }}_kernelsu.ko" > ${{ inputs.version }}_kernelsu.ko - - name: Upload LKM - uses: actions/upload-artifact@v4 - if: ${{ inputs.build_lkm == true }} - with: - name: ${{ inputs.version }}-lkm - path: ./*_kernelsu.ko diff --git a/.github/workflows/gki-kernel.yml b/.github/workflows/gki-kernel.yml deleted file mode 100644 index 21bb6346..00000000 --- a/.github/workflows/gki-kernel.yml +++ /dev/null @@ -1,261 +0,0 @@ -name: GKI Kernel Build - -on: - workflow_call: - inputs: - version: - required: true - type: string - description: > - Output directory of gki, - for example: android12-5.10 - version_name: - required: true - type: string - description: > - With SUBLEVEL of kernel, - for example: android12-5.10.66 - tag: - required: true - type: string - description: > - Part of branch name of common kernel manifest, - for example: android12-5.10-2021-11 - os_patch_level: - required: false - type: string - description: > - Patch level of common kernel manifest, - for example: 2021-11 - default: 2022-05 - patch_path: - required: false - type: string - description: > - Directory name of .github/patches/ - for example: 5.10 - use_cache: - required: false - type: boolean - default: true - embed_ksud: - required: false - type: string - default: ksud-aarch64-linux-android - description: > - Artifact name of prebuilt ksud to be embedded - for example: ksud-aarch64-linux-android - debug: - required: false - type: boolean - default: false - build_lkm: - required: false - type: boolean - default: false - 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-latest - env: - CCACHE_COMPILERCHECK: "%compiler% -dumpmachine; %compiler% -dumpversion" - CCACHE_NOHASHDIR: "true" - CCACHE_HARDLINK: "true" - 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 -b common-${{ inputs.tag }} --repo-rev=v2.35 - REMOTE_BRANCH=$(git ls-remote https://android.googlesource.com/kernel/common ${{ inputs.tag }}) - DEFAULT_MANIFEST_PATH=.repo/manifests/default.xml - if grep -q deprecated <<< $REMOTE_BRANCH; then - echo "Found deprecated branch: ${{ inputs.tag }}" - sed -i 's/"${{ inputs.tag }}"/"deprecated\/${{ inputs.tag }}"/g' $DEFAULT_MANIFEST_PATH - cat $DEFAULT_MANIFEST_PATH - fi - 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 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" - printf "\nccflags-y += -DCONFIG_KSU_DEBUG\n" >> $GITHUB_WORKSPACE/KernelSU/kernel/Makefile - fi - repo status - echo "[+] KernelSU setup done." - - - name: Symbol magic - run: | - echo "[+] Export all symbol from abi_gki_aarch64.xml" - COMMON_ROOT=$GITHUB_WORKSPACE/android-kernel/common - KSU_ROOT=$GITHUB_WORKSPACE/KernelSU - ABI_XML=$COMMON_ROOT/android/abi_gki_aarch64.xml - SYMBOL_LIST=$COMMON_ROOT/android/abi_gki_aarch64 - # python3 $KSU_ROOT/scripts/abi_gki_all.py $ABI_XML > $SYMBOL_LIST - echo "[+] Add KernelSU symbols" - cat $KSU_ROOT/kernel/export_symbol.txt | awk '{sub("[ \t]+","");print " "$0}' >> $SYMBOL_LIST - - - name: Setup ccache - if: inputs.use_cache == true - 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: 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: | - if [ -e common/BUILD.bazel ]; then - sed -i '/^[[:space:]]*"protected_exports_list"[[:space:]]*:[[:space:]]*"android\/abi_gki_protected_exports_aarch64",$/d' common/BUILD.bazel - fi - 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 - export KSU_EXPECTED_SIZE=${{ vars.EXPECTED_SIZE }} - export KSU_EXPECTED_HASH=${{ vars.EXPECTED_HASH }} - fi - if [ -e build/build.sh ]; then - 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 - id: prepareArtifacts - run: | - OUTDIR=android-kernel/out/${{ inputs.version }}/dist - if [ ! -e $OUTDIR ]; then - OUTDIR=android-kernel/dist - fi - mkdir output - 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@v4 - if: ${{ inputs.build_lkm == false }} - with: - name: Image-${{ inputs.version_name }}_${{ inputs.os_patch_level }} - path: ./output/* - - - name: Upload AnyKernel3 - 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 diff --git a/.github/workflows/kpmmgr.yml b/.github/workflows/kpmmgr.yml deleted file mode 100644 index 18ba4513..00000000 --- a/.github/workflows/kpmmgr.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Build kpmmgr - -on: - push: - branches: [ "mian" ] - paths: - - '.github/workflows/kpmmgr.yml' - - 'userspace/kpmmgr/**' - workflow_dispatch: - workflow_call: - inputs: - target: - required: true - type: string - os: - required: false - type: string - default: self-hosted - -jobs: - build-susfs: - name: Build userspace kpmmgr - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Build kpmmgr - working-directory: ./userspace/kpmmgr - run: | - $ANDROID_NDK_HOME/ndk-build - - - name: Upload a Build Artifact - uses: actions/upload-artifact@v4 - with: - name: kpmmgr-aarch64-linux-android - path: ./userspace/kpmmgr/libs diff --git a/.github/workflows/ksud.yml b/.github/workflows/ksud.yml deleted file mode 100644 index 5fedf28d..00000000 --- a/.github/workflows/ksud.yml +++ /dev/null @@ -1,74 +0,0 @@ -name: Build ksud -on: - workflow_call: - inputs: - target: - required: true - type: string - os: - required: false - type: string - default: ubuntu-latest - pull_lkm: - required: false - type: boolean - default: true - pack_lkm: - required: false - type: boolean - default: true - use_cache: - required: false - type: boolean - default: true -jobs: - build: - runs-on: ${{ inputs.os }} - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Pull lkms from branch - if: ${{ inputs.pack_lkm && inputs.pull_lkm }} - uses: actions/checkout@v4 - with: - ref: lkm - path: lkm - - - name: Download lkms from artifacts - if: ${{ inputs.pack_lkm && !inputs.pull_lkm }} - uses: actions/download-artifact@v4 - - - name: Prepare LKM files - if: ${{ inputs.pack_lkm && inputs.pull_lkm }} - run: | - cp lkm/*_kernelsu.ko ./userspace/ksud/bin/aarch64/ - - - name: Prepare LKM files - if: ${{ inputs.pack_lkm && !inputs.pull_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: | - RUSTFLAGS="" cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1 - - - name: Build ksud - run: CROSS_NO_WARNINGS=0 cross build --target ${{ inputs.target }} --release --manifest-path ./userspace/ksud/Cargo.toml - - - name: Upload ksud artifact - uses: actions/upload-artifact@v4 - with: - name: ksud-${{ inputs.target }} - path: userspace/ksud/target/**/release/zakozako* diff --git a/.github/workflows/rustfmt.yml b/.github/workflows/rustfmt.yml deleted file mode 100644 index 18a178b6..00000000 --- a/.github/workflows/rustfmt.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Rustfmt check - -on: - push: - branches: - - 'main' - paths: - - '.github/workflows/rustfmt.yml' - - 'userspace/ksud/**' - pull_request: - branches: - - 'main' - paths: - - '.github/workflows/rustfmt.yml' - - 'userspace/ksud/**' - -permissions: - checks: write - -jobs: - format: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: dtolnay/rust-toolchain@nightly - with: - components: rustfmt - - - uses: LoliGothick/rustfmt-check@master - with: - token: ${{ github.token }} - working-directory: userspace/ksud diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml deleted file mode 100644 index e4287380..00000000 --- a/.github/workflows/shellcheck.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: ShellCheck - -on: - push: - branches: - - 'main' - paths: - - '.github/workflows/shellcheck.yml' - - '**/*.sh' - pull_request: - branches: - - 'main' - paths: - - '.github/workflows/shellcheck.yml' - - '**/*.sh' - -jobs: - shellcheck: - runs-on: self-hosted - steps: - - uses: actions/checkout@v4 - - - name: Run ShellCheck - uses: ludeeus/action-shellcheck@2.0.0 - with: - ignore_names: gradlew - ignore_paths: ./userspace/ksud/src/installer.sh diff --git a/.github/workflows/susfs.yml b/.github/workflows/susfs.yml deleted file mode 100644 index 957be1f1..00000000 --- a/.github/workflows/susfs.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Build susfs - -on: - push: - branches: [ "mian" ] - paths: - - '.github/workflows/susfs.yml' - - 'userspace/susfs/**' - workflow_dispatch: - workflow_call: - inputs: - target: - required: true - type: string - os: - required: false - type: string - default: self-hosted - -jobs: - build-susfs: - name: Build userspace susfs - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Build susfs - working-directory: ./userspace/susfs - run: | - $ANDROID_NDK_HOME/ndk-build - - - name: Upload a Build Artifact - uses: actions/upload-artifact@v4 - with: - name: susfs-aarch64-linux-android - path: ./userspace/susfs/libs diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 706fd07f..00000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -.idea -.vscode diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 83040d98..00000000 --- a/SECURITY.md +++ /dev/null @@ -1,7 +0,0 @@ -# Reporting Security Issues - -The KernelSU team and community take security bugs in KernelSU seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. - -To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/tiann/KernelSU/security/advisories/new) tab, or you can mailto [weishu](mailto:twsxtd@gmail.com) directly. - -The KernelSU team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. diff --git a/docs/README-en.md b/docs/README-en.md deleted file mode 100644 index f47303b9..00000000 --- a/docs/README-en.md +++ /dev/null @@ -1,112 +0,0 @@ -# SukiSU Ultra - -**English** | [简体中文](README.md) | [日本語](README-ja.md) - -Android device root solution based on [KernelSU](https://github.com/tiann/KernelSU) - -**Experimental! Use at your own risk!** This solution is based on [KernelSU](https://github.com/tiann/KernelSU) and is experimental! - -> This is an unofficial fork. All rights are reserved to [@tiann](https://github.com/tiann) -> -> However, we will be a separately maintained branch of KSU in the future - -- Fully adapted for non-GKI devices (susfs-dev and unsusfs-patched dev branches only) - -## How to add - -Use the susfs-stable or susfs-dev branch (integrated susfs with support for non-GKI devices) -``` -curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev -``` - -Use the main branch -``` -curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main -``` - -## How to use integrated susfs - -1. Use the susfs-dev branch directly without any patching - -## KPM support - -- We have removed duplicate KSU functions based on KernelPatch and retained KPM support. -- We will introduce more APatch-compatible functions to ensure the integrity of KPM functionality. - -Open source address: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch - -KPM template address: https://github.com/udochina/KPM-Build-Anywhere - -## More links - -Projects compiled based on Sukisu and susfs -- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) -- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) - -## Hook method -- This method references the hook method from (https://github.com/rsuntk/KernelSU) - -1. **KPROBES hook:** - - Also used for Loadable Kernel Module (LKM) - - Default hook method on GKI kernels. - - Need `CONFIG_KPROBES=y` - -2. **Manual hook:** - - Standard KernelSU hook: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source - - backslashxx's syscall manual hook: https://github.com/backslashxx/KernelSU/issues/5 - - Default hook method on Non-GKI kernels. - - Need `CONFIG_KSU_MANUAL_HOOK=y` - -## Usage - -### GKI - -Please follow this guide. - -https://kernelsu.org/guide/installation.html - - -### OnePlus - -1. Use the link mentioned in the 'More Links' section to create a customized build with your device information, and then flash the zip file with the AnyKernel3 suffix. - -> [!Note] -> - You only need to fill in the first two parts of kernel versions, such as 5.10, 5.15, 6.1, or 6.6. -> - Please search for the processor codename by yourself, usually it is all English without numbers. -> - You can find the branch and configuration files from the OnePlus open-source kernel repository. - -## Features - -1. Kernel-based `su` and root access management. -2. Not based on [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) module system, but based on [Magic Mount](https://github.com/5ec1cff/KernelSU) from 5ec1cff -3. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock root privileges in a cage. -4. Bringing back non-GKI/GKI 1.0 support -5. More customization -6. Support for KPM kernel modules - -## License - -- The file in the “kernel” directory is under [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) license. -- All other parts except the “kernel” directory are under [GPL-3.0 or later](https://www.gnu.org/licenses/gpl-3.0.html) license. - -## Sponsorship list - -- [Ktouls](https://github.com/Ktouls) Thanks so much for bringing me support -- [zaoqi123](https://github.com/zaoqi123) It's not a bad idea to buy me a milk tea -- [wswzgdg](https://github.com/wswzgdg) Many thanks for supporting this project -- [yspbwx2010](https://github.com/yspbwx2010) Many thanks -- [DARKWWEE](https://github.com/DARKWWEE) Thanks for the 100 USDT Lao - -If the above list does not have your name, I will update it as soon as possible, and thanks again for your support! - -## Contributions - -- [KernelSU](https://github.com/tiann/KernelSU): original project -- [MKSU](https://github.com/5ec1cff/KernelSU): Used project -- [RKSU](https://github.com/rsuntk/KernelsU): Reintroduced the support of non-GKI devices using the kernel of this project -- [susfs](https://gitlab.com/simonpunk/susfs4ksu):Used susfs file system -- [KernelSU](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU conceptualization -- [Magisk](https://github.com/topjohnwu/Magisk): Powerful root utility -- [genuine](https://github.com/brevent/genuine/): APK v2 Signature Verification -- [Diamorphine](https://github.com/m0nad/Diamorphine): Some rootkit utilities. -- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch is a key part of the APatch implementation of the kernel module diff --git a/docs/README-ja.md b/docs/README-ja.md deleted file mode 100644 index 51ae7a09..00000000 --- a/docs/README-ja.md +++ /dev/null @@ -1,113 +0,0 @@ -# SukiSU Ultra - -**日本語** | [简体中文](README.md) | [English](README-en.md) - -[KernelSU](https://github.com/tiann/KernelSU) をベースとした Android デバイスの root ソリューション - -**試験中なビルドです!自己責任で使用してください!**
-このソリューションは [KernelSU](https://github.com/tiann/KernelSU) に基づいていますが、試験中なビルドです。 - -> これは非公式なフォークです。すべての権利は [@tiann](https://github.com/tiann) に帰属します。 -> -> ただし、将来的には KSU とは別に管理されるブランチとなる予定です。 - -- GKI 非対応なデバイスに完全に適応 (susfs-dev と unsusfs-patched dev ブランチのみ) - -## 追加方法 - -susfs-stable または susfs-dev ブランチ (GKI 非対応デバイスに対応する統合された susfs) 使用してください。 -``` -curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev -``` - -メインブランチを使用する場合 -``` -curl -LSs "https://raw.githubusercontent.com/ShirkNeko/KernelSU/main/kernel/setup.sh" | bash -s main -``` -## 統合された susfs の使い方 - -1. パッチを当てずに susfs-dev ブランチを直接使用してください。 - -## KPM に対応 - -- KernelPatch に基づいて重複した KSU の機能を削除、KPM の対応を維持させています。 -- KPM 機能の整合性を確保するために、APatch の互換機能を更に向上させる予定です。 - -オープンソースアドレス: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch - -KPM テンプレートのアドレス: https://github.com/udochina/KPM-Build-Anywhere - -## その他のリンク - -SukiSU と susfs をベースにコンパイルされたプロジェクトです。 - -- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) -- [OnePlus](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) - -## フックの方式 - -- この方式は (https://github.com/rsuntk/KernelSU) のフック方式を参照してください。 - -1. **KPROBES フック:** - - 読み込み可能なカーネルモジュールの場合 (LKM) - - GKI カーネルのデフォルトとなるフック方式 - - `CONFIG_KPROBES=y` が必要です - -2. **手動でフック:** - - 標準の KernelSU フック: https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source - - backslashxx syscall フック: https://github.com/backslashxx/KernelSU/issues/5 - - 非 GKI カーネル用のデフォルトフッキングメソッド - - `CONFIG_KSU_MANUAL_HOOK=y` が必要です - -## 使い方 - -### GKI - -このガイドに従ってください。 - -https://kernelsu.org/ja_JP/guide/installation.html - -### OnePlus - -1. `その他のリンク`の項目に記載されているリンクを開き、デバイス情報を使用してカスタマイズされたカーネルをビルドし、AnyKernel3 の接頭辞を持つ .zip ファイルをフラッシュします。 - -> [!Note] -> - 5.10、5.15、6.1、6.6 などのカーネルバージョンの最初の 2 文字のみを入力する必要があります。 -> - SoC のコードネームは自分で検索してください。通常は、数字がなく英語表記のみです。 -> - ブランチと構成ファイルは、OnePlus オープンソースカーネルリポジトリから見つけることができます。 - -## 機能 - -1. カーネルベースな `su` および root アクセスの管理。 -2. [OverlayFS](https://en.wikipedia.org/wiki/OverlayFS) モジュールシステムではなく、 5ec1cff 氏の [Magic Mount](https://github.com/5ec1cff/KernelSU) に基づいています。 -3. [アプリプロファイル](https://kernelsu.org/guide/app-profile.html): root 権限をケージ内にロックします。 -4. 非 GKI / GKI 1.0 の対応を復活 -5. その他のカスタマイズ -6. KPM カーネルモジュールに対応 - -## ライセンス - -- “kernel” ディレクトリ内のファイルは [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.ja.html) のみライセンス下にあります。 -- “kernel” ディレクトリを除くその他すべての部分は [GPL-3.0 またはそれ以降](https://www.gnu.org/licenses/gpl-3.0.html) のライセンス下にあります。 - -## スポンサーシップの一覧 - -- [Ktouls](https://github.com/Ktouls) 応援をしてくれたことに感謝。 -- [zaoqi123](https://github.com/zaoqi123) ミルクティーを買ってあげるのも良い考えですね。 -- [wswzgdg](https://github.com/wswzgdg) このプロジェクトを支援していただき、ありがとうございます。 -- [yspbwx2010](https://github.com/yspbwx2010) どうもありがとう。 -- [DARKWWEE](https://github.com/DARKWWEE) ラオウ100USDTありがとう! - -上記の一覧にあなたの名前がない場合は、できるだけ早急に更新しますので再度ご支援をお願いします。 - -## 貢献者 - -- [KernelSU](https://github.com/tiann/KernelSU): オリジナルのプロジェクトです。 -- [MKSU](https://github.com/5ec1cff/KernelSU): 使用しているプロジェクトです。 -- [RKSU](https://github.com/rsuntk/KernelsU): このプロジェクトのカーネルを使用して非 GKI デバイスのサポートを追加しています。 -- [susfs](https://gitlab.com/simonpunk/susfs4ksu):使用している susfs ファイルシステムです。 -- [KernelSU](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 ユーティリティを使用しています。 -- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch はカーネルモジュールの APatch 実装での重要な部分となります。 diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 3173c8e9..00000000 --- a/docs/README.md +++ /dev/null @@ -1,116 +0,0 @@ -# SukiSU Ultra - -**简体中文** | [English](README-en.md) | [日本語](README-ja.md) - -基于 [KernelSU](https://github.com/tiann/KernelSU) 的安卓设备 root 解决方案 - -**实验性! 使用风险自负!** - -> 这是非官方分支,保留所有权利 [@tiann](https://github.com/tiann) -> -> 但是,我们将会在未来成为一个单独维护的 KSU 分支 - -## 如何添加 - -在内核源码的根目录下执行以下命令: - -使用 susfs-dev 分支(已集成 susfs,带非 GKI 设备的支持) -``` -curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s susfs-dev -``` - -使用 main 分支 -``` -curl -LSs "https://raw.githubusercontent.com/ShirkNeko/SukiSU-Ultra/main/kernel/setup.sh" | bash -s main -``` - -## 如何集成 susfs - -1. 直接使用 susfs-stable 或者 susfs-dev 分支,不需要再集成 susfs - -## 钩子方法 - -- 此部分引用自 [rsuntk 的钩子方法](https://github.com/rsuntk/KernelSU) - -1. **KPROBES 钩子:** - - 用于可加载内核模块 (LKM) - - GKI 2.0 内核的默认钩子方法 - - 需要 `CONFIG_KPROBES=y` - -2. **手动钩子:** - - 标准的 KernelSU 钩子:https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#manually-modify-the-kernel-source - - backslashxx 的 syscall 手动钩子:https://github.com/backslashxx/KernelSU/issues/5 - - 非 GKI 内核的默认挂钩方法 - - 需要 `CONFIG_KSU_MANUAL_HOOK=y` - -## KPM 支持 - -- 我们基于 KernelPatch 去掉了和 KSU 重复的功能,仅保留了 KPM 支持 -- 我们将会引入更多的兼容 APatch 的函数来确保 KPM 功能的完整性 - -开源地址: https://github.com/ShirkNeko/SukiSU_KernelPatch_patch - -KPM 模板地址: https://github.com/udochina/KPM-Build-Anywhere - -## 更多链接 - -基于 SukiSU 和 susfs 编译的项目 -- [GKI](https://github.com/ShirkNeko/GKI_KernelSU_SUSFS) -- [一加](https://github.com/ShirkNeko/Action_OnePlus_MKSU_SUSFS) - -## 使用方法 - -### 普适的 GKI - -请**全部**参考 https://kernelsu.org/zh_CN/guide/installation.html - -> [!Note] -> 1. 适用于如小米、红米、三星等的 GKI 2.0 的设备 (不包含魔改内核的厂商如魅族、一加、真我和 oppo) -> 2. 找到[更多链接](#%E6%9B%B4%E5%A4%9A%E9%93%BE%E6%8E%A5)里的 GKI 构建的项目。找到设备内核版本。然后下载下来,用TWRP或者内核刷写工具刷入带 AnyKernel3 后缀的压缩包即可 -> 3. 一般不带后缀的 .zip 压缩包是未压缩的,gz 后缀的为天玑机型所使用的压缩方式 - - -### 一加 - -1.找到更多链接里的一加项目进行自行填写,然后云编译构建,最后刷入带 AnyKernel3 后缀的压缩包即可 - -> [!Note] -> - 内核版本只需要填写前两位即可,如 5.10,5.15,6.1,6.6 -> - 处理器代号请自行搜索,一般为全英文不带数字的代号 -> - 分支和配置文件请自行到一加内核开源地址进行填写 - -## 特点 - -1. 基于内核的 `su` 和 root 访问管理 -2. 基于 5ec1cff 的 [Magic Mount](https://github.com/5ec1cff/KernelSU) 的模块系统 -3. [App Profile](https://kernelsu.org/guide/app-profile.html):将 root 权限锁在笼子里 -4. 恢复对非 GKI 2.0 内核的支持 -5. 更多自定义功能 -6. 对 KPM 内核模块的支持 - -## 许可证 - -- `kernel` 目录下的文件是 [GPL-2.0-only](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)。 - -## 赞助名单 - -- [Ktouls](https://github.com/Ktouls) 非常感谢你给我带来的支持 -- [zaoqi123](https://github.com/zaoqi123) 请我喝奶茶也不错 -- [wswzgdg](https://github.com/wswzgdg) 非常感谢对此项目的支持 -- [yspbwx2010](https://github.com/yspbwx2010) 非常感谢 -- [DARKWWEE](https://github.com/DARKWWEE) 感谢老哥的 100 USDT - -如果以上名单没有你的名称,我会及时更新,再次感谢大家的支持 - -## 贡献 - -- [KernelSU](https://github.com/tiann/KernelSU):原始项目 -- [MKSU](https://github.com/5ec1cff/KernelSU):使用的项目 -- [RKSU](https://github.com/rsuntk/KernelsU):使用该项目的 kernel 对非GKI设备重新进行支持 -- [susfs4ksu](https://gitlab.com/simonpunk/susfs4ksu):使用的 susfs 文件系统 -- [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 技能 -- [KernelPatch](https://github.com/bmax121/KernelPatch): KernelPatch 是 APatch 实现内核模块的关键部分 \ No newline at end of file diff --git a/js/README.md b/js/README.md deleted file mode 100644 index 422377dd..00000000 --- a/js/README.md +++ /dev/null @@ -1,121 +0,0 @@ -# Library for KernelSU's module WebUI - -## Install - -```sh -yarn add kernelsu -``` - -## API - -### exec - -Spawns a **root** shell and runs a command within that shell, returning a Promise that resolves with the `stdout` and `stderr` outputs upon completion. - -- `command` `` The command to run, with space-separated arguments. -- `options` `` - - `cwd` - Current working directory of the child process. - - `env` - Environment key-value pairs. - -```javascript -import { exec } from 'kernelsu'; - -const { errno, stdout, stderr } = await exec('ls -l', { cwd: '/tmp' }); -if (errno === 0) { - // success - console.log(stdout); -} -``` - -### spawn - -Spawns a new process using the given `command` in **root** shell, with command-line arguments in `args`. If omitted, `args` defaults to an empty array. - -Returns a `ChildProcess` instance. Instances of `ChildProcess` represent spawned child processes. - -- `command` `` The command to run. -- `args` `` List of string arguments. -- `options` ``: - - `cwd` `` - Current working directory of the child process. - - `env` `` - Environment key-value pairs. - -Example of running `ls -lh /data`, capturing `stdout`, `stderr`, and the exit code: - -```javascript -import { spawn } from 'kernelsu'; - -const ls = spawn('ls', ['-lh', '/data']); - -ls.stdout.on('data', (data) => { - console.log(`stdout: ${data}`); -}); - -ls.stderr.on('data', (data) => { - console.log(`stderr: ${data}`); -}); - -ls.on('exit', (code) => { - console.log(`child process exited with code ${code}`); -}); -``` - -#### ChildProcess - -##### Event 'exit' - -- `code` `` The exit code if the child process exited on its own. - -The `'exit'` event is emitted when the child process ends. If the process exits, `code` contains the final exit code; otherwise, it is null. - -##### Event 'error' - -- `err` `` The error. - -The `'error'` event is emitted whenever: - -- The process could not be spawned. -- The process could not be killed. - -##### `stdout` - -A `Readable Stream` that represents the child process's `stdout`. - -```javascript -const subprocess = spawn('ls'); - -subprocess.stdout.on('data', (data) => { - console.log(`Received chunk ${data}`); -}); -``` - -#### `stderr` - -A `Readable Stream` that represents the child process's `stderr`. - -### fullScreen - -Request the WebView enter/exit full screen. - -```javascript -import { fullScreen } from 'kernelsu'; -fullScreen(true); -``` - -### 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()); -``` diff --git a/js/index.d.ts b/js/index.d.ts deleted file mode 100644 index c9278175..00000000 --- a/js/index.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index 29b928ac..00000000 --- a/js/index.js +++ /dev/null @@ -1,119 +0,0 @@ -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 deleted file mode 100644 index 12002a05..00000000 --- a/js/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "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 deleted file mode 100644 index 51bef767..00000000 --- a/justfile +++ /dev/null @@ -1,14 +0,0 @@ -alias bk := build_ksud -alias bm := build_manager - -build_ksud: - cross build --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml - -build_manager: build_ksud - cp userspace/ksud/target/aarch64-linux-android/release/ksud manager/app/src/main/jniLibs/arm64-v8a/libksud.so - cd manager && ./gradlew aDebug - -clippy: - cargo fmt --manifest-path ./userspace/ksud/Cargo.toml - cross clippy --target x86_64-pc-windows-gnu --release --manifest-path ./userspace/ksud/Cargo.toml - cross clippy --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml diff --git a/manager/.gitignore b/manager/.gitignore deleted file mode 100644 index a595ddf7..00000000 --- a/manager/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -*.iml -.gradle -.idea -.kotlin -.DS_Store -build -captures -.cxx -local.properties -key.jks diff --git a/manager/app/.gitignore b/manager/app/.gitignore deleted file mode 100644 index dc5ca963..00000000 --- a/manager/app/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/build -/release/ diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts deleted file mode 100644 index b04edb38..00000000 --- a/manager/app/build.gradle.kts +++ /dev/null @@ -1,157 +0,0 @@ -@file:Suppress("UnstableApiUsage") - -import com.android.build.api.dsl.ApkSigningConfig -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") - - -} - -val managerVersionCode: Int by rootProject.extra -val managerVersionName: String by rootProject.extra - -apksign { - storeFileProperty = "KEYSTORE_FILE" - storePasswordProperty = "KEYSTORE_PASSWORD" - keyAliasProperty = "KEY_ALIAS" - keyPasswordProperty = "KEY_PASSWORD" -} - - -android { - - /**signingConfigs { - create("Debug") { - storeFile = file("D:\\other\\AndroidTool\\android_key\\keystore\\release-key.keystore") - storePassword = "" - keyAlias = "" - keyPassword = "" - } - }**/ - namespace = "com.sukisu.ultra" - - buildTypes { - release { - isMinifyEnabled = true - isShrinkResources = true - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - /**debug { - signingConfig = signingConfigs.named("Debug").get() as ApkSigningConfig - }**/ - } - - buildFeatures { - aidl = true - buildConfig = true - compose = true - prefab = true - } - - kotlinOptions { - jvmTarget = "21" - } - - packaging { - jniLibs { - useLegacyPackaging = true - } - resources { - // 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" - } - } - - externalNativeBuild { - cmake { - path("src/main/cpp/CMakeLists.txt") - } - } - - applicationVariants.all { - outputs.forEach { - val output = it as BaseVariantOutputImpl - output.outputFileName = "SukiSU_${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 { - implementation(libs.androidx.activity.compose) - implementation(libs.androidx.navigation.compose) - - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.material.icons.extended) - implementation(libs.androidx.compose.material) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.ui.tooling.preview) - implementation(libs.androidx.foundation) - implementation(libs.androidx.documentfile) - - debugImplementation(libs.androidx.compose.ui.test.manifest) - debugImplementation(libs.androidx.compose.ui.tooling) - - implementation(libs.androidx.lifecycle.runtime.compose) - implementation(libs.androidx.lifecycle.runtime.ktx) - implementation(libs.androidx.lifecycle.viewmodel.compose) - - implementation(libs.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) - - implementation(libs.io.coil.kt.coil.compose) - - implementation(libs.kotlinx.coroutines.core) - - implementation(libs.me.zhanghai.android.appiconloader.coil) - - implementation(libs.sheet.compose.dialogs.core) - implementation(libs.sheet.compose.dialogs.list) - implementation(libs.sheet.compose.dialogs.input) - - implementation(libs.markdown) - implementation(libs.androidx.webkit) - - implementation(libs.lsposed.cxx) - - implementation(libs.com.github.topjohnwu.libsu.core) - -} \ No newline at end of file diff --git a/manager/app/proguard-rules.pro b/manager/app/proguard-rules.pro deleted file mode 100644 index e69de29b..00000000 diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml deleted file mode 100644 index 59a93490..00000000 --- a/manager/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl b/manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl deleted file mode 100644 index 93f84492..00000000 --- a/manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl +++ /dev/null @@ -1,8 +0,0 @@ -package com.sukisu.zako; - -import android.content.pm.PackageInfo; -import rikka.parcelablelist.ParcelableListSlice; - -interface IKsuInterface { - ParcelableListSlice getPackages(int flags); -} \ No newline at end of file diff --git a/manager/app/src/main/assets/5_10-mkbootfs b/manager/app/src/main/assets/5_10-mkbootfs deleted file mode 100644 index 2af1167a..00000000 Binary files a/manager/app/src/main/assets/5_10-mkbootfs and /dev/null differ diff --git a/manager/app/src/main/assets/5_15+-mkbootfs b/manager/app/src/main/assets/5_15+-mkbootfs deleted file mode 100644 index 2eca159b..00000000 Binary files a/manager/app/src/main/assets/5_15+-mkbootfs and /dev/null differ diff --git a/manager/app/src/main/cpp/CMakeLists.txt b/manager/app/src/main/cpp/CMakeLists.txt deleted file mode 100644 index 759af3c8..00000000 --- a/manager/app/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,21 +0,0 @@ - -# For more information about using CMake with Android Studio, read the -# documentation: https://d.android.com/studio/projects/add-native-code.html - -# Sets the minimum version of CMake required to build the native library. -cmake_minimum_required(VERSION 3.18.1) - -project("kernelsu") - -find_package(cxx REQUIRED CONFIG) -link_libraries(cxx::cxx) - -add_library(zako - SHARED - jni.cc - ksu.cc - ) - -find_library(log-lib log) - -target_link_libraries(zako ${log-lib}) \ No newline at end of file diff --git a/manager/app/src/main/cpp/jni.cc b/manager/app/src/main/cpp/jni.cc deleted file mode 100644 index 05463b35..00000000 --- a/manager/app/src/main/cpp/jni.cc +++ /dev/null @@ -1,308 +0,0 @@ -#include - -#include - -#include -#include - -#include "ksu.h" - -#define LOG_TAG "KernelSU" -#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) - -extern "C" -JNIEXPORT jboolean JNICALL -Java_com_sukisu_ultra_Natives_becomeManager(JNIEnv *env, jobject, jstring pkg) { - auto cpkg = env->GetStringUTFChars(pkg, nullptr); - auto result = become_manager(cpkg); - env->ReleaseStringUTFChars(pkg, cpkg); - return result; -} - -extern "C" -JNIEXPORT jint JNICALL -Java_com_sukisu_ultra_Natives_getVersion(JNIEnv *env, jobject) { - return get_version(); -} - -extern "C" -JNIEXPORT jintArray JNICALL -Java_com_sukisu_ultra_Natives_getAllowList(JNIEnv *env, jobject) { - int uids[1024]; - int size = 0; - bool result = get_allow_list(uids, &size); - LOGD("getAllowList: %d, size: %d", result, size); - if (result) { - auto array = env->NewIntArray(size); - env->SetIntArrayRegion(array, 0, size, uids); - return array; - } - return env->NewIntArray(0); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_com_sukisu_ultra_Natives_isSafeMode(JNIEnv *env, jclass clazz) { - return is_safe_mode(); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_com_sukisu_ultra_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"); - auto integerCls = env->FindClass("java/lang/Integer"); - auto constructor = env->GetMethodID(integerCls, "", "(I)V"); - for (int i = 0; i < count; ++i) { - auto integer = env->NewObject(integerCls, constructor, data[i]); - env->CallBooleanMethod(list, add, integer); - } -} - -static void addIntToList(JNIEnv *env, jobject list, int ele) { - auto cls = env->GetObjectClass(list); - auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z"); - auto integerCls = env->FindClass("java/lang/Integer"); - auto constructor = env->GetMethodID(integerCls, "", "(I)V"); - auto integer = env->NewObject(integerCls, constructor, ele); - env->CallBooleanMethod(list, add, integer); -} - -static uint64_t capListToBits(JNIEnv *env, jobject list) { - auto cls = env->GetObjectClass(list); - auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;"); - auto size = env->GetMethodID(cls, "size", "()I"); - auto listSize = env->CallIntMethod(list, size); - auto integerCls = env->FindClass("java/lang/Integer"); - auto intValue = env->GetMethodID(integerCls, "intValue", "()I"); - uint64_t result = 0; - for (int i = 0; i < listSize; ++i) { - auto integer = env->CallObjectMethod(list, get, i); - int data = env->CallIntMethod(integer, intValue); - - if (cap_valid(data)) { - result |= (1ULL << data); - } - } - - return result; -} - -static int getListSize(JNIEnv *env, jobject list) { - auto cls = env->GetObjectClass(list); - auto size = env->GetMethodID(cls, "size", "()I"); - return env->CallIntMethod(list, size); -} - -static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) { - auto cls = env->GetObjectClass(list); - auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;"); - auto integerCls = env->FindClass("java/lang/Integer"); - auto intValue = env->GetMethodID(integerCls, "intValue", "()I"); - for (int i = 0; i < count; ++i) { - auto integer = env->CallObjectMethod(list, get, i); - data[i] = env->CallIntMethod(integer, intValue); - } -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_com_sukisu_ultra_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) { - if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) { - return nullptr; - } - - p_key_t key = {}; - auto cpkg = env->GetStringUTFChars(pkg, nullptr); - strcpy(key, cpkg); - env->ReleaseStringUTFChars(pkg, cpkg); - - app_profile profile = {}; - profile.version = KSU_APP_PROFILE_VER; - - strcpy(profile.key, key); - profile.current_uid = uid; - - bool useDefaultProfile = !get_app_profile(key, &profile); - - auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile"); - auto constructor = env->GetMethodID(cls, "", "()V"); - auto obj = env->NewObject(cls, constructor); - auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;"); - auto currentUidField = env->GetFieldID(cls, "currentUid", "I"); - auto allowSuField = env->GetFieldID(cls, "allowSu", "Z"); - - auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z"); - auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;"); - - auto uidField = env->GetFieldID(cls, "uid", "I"); - auto gidField = env->GetFieldID(cls, "gid", "I"); - auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;"); - auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;"); - auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;"); - auto namespacesField = env->GetFieldID(cls, "namespace", "I"); - - auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z"); - auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z"); - - env->SetObjectField(obj, keyField, env->NewStringUTF(profile.key)); - env->SetIntField(obj, currentUidField, profile.current_uid); - - if (useDefaultProfile) { - // no profile found, so just use default profile: - // don't allow root and use default profile! - LOGD("use default profile for: %s, %d", key, uid); - - // allow_su = false - // non root use default = true - env->SetBooleanField(obj, allowSuField, false); - env->SetBooleanField(obj, nonRootUseDefaultField, true); - - return obj; - } - - auto allowSu = profile.allow_su; - - if (allowSu) { - env->SetBooleanField(obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default); - if (strlen(profile.rp_config.template_name) > 0) { - env->SetObjectField(obj, rootTemplateField, - env->NewStringUTF(profile.rp_config.template_name)); - } - - env->SetIntField(obj, uidField, profile.rp_config.profile.uid); - env->SetIntField(obj, gidField, profile.rp_config.profile.gid); - - jobject groupList = env->GetObjectField(obj, groupsField); - int groupCount = profile.rp_config.profile.groups_count; - if (groupCount > KSU_MAX_GROUPS) { - LOGD("kernel group count too large: %d???", groupCount); - groupCount = KSU_MAX_GROUPS; - } - fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount); - - jobject capList = env->GetObjectField(obj, capabilitiesField); - for (int i = 0; i <= CAP_LAST_CAP; i++) { - if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) { - addIntToList(env, capList, i); - } - } - - env->SetObjectField(obj, domainField, - env->NewStringUTF(profile.rp_config.profile.selinux_domain)); - env->SetIntField(obj, namespacesField, profile.rp_config.profile.namespaces); - env->SetBooleanField(obj, allowSuField, profile.allow_su); - } else { - env->SetBooleanField(obj, nonRootUseDefaultField, - (jboolean) profile.nrp_config.use_default); - env->SetBooleanField(obj, umountModulesField, profile.nrp_config.profile.umount_modules); - } - - return obj; -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_com_sukisu_ultra_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) { - auto cls = env->FindClass("com/sukisu/ultra/Natives$Profile"); - - auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;"); - auto currentUidField = env->GetFieldID(cls, "currentUid", "I"); - auto allowSuField = env->GetFieldID(cls, "allowSu", "Z"); - - auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z"); - auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;"); - - auto uidField = env->GetFieldID(cls, "uid", "I"); - auto gidField = env->GetFieldID(cls, "gid", "I"); - auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;"); - auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;"); - auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;"); - auto namespacesField = env->GetFieldID(cls, "namespace", "I"); - - auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z"); - auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z"); - - auto key = env->GetObjectField(profile, keyField); - if (!key) { - return false; - } - if (env->GetStringLength((jstring) key) > KSU_MAX_PACKAGE_NAME) { - return false; - } - - auto cpkg = env->GetStringUTFChars((jstring) key, nullptr); - p_key_t p_key = {}; - strcpy(p_key, cpkg); - env->ReleaseStringUTFChars((jstring) key, cpkg); - - auto currentUid = env->GetIntField(profile, currentUidField); - - auto uid = env->GetIntField(profile, uidField); - auto gid = env->GetIntField(profile, gidField); - auto groups = env->GetObjectField(profile, groupsField); - auto capabilities = env->GetObjectField(profile, capabilitiesField); - auto domain = env->GetObjectField(profile, domainField); - auto allowSu = env->GetBooleanField(profile, allowSuField); - auto umountModules = env->GetBooleanField(profile, umountModulesField); - - app_profile p = {}; - p.version = KSU_APP_PROFILE_VER; - - strcpy(p.key, p_key); - p.allow_su = allowSu; - p.current_uid = currentUid; - - if (allowSu) { - p.rp_config.use_default = env->GetBooleanField(profile, rootUseDefaultField); - auto templateName = env->GetObjectField(profile, rootTemplateField); - if (templateName) { - auto ctemplateName = env->GetStringUTFChars((jstring) templateName, nullptr); - strcpy(p.rp_config.template_name, ctemplateName); - env->ReleaseStringUTFChars((jstring) templateName, ctemplateName); - } - - p.rp_config.profile.uid = uid; - p.rp_config.profile.gid = gid; - - int groups_count = getListSize(env, groups); - if (groups_count > KSU_MAX_GROUPS) { - LOGD("groups count too large: %d", groups_count); - return false; - } - p.rp_config.profile.groups_count = groups_count; - fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count); - - p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities); - - auto cdomain = env->GetStringUTFChars((jstring) domain, nullptr); - strcpy(p.rp_config.profile.selinux_domain, cdomain); - env->ReleaseStringUTFChars((jstring) domain, cdomain); - - p.rp_config.profile.namespaces = env->GetIntField(profile, namespacesField); - } else { - p.nrp_config.use_default = env->GetBooleanField(profile, nonRootUseDefaultField); - p.nrp_config.profile.umount_modules = umountModules; - } - - return set_app_profile(&p); -} -extern "C" -JNIEXPORT jboolean JNICALL -Java_com_sukisu_ultra_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) { - return uid_should_umount(uid); -} -extern "C" -JNIEXPORT jboolean JNICALL -Java_com_sukisu_ultra_Natives_isSuEnabled(JNIEnv *env, jobject thiz) { - return is_su_enabled(); -} -extern "C" -JNIEXPORT jboolean JNICALL -Java_com_sukisu_ultra_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) { - return set_su_enabled(enabled); -} \ No newline at end of file diff --git a/manager/app/src/main/cpp/ksu.cc b/manager/app/src/main/cpp/ksu.cc deleted file mode 100644 index 720c5fd0..00000000 --- a/manager/app/src/main/cpp/ksu.cc +++ /dev/null @@ -1,99 +0,0 @@ -// -// Created by weishu on 2022/12/9. -// - -#include -#include -#include -#include -#include - -#include "ksu.h" - -#define KERNEL_SU_OPTION 0xDEADBEEF - -#define CMD_GRANT_ROOT 0 - -#define CMD_BECOME_MANAGER 1 -#define CMD_GET_VERSION 2 -#define CMD_ALLOW_SU 3 -#define CMD_DENY_SU 4 -#define CMD_GET_SU_LIST 5 -#define CMD_GET_DENY_LIST 6 -#define CMD_CHECK_SAFEMODE 9 - -#define CMD_GET_APP_PROFILE 10 -#define CMD_SET_APP_PROFILE 11 - -#define CMD_IS_UID_GRANTED_ROOT 12 -#define CMD_IS_UID_SHOULD_UMOUNT 13 -#define CMD_IS_SU_ENABLED 14 -#define CMD_ENABLE_SU 15 - -static bool ksuctl(int cmd, void* arg1, void* arg2) { - int32_t result = 0; - prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result); - return result == KERNEL_SU_OPTION; -} - -bool become_manager(const char* pkg) { - char param[128]; - uid_t uid = getuid(); - uint32_t userId = uid / 100000; - if (userId == 0) { - sprintf(param, "/data/data/%s", pkg); - } else { - snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg); - } - - 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; - int32_t lkm = 0; - ksuctl(CMD_GET_VERSION, &version, &lkm); - if (!is_lkm && lkm != 0) { - is_lkm = true; - } - return version; -} - -bool get_allow_list(int *uids, int *size) { - return ksuctl(CMD_GET_SU_LIST, uids, size); -} - -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; -} - -bool set_app_profile(const app_profile *profile) { - return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, nullptr); -} - -bool get_app_profile(p_key_t key, app_profile *profile) { - return ksuctl(CMD_GET_APP_PROFILE, (void*) profile, nullptr); -} - -bool set_su_enabled(bool enabled) { - return ksuctl(CMD_ENABLE_SU, (void*) enabled, nullptr); -} - -bool is_su_enabled() { - bool enabled = true; - // if ksuctl failed, we assume su is enabled, and it cannot be disabled. - ksuctl(CMD_IS_SU_ENABLED, &enabled, nullptr); - return enabled; -} \ No newline at end of file diff --git a/manager/app/src/main/cpp/ksu.h b/manager/app/src/main/cpp/ksu.h deleted file mode 100644 index 3854356a..00000000 --- a/manager/app/src/main/cpp/ksu.h +++ /dev/null @@ -1,86 +0,0 @@ -// -// Created by weishu on 2022/12/9. -// - -#ifndef KERNELSU_KSU_H -#define KERNELSU_KSU_H - -#include - -bool become_manager(const char *); - -int get_version(); - -bool get_allow_list(int *uids, int *size); - -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. -#define KSU_MAX_GROUPS 32 -#define KSU_SELINUX_DOMAIN 64 - -using p_key_t = char[KSU_MAX_PACKAGE_NAME]; - -struct root_profile { - int32_t uid; - int32_t gid; - - int32_t groups_count; - int32_t groups[KSU_MAX_GROUPS]; - - // kernel_cap_t is u32[2] for capabilities v3 - struct { - uint64_t effective; - uint64_t permitted; - uint64_t inheritable; - } capabilities; - - char selinux_domain[KSU_SELINUX_DOMAIN]; - - int32_t namespaces; -}; - -struct non_root_profile { - bool umount_modules; -}; - -struct app_profile { - // It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this. - uint32_t version; - - // this is usually the package of the app, but can be other value for special apps - char key[KSU_MAX_PACKAGE_NAME]; - int32_t current_uid; - bool allow_su; - - union { - struct { - bool use_default; - char template_name[KSU_MAX_PACKAGE_NAME]; - - struct root_profile profile; - } rp_config; - - struct { - bool use_default; - - struct non_root_profile profile; - } nrp_config; - }; -}; - -bool set_app_profile(const app_profile *profile); - -bool get_app_profile(p_key_t key, app_profile *profile); - -bool set_su_enabled(bool enabled); - -bool is_su_enabled(); - -#endif //KERNELSU_KSU_H diff --git a/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt b/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt deleted file mode 100644 index fb6360a5..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.sukisu.ultra - -import android.app.Application -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 - -class KernelSUApplication : Application() { - - override fun onCreate() { - super.onCreate() - ksuApp = this - - val context = this - val iconSize = resources.getDimensionPixelSize(android.R.dimen.app_icon_size) - Coil.setImageLoader( - ImageLoader.Builder(context) - .components { - add(AppIconKeyer()) - add(AppIconFetcher.Factory(iconSize, false, context)) - } - .build() - ) - - val webroot = File(dataDir, "webroot") - if (!webroot.exists()) { - webroot.mkdir() - } - } - - -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/Kernels.kt b/manager/app/src/main/java/com/sukisu/ultra/Kernels.kt deleted file mode 100644 index 26219505..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/Kernels.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.sukisu.ultra - -import android.system.Os - -/** - * @author weishu - * @date 2022/12/10. - */ - -data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) { - override fun toString(): String { - return "$major.$patchLevel.$subLevel" - } - - fun isGKI(): Boolean { - - // kernel 6.x - if (major > 5) { - return true - } - - // kernel 5.10.x - if (major == 5) { - return patchLevel >= 10 - } - - return false - } -} - -fun parseKernelVersion(version: String): KernelVersion { - val find = "(\\d+)\\.(\\d+)\\.(\\d+)".toRegex().find(version) - return if (find != null) { - KernelVersion(find.groupValues[1].toInt(), find.groupValues[2].toInt(), find.groupValues[3].toInt()) - } else { - KernelVersion(-1, -1, -1) - } -} - -fun getKernelVersion(): KernelVersion { - Os.uname().release.let { - return parseKernelVersion(it) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt deleted file mode 100644 index ed65fed8..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt +++ /dev/null @@ -1,137 +0,0 @@ -package com.sukisu.ultra - -import android.os.Parcelable -import androidx.annotation.Keep -import androidx.compose.runtime.Immutable -import kotlinx.parcelize.Parcelize - -/** - * @author weishu - * @date 2022/12/8. - */ -object Natives { - // minimal supported kernel version - // 10915: allowlist breaking change, add app profile - // 10931: app profile struct add 'version' field - // 10946: add capabilities - // 10977: change groups_count and groups to avoid overflow write - // 11071: Fix the issue of failing to set a custom SELinux type. - const val MINIMAL_SUPPORTED_KERNEL = 12800 - - // 11640: Support query working mode, LKM or GKI - // when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant. - const val MINIMAL_SUPPORTED_KERNEL_LKM = 12800 - - // 12040: Support disable sucompat mode - const val MINIMAL_SUPPORTED_SU_COMPAT = 12800 - const val KERNEL_SU_DOMAIN = "u:r:su:s0" - - const val ROOT_UID = 0 - const val ROOT_GID = 0 - - init { - System.loadLibrary("zako") - } - - // become root manager, return true if success. - external fun becomeManager(pkg: String?): Boolean - val version: Int - external get - - // get the uid list of allowed su processes. - val allowList: IntArray - external get - - val isSafeMode: Boolean - external get - - val isLkmMode: Boolean - external get - - external fun uidShouldUmount(uid: Int): Boolean - - /** - * Get the profile of the given package. - * @param key usually the package name - * @return return null if failed. - */ - external fun getAppProfile(key: String?, uid: Int): Profile - external fun setAppProfile(profile: Profile?): Boolean - - /** - * `su` compat mode can be disabled temporarily. - * 0: disabled - * 1: enabled - * negative : error - */ - external fun isSuEnabled(): Boolean - external fun setSuEnabled(enabled: Boolean): Boolean - - private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$" - private const val NOBODY_UID = 9999 - - fun setDefaultUmountModules(umountModules: Boolean): Boolean { - Profile( - NON_ROOT_DEFAULT_PROFILE_KEY, - NOBODY_UID, - false, - umountModules = umountModules - ).let { - return setAppProfile(it) - } - } - - fun isDefaultUmountModules(): Boolean { - getAppProfile(NON_ROOT_DEFAULT_PROFILE_KEY, NOBODY_UID).let { - return it.umountModules - } - } - - fun requireNewKernel(): Boolean { - return version < MINIMAL_SUPPORTED_KERNEL - } - - fun isKsuValid(pkgName: String?): Boolean { - if (becomeManager(pkgName)) { - return true - } else { - return false - } - } - - @Immutable - @Parcelize - @Keep - data class Profile( - // and there is a default profile for root and non-root - val name: String, - // current uid for the package, this is convivent for kernel to check - // if the package name doesn't match uid, then it should be invalidated. - val currentUid: Int = 0, - - // if this is true, kernel will grant root permission to this package - val allowSu: Boolean = false, - - // these are used for root profile - val rootUseDefault: Boolean = true, - val rootTemplate: String? = null, - val uid: Int = ROOT_UID, - val gid: Int = ROOT_GID, - val groups: List = mutableListOf(), - val capabilities: List = mutableListOf(), - val context: String = KERNEL_SU_DOMAIN, - val namespace: Int = Namespace.INHERITED.ordinal, - - val nonRootUseDefault: Boolean = true, - val umountModules: Boolean = true, - var rules: String = "", // this field is save in ksud!! - ) : Parcelable { - enum class Namespace { - INHERITED, - GLOBAL, - INDIVIDUAL, - } - - constructor() : this("") - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt b/manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt deleted file mode 100644 index 48be9668..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/flash/KernelFlash.kt +++ /dev/null @@ -1,446 +0,0 @@ -package com.sukisu.ultra.flash - -import android.app.Activity -import android.content.Context -import android.net.Uri -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.CheckCircle -import androidx.compose.material.icons.filled.Error -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.documentfile.provider.DocumentFile -import com.sukisu.ultra.R -import com.sukisu.ultra.utils.AssetsUtil -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import java.io.File -import java.io.FileOutputStream -import java.io.IOException - -data class FlashState( - val isFlashing: Boolean = false, - val isCompleted: Boolean = false, - val progress: Float = 0f, - val currentStep: String = "", - val logs: List = emptyList(), - val error: String = "" -) - -class HorizonKernelState { - private val _state = MutableStateFlow(FlashState()) - val state: StateFlow = _state.asStateFlow() - - fun updateProgress(progress: Float) { - _state.update { it.copy(progress = progress) } - } - - fun updateStep(step: String) { - _state.update { it.copy(currentStep = step) } - } - - fun addLog(log: String) { - _state.update { - it.copy(logs = it.logs + log) - } - } - - fun setError(error: String) { - _state.update { it.copy(error = error) } - } - - fun startFlashing() { - _state.update { - it.copy( - isFlashing = true, - isCompleted = false, - progress = 0f, - currentStep = "under preparation...", - logs = emptyList(), - error = "" - ) - } - } - - fun completeFlashing() { - _state.update { it.copy(isCompleted = true, progress = 1f) } - } - - fun reset() { - _state.value = FlashState() - } -} - -class HorizonKernelWorker( - private val context: Context, - private val state: HorizonKernelState, - private val slot: String? = null -) : Thread() { - var uri: Uri? = null - private lateinit var filePath: String - private lateinit var binaryPath: String - - private var onFlashComplete: (() -> Unit)? = null - private var originalSlot: String? = null - - fun setOnFlashCompleteListener(listener: () -> Unit) { - onFlashComplete = listener - } - - override fun run() { - state.startFlashing() - state.updateStep(context.getString(R.string.horizon_preparing)) - - filePath = "${context.filesDir.absolutePath}/${DocumentFile.fromSingleUri(context, uri!!)?.name}" - binaryPath = "${context.filesDir.absolutePath}/META-INF/com/google/android/update-binary" - - try { - state.updateStep(context.getString(R.string.horizon_cleaning_files)) - state.updateProgress(0.1f) - cleanup() - - if (!rootAvailable()) { - state.setError(context.getString(R.string.root_required)) - return - } - - state.updateStep(context.getString(R.string.horizon_copying_files)) - state.updateProgress(0.2f) - copy() - - if (!File(filePath).exists()) { - state.setError(context.getString(R.string.horizon_copy_failed)) - return - } - - state.updateStep(context.getString(R.string.horizon_extracting_tool)) - state.updateProgress(0.4f) - getBinary() - - state.updateStep(context.getString(R.string.horizon_patching_script)) - state.updateProgress(0.6f) - patch() - - state.updateStep(context.getString(R.string.horizon_flashing)) - state.updateProgress(0.7f) - - val isAbDevice = isAbDevice() - - if (isAbDevice && slot != null) { - state.updateStep(context.getString(R.string.horizon_getting_original_slot)) - state.updateProgress(0.72f) - originalSlot = runCommandGetOutput(true, "getprop ro.boot.slot_suffix") - - state.updateStep(context.getString(R.string.horizon_setting_target_slot)) - state.updateProgress(0.74f) - runCommand(true, "resetprop -n ro.boot.slot_suffix _$slot") - } - - flash() - - if (isAbDevice && !originalSlot.isNullOrEmpty()) { - state.updateStep(context.getString(R.string.horizon_restoring_original_slot)) - state.updateProgress(0.8f) - runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot") - } - - state.updateStep(context.getString(R.string.horizon_flash_complete_status)) - state.completeFlashing() - - (context as? Activity)?.runOnUiThread { - onFlashComplete?.invoke() - } - } catch (e: Exception) { - state.setError(e.message ?: context.getString(R.string.horizon_unknown_error)) - - if (isAbDevice() && !originalSlot.isNullOrEmpty()) { - state.updateStep(context.getString(R.string.horizon_restoring_original_slot)) - state.updateProgress(0.8f) - runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot") - } - } - } - - // 检查设备是否为AB分区设备 - private fun isAbDevice(): Boolean { - val abUpdate = runCommandGetOutput(true, "getprop ro.build.ab_update")?.trim() ?: "" - if (abUpdate.equals("false", ignoreCase = true) || abUpdate.isEmpty()) { - return false - } - - val slotSuffix = runCommandGetOutput(true, "getprop ro.boot.slot_suffix") - return !slotSuffix.isNullOrEmpty() - } - - private fun cleanup() { - runCommand(false, "find ${context.filesDir.absolutePath} -type f ! -name '*.jpg' ! -name '*.png' -delete") - } - - private fun copy() { - uri?.let { safeUri -> - context.contentResolver.openInputStream(safeUri)?.use { input -> - FileOutputStream(File(filePath)).use { output -> - input.copyTo(output) - } - } - } - } - - private fun getBinary() { - runCommand(false, "unzip \"$filePath\" \"*/update-binary\" -d ${context.filesDir.absolutePath}") - if (!File(binaryPath).exists()) { - throw IOException("Failed to extract update-binary") - } - } - - private fun patch() { - val kernelVersion = runCommandGetOutput(true, "cat /proc/version") - val versionRegex = """\d+\.\d+\.\d+""".toRegex() - val version = kernelVersion?.let { versionRegex.find(it) }?.value ?: "" - val toolName = if (version.isNotEmpty()) { - val parts = version.split('.') - if (parts.size >= 2) { - val major = parts[0].toIntOrNull() ?: 0 - val minor = parts[1].toIntOrNull() ?: 0 - if (major < 5 || (major == 5 && minor <= 10)) "5_10" else "5_15+" - } else { - "5_15+" - } - } else { - "5_15+" - } - val toolPath = "${context.filesDir.absolutePath}/mkbootfs" - AssetsUtil.exportFiles(context, "$toolName-mkbootfs", toolPath) - state.addLog("${context.getString(R.string.kernel_version_log, version)} ${context.getString(R.string.tool_version_log, toolName)}") - runCommand(false, "sed -i '/chmod -R 755 tools bin;/i cp -f $toolPath \$AKHOME/tools;' $binaryPath") - } - - private fun flash() { - val process = ProcessBuilder("su") - .redirectErrorStream(true) - .start() - - try { - process.outputStream.bufferedWriter().use { writer -> - writer.write("export POSTINSTALL=${context.filesDir.absolutePath}\n") - - // 写入槽位信息到临时文件 - slot?.let { selectedSlot -> - writer.write("echo \"$selectedSlot\" > ${context.filesDir.absolutePath}/bootslot\n") - } - - // 构建刷写命令 - val flashCommand = buildString { - append("sh $binaryPath 3 1 \"$filePath\"") - if (slot != null) { - append(" \"$(cat ${context.filesDir.absolutePath}/bootslot)\"") - } - append(" && touch ${context.filesDir.absolutePath}/done\n") - } - - writer.write(flashCommand) - writer.write("exit\n") - writer.flush() - } - - process.inputStream.bufferedReader().use { reader -> - reader.lineSequence().forEach { line -> - if (line.startsWith("ui_print")) { - val logMessage = line.removePrefix("ui_print").trim() - state.addLog(logMessage) - - when { - logMessage.contains("extracting", ignoreCase = true) -> { - state.updateProgress(0.75f) - } - logMessage.contains("installing", ignoreCase = true) -> { - state.updateProgress(0.85f) - } - logMessage.contains("complete", ignoreCase = true) -> { - state.updateProgress(0.95f) - } - } - } - } - } - } finally { - process.destroy() - } - - if (!File("${context.filesDir.absolutePath}/done").exists()) { - throw IOException(context.getString(R.string.flash_failed_message)) - } - } - - private fun runCommand(su: Boolean, cmd: String): Int { - val process = ProcessBuilder(if (su) "su" else "sh") - .redirectErrorStream(true) - .start() - - return try { - process.outputStream.bufferedWriter().use { writer -> - writer.write("$cmd\n") - writer.write("exit\n") - writer.flush() - } - process.waitFor() - } finally { - process.destroy() - } - } - - private fun runCommandGetOutput(su: Boolean, cmd: String): String? { - val process = ProcessBuilder(if (su) "su" else "sh") - .redirectErrorStream(true) - .start() - - return try { - process.outputStream.bufferedWriter().use { writer -> - writer.write("$cmd\n") - writer.write("exit\n") - writer.flush() - } - process.inputStream.bufferedReader().use { reader -> - reader.readText().trim() - } - } catch (_: Exception) { - "" - } finally { - process.destroy() - } - } - - private fun rootAvailable(): Boolean { - return try { - val process = Runtime.getRuntime().exec("su -c id") - val exitValue = process.waitFor() - exitValue == 0 - } catch (_: Exception) { - false - } - } -} - -@Composable -fun HorizonKernelFlashProgress(state: FlashState) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = stringResource(id = R.string.horizon_flash_title), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(bottom = 8.dp) - ) - - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - progress = { state.progress }, - ) - - Text( - text = state.currentStep, - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(vertical = 4.dp) - ) - - if (state.logs.isNotEmpty()) { - Text( - text = stringResource(id = R.string.horizon_logs_label), - style = MaterialTheme.typography.labelMedium, - modifier = Modifier - .align(Alignment.Start) - .padding(top = 8.dp, bottom = 4.dp) - ) - - Surface( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 230.dp) - .padding(vertical = 4.dp), - color = MaterialTheme.colorScheme.surface, - tonalElevation = 1.dp, - shape = MaterialTheme.shapes.small - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(8.dp) - .verticalScroll(rememberScrollState()) - ) { - state.logs.forEach { log -> - Text( - text = log, - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(vertical = 2.dp), - overflow = TextOverflow.Ellipsis, - maxLines = 1 - ) - } - } - } - } - - if (state.error.isNotEmpty()) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - ) { - Icon( - imageVector = Icons.Default.Error, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(end = 8.dp) - ) - Text( - text = state.error, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error - ) - } - } else if (state.isCompleted) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - ) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(end = 8.dp) - ) - Text( - text = stringResource(id = R.string.horizon_flash_complete), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - } - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/profile/Capabilities.kt b/manager/app/src/main/java/com/sukisu/ultra/profile/Capabilities.kt deleted file mode 100644 index d44913b1..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/profile/Capabilities.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.sukisu.ultra.profile - -/** - * @author weishu - * @date 2023/6/3. - */ -enum class Capabilities(val cap: Int, val display: String, val desc: String) { - CAP_CHOWN(0, "CHOWN", "Make arbitrary changes to file UIDs and GIDs (see chown(2))"), - CAP_DAC_OVERRIDE(1, "DAC_OVERRIDE", "Bypass file read, write, and execute permission checks"), - CAP_DAC_READ_SEARCH(2, "DAC_READ_SEARCH", "Bypass file read permission checks and directory read and execute permission checks"), - CAP_FOWNER(3, "FOWNER", "Bypass permission checks on operations that normally require the filesystem UID of the process to match the UID of the file (e.g., chmod(2), utime(2)), excluding those operations covered by CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH"), - CAP_FSETID(4, "FSETID", "Don’t clear set-user-ID and set-group-ID permission bits when a file is modified; set the set-group-ID bit for a file whose GID does not match the filesystem or any of the supplementary GIDs of the calling process"), - CAP_KILL(5, "KILL", "Bypass permission checks for sending signals (see kill(2))."), - CAP_SETGID(6, "SETGID", "Make arbitrary manipulations of process GIDs and supplementary GID list; allow setgid(2) manipulation of the caller’s effective and real group IDs"), - CAP_SETUID(7, "SETUID", "Make arbitrary manipulations of process UIDs (setuid(2), setreuid(2), setresuid(2), setfsuid(2)); allow changing the current process user IDs; allow changing of the current process group ID to any value in the system’s range of legal group IDs"), - CAP_SETPCAP(8, "SETPCAP", "If file capabilities are supported: grant or remove any capability in the caller’s permitted capability set to or from any other process. (This property supersedes the obsolete notion of giving a process all capabilities by granting all capabilities in its permitted set, and of removing all capabilities from a process by granting no capabilities in its permitted set. It does not permit any actions that were not permitted before.)"), - CAP_LINUX_IMMUTABLE(9, "LINUX_IMMUTABLE", "Set the FS_APPEND_FL and FS_IMMUTABLE_FL inode flags (see chattr(1))."), - CAP_NET_BIND_SERVICE(10, "NET_BIND_SERVICE", "Bind a socket to Internet domain"), - CAP_NET_BROADCAST(11, "NET_BROADCAST", "Make socket broadcasts, and listen to multicasts"), - CAP_NET_ADMIN(12, "NET_ADMIN", "Perform various network-related operations: interface configuration, administration of IP firewall, masquerading, and accounting, modify routing tables, bind to any address for transparent proxying, set type-of-service (TOS), clear driver statistics, set promiscuous mode, enabling multicasting, use setsockopt(2) to set the following socket options: SO_DEBUG, SO_MARK, SO_PRIORITY (for a priority outside the range 0 to 6), SO_RCVBUFFORCE, and SO_SNDBUFFORCE"), - CAP_NET_RAW(13, "NET_RAW", "Use RAW and PACKET sockets"), - CAP_IPC_LOCK(14, "IPC_LOCK", "Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2))"), - CAP_IPC_OWNER(15, "IPC_OWNER", "Bypass permission checks for operations on System V IPC objects"), - CAP_SYS_MODULE(16, "SYS_MODULE", "Load and unload kernel modules (see init_module(2) and delete_module(2)); in kernels before 2.6.25, this also granted rights for various other operations related to kernel modules"), - CAP_SYS_RAWIO(17, "SYS_RAWIO", "Perform I/O port operations (iopl(2) and ioperm(2)); access /proc/kcore"), - CAP_SYS_CHROOT(18, "SYS_CHROOT", "Use chroot(2)"), - CAP_SYS_PTRACE(19, "SYS_PTRACE", "Trace arbitrary processes using ptrace(2)"), - CAP_SYS_PACCT(20, "SYS_PACCT", "Use acct(2)"), - CAP_SYS_ADMIN(21, "SYS_ADMIN", "Perform a range of system administration operations including: quotactl(2), mount(2), umount(2), swapon(2), swapoff(2), sethostname(2), and setdomainname(2); set and modify process resource limits (setrlimit(2)); perform various network-related operations (e.g., setting privileged socket options, enabling multicasting, interface configuration); perform various IPC operations (e.g., SysV semaphores, POSIX message queues, System V shared memory); allow reboot and kexec_load(2); override /proc/sys kernel tunables; perform ptrace(2) PTRACE_SECCOMP_GET_FILTER operation; perform some tracing and debugging operations (see ptrace(2)); administer the lifetime of kernel tracepoints (tracefs(5)); perform the KEYCTL_CHOWN and KEYCTL_SETPERM keyctl(2) operations; perform the following keyctl(2) operations: KEYCTL_CAPABILITIES, KEYCTL_CAPSQUASH, and KEYCTL_PKEY_ OPERATIONS; set state for the Extensible Authentication Protocol (EAP) kernel module; and override the RLIMIT_NPROC resource limit; allow ioperm/iopl access to I/O ports"), - CAP_SYS_BOOT(22, "SYS_BOOT", "Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution"), - CAP_SYS_NICE(23, "SYS_NICE", "Raise process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes; set real-time scheduling policies for calling process, and set scheduling policies and priorities for arbitrary processes (sched_setscheduler(2), sched_setparam(2)"), - CAP_SYS_RESOURCE(24, "SYS_RESOURCE", "Override resource Limits. Set resource limits (setrlimit(2), prlimit(2)), override quota limits (quota(2), quotactl(2)), override reserved space on ext2 filesystem (ext2_ioctl(2)), override size restrictions on IPC message queues (msg(2)) and system V shared memory segments (shmget(2)), and override the /proc/sys/fs/pipe-size-max limit"), - CAP_SYS_TIME(25, "SYS_TIME", "Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock"), - CAP_SYS_TTY_CONFIG(26, "SYS_TTY_CONFIG", "Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals"), - CAP_MKNOD(27, "MKNOD", "Create special files using mknod(2)"), - CAP_LEASE(28, "LEASE", "Establish leases on arbitrary files (see fcntl(2))"), - CAP_AUDIT_WRITE(29, "AUDIT_WRITE", "Write records to kernel auditing log"), - CAP_AUDIT_CONTROL(30, "AUDIT_CONTROL", "Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules"), - CAP_SETFCAP(31, "SETFCAP", "If file capabilities are supported: grant or remove any capability in any capability set to any file"), - CAP_MAC_OVERRIDE(32, "MAC_OVERRIDE", "Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM)"), - CAP_MAC_ADMIN(33, "MAC_ADMIN", "Allow MAC configuration or state changes. Implemented for the Smack LSM"), - CAP_SYSLOG(34, "SYSLOG", "Perform privileged syslog(2) operations. See syslog(2) for information on which operations require privilege"), - CAP_WAKE_ALARM(35, "WAKE_ALARM", "Trigger something that will wake up the system"), - CAP_BLOCK_SUSPEND(36, "BLOCK_SUSPEND", "Employ features that can block system suspend"), - CAP_AUDIT_READ(37, "AUDIT_READ", "Allow reading the audit log via a multicast netlink socket"), - CAP_PERFMON(38, "PERFMON", "Allow performance monitoring via perf_event_open(2)"), - CAP_BPF(39, "BPF", "Allow BPF operations via bpf(2)"), - CAP_CHECKPOINT_RESTORE(40, "CHECKPOINT_RESTORE", "Allow processes to be checkpointed via checkpoint/restore in user namespace(2)"), -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/profile/Groups.kt b/manager/app/src/main/java/com/sukisu/ultra/profile/Groups.kt deleted file mode 100644 index 2ba73ba5..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/profile/Groups.kt +++ /dev/null @@ -1,130 +0,0 @@ -package com.sukisu.ultra.profile - -/** - * https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h - * @author weishu - * @date 2023/6/3. - */ -enum class Groups(val gid: Int, val display: String, val desc: String) { - ROOT(0, "root", "traditional unix root user"), - DAEMON(1, "daemon", "Traditional unix daemon owner."), - BIN(2, "bin", "Traditional unix binaries owner."), - SYS(3, "sys", "A group with the same gid on Linux/macOS/Android."), - SYSTEM(1000, "system", "system server"), - RADIO(1001, "radio", "telephony subsystem, RIL"), - BLUETOOTH(1002, "bluetooth", "bluetooth subsystem"), - GRAPHICS(1003, "graphics", "graphics devices"), - INPUT(1004, "input", "input devices"), - AUDIO(1005, "audio", "audio devices"), - CAMERA(1006, "camera", "camera devices"), - LOG(1007, "log", "log devices"), - COMPASS(1008, "compass", "compass device"), - MOUNT(1009, "mount", "mountd socket"), - WIFI(1010, "wifi", "wifi subsystem"), - ADB(1011, "adb", "android debug bridge (adbd)"), - INSTALL(1012, "install", "group for installing packages"), - MEDIA(1013, "media", "mediaserver process"), - DHCP(1014, "dhcp", "dhcp client"), - SDCARD_RW(1015, "sdcard_rw", "external storage write access"), - VPN(1016, "vpn", "vpn system"), - KEYSTORE(1017, "keystore", "keystore subsystem"), - USB(1018, "usb", "USB devices"), - DRM(1019, "drm", "DRM server"), - MDNSR(1020, "mdnsr", "MulticastDNSResponder (service discovery)"), - GPS(1021, "gps", "GPS daemon"), - UNUSED1(1022, "unused1", "deprecated, DO NOT USE"), - MEDIA_RW(1023, "media_rw", "internal media storage write access"), - MTP(1024, "mtp", "MTP USB driver access"), - UNUSED2(1025, "unused2", "deprecated, DO NOT USE"), - DRMRPC(1026, "drmrpc", "group for drm rpc"), - NFC(1027, "nfc", "nfc subsystem"), - SDCARD_R(1028, "sdcard_r", "external storage read access"), - CLAT(1029, "clat", "clat part of nat464"), - LOOP_RADIO(1030, "loop_radio", "loop radio devices"), - MEDIA_DRM(1031, "media_drm", "MediaDrm plugins"), - PACKAGE_INFO(1032, "package_info", "access to installed package details"), - SDCARD_PICS(1033, "sdcard_pics", "external storage photos access"), - SDCARD_AV(1034, "sdcard_av", "external storage audio/video access"), - SDCARD_ALL(1035, "sdcard_all", "access all users external storage"), - LOGD(1036, "logd", "log daemon"), - SHARED_RELRO(1037, "shared_relro", "creator of shared GNU RELRO files"), - DBUS(1038, "dbus", "dbus-daemon IPC broker process"), - TLSDATE(1039, "tlsdate", "tlsdate unprivileged user"), - MEDIA_EX(1040, "media_ex", "mediaextractor process"), - AUDIOSERVER(1041, "audioserver", "audioserver process"), - METRICS_COLL(1042, "metrics_coll", "metrics_collector process"), - METRICSD(1043, "metricsd", "metricsd process"), - WEBSERV(1044, "webserv", "webservd process"), - DEBUGGERD(1045, "debuggerd", "debuggerd unprivileged user"), - MEDIA_CODEC(1046, "media_codec", "media_codec process"), - CAMERASERVER(1047, "cameraserver", "cameraserver process"), - FIREWALL(1048, "firewall", "firewall process"), - TRUNKS(1049, "trunks", "trunksd process"), - NVRAM(1050, "nvram", "nvram daemon"), - DNS(1051, "dns", "DNS resolution daemon (system: netd)"), - DNS_TETHER(1052, "dns_tether", "DNS resolution daemon (tether: dnsmasq)"), - WEBVIEW_ZYGOTE(1053, "webview_zygote", "WebView zygote process"), - VEHICLE_NETWORK(1054, "vehicle_network", "Vehicle network service"), - MEDIA_AUDIO(1055, "media_audio", "GID for audio files on internal media storage"), - MEDIA_VIDEO(1056, "media_video", "GID for video files on internal media storage"), - MEDIA_IMAGE(1057, "media_image", "GID for image files on internal media storage"), - TOMBSTONED(1058, "tombstoned", "tombstoned user"), - MEDIA_OBB(1059, "media_obb", "GID for OBB files on internal media storage"), - ESE(1060, "ese", "embedded secure element (eSE) subsystem"), - OTA_UPDATE(1061, "ota_update", "resource tracking UID for OTA updates"), - AUTOMOTIVE_EVS(1062, "automotive_evs", "Automotive rear and surround view system"), - LOWPAN(1063, "lowpan", "LoWPAN subsystem"), - HSM(1064, "lowpan", "hardware security module subsystem"), - RESERVED_DISK(1065, "reserved_disk", "GID that has access to reserved disk space"), - STATSD(1066, "statsd", "statsd daemon"), - INCIDENTD(1067, "incidentd", "incidentd daemon"), - SECURE_ELEMENT(1068, "secure_element", "secure element subsystem"), - LMKD(1069, "lmkd", "low memory killer daemon"), - LLKD(1070, "llkd", "live lock daemon"), - IORAPD(1071, "iorapd", "input/output readahead and pin daemon"), - GPU_SERVICE(1072, "gpu_service", "GPU service daemon"), - NETWORK_STACK(1073, "network_stack", "network stack service"), - GSID(1074, "GSID", "GSI service daemon"), - FSVERITY_CERT(1075, "fsverity_cert", "fs-verity key ownership in keystore"), - CREDSTORE(1076, "credstore", "identity credential manager service"), - EXTERNAL_STORAGE(1077, "external_storage", "Full external storage access including USB OTG volumes"), - EXT_DATA_RW(1078, "ext_data_rw", "GID for app-private data directories on external storage"), - EXT_OBB_RW(1079, "ext_obb_rw", "GID for OBB directories on external storage"), - CONTEXT_HUB(1080, "context_hub", "GID for access to the Context Hub"), - VIRTUALIZATIONSERVICE(1081, "virtualizationservice", "VirtualizationService daemon"), - ARTD(1082, "artd", "ART Service daemon"), - UWB(1083, "uwb", "UWB subsystem"), - THREAD_NETWORK(1084, "thread_network", "Thread Network subsystem"), - DICED(1085, "diced", "Android's DICE daemon"), - DMESGD(1086, "dmesgd", "dmesg parsing daemon for kernel report collection"), - JC_WEAVER(1087, "jc_weaver", "Javacard Weaver HAL - to manage omapi ARA rules"), - JC_STRONGBOX(1088, "jc_strongbox", "Javacard Strongbox HAL - to manage omapi ARA rules"), - JC_IDENTITYCRED(1089, "jc_identitycred", "Javacard Identity Cred HAL - to manage omapi ARA rules"), - SDK_SANDBOX(1090, "sdk_sandbox", "SDK sandbox virtual UID"), - SECURITY_LOG_WRITER(1091, "security_log_writer", "write to security log"), - PRNG_SEEDER(1092, "prng_seeder", "PRNG seeder daemon"), - - SHELL(2000, "shell", "adb and debug shell user"), - CACHE(2001, "cache", "cache access"), - DIAG(2002, "diag", "access to diagnostic resources"), - - /* The 3000 series are intended for use as supplemental group id's only. - * They indicate special Android capabilities that the kernel is aware of. */ - NET_BT_ADMIN(3001, "net_bt_admin", "bluetooth: create any socket"), - NET_BT(3002, "net_bt", "bluetooth: create sco, rfcomm or l2cap sockets"), - INET(3003, "inet", "can create AF_INET and AF_INET6 sockets"), - NET_RAW(3004, "net_raw", "can create raw INET sockets"), - NET_ADMIN(3005, "net_admin", "can configure interfaces and routing tables."), - NET_BW_STATS(3006, "net_bw_stats", "read bandwidth statistics"), - NET_BW_ACCT(3007, "net_bw_acct", "change bandwidth statistics accounting"), - NET_BT_STACK(3008, "net_bt_stack", "access to various bluetooth management functions"), - READPROC(3009, "readproc", "Allow /proc read access"), - WAKELOCK(3010, "wakelock", "Allow system wakelock read/write access"), - UHID(3011, "uhid", "Allow read/write to /dev/uhid node"), - READTRACEFS(3012, "readtracefs", "Allow tracefs read"), - - EVERYBODY(9997, "everybody", "Shared external storage read/write"), - MISC(9998, "misc", "Access to misc storage"), - NOBODY(9999, "nobody", "Reserved"), - APP(10000, "app", "Access to app data"), -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.java b/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.java deleted file mode 100644 index 8b4fcd71..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.java +++ /dev/null @@ -1,77 +0,0 @@ -package com.sukisu.ultra.ui; - -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.os.IBinder; -import android.os.UserHandle; -import android.os.UserManager; -import android.util.Log; - -import androidx.annotation.NonNull; - -import com.topjohnwu.superuser.ipc.RootService; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; - -import com.sukisu.zako.IKsuInterface; -import rikka.parcelablelist.ParcelableListSlice; - -/** - * @author weishu - * @date 2023/4/18. - */ - -public class KsuService extends RootService { - - private static final String TAG = "KsuService"; - - class Stub extends IKsuInterface.Stub { - @Override - public ParcelableListSlice getPackages(int flags) { - List list = getInstalledPackagesAll(flags); - Log.i(TAG, "getPackages: " + list.size()); - return new ParcelableListSlice<>(list); - } - } - - @Override - public IBinder onBind(@NonNull Intent intent) { - return new Stub(); - } - - List getUserIds() { - List result = new ArrayList<>(); - UserManager um = (UserManager) getSystemService(Context.USER_SERVICE); - List userProfiles = um.getUserProfiles(); - for (UserHandle userProfile : userProfiles) { - int userId = userProfile.hashCode(); - result.add(userProfile.hashCode()); - } - return result; - } - - ArrayList getInstalledPackagesAll(int flags) { - ArrayList packages = new ArrayList<>(); - for (Integer userId : getUserIds()) { - Log.i(TAG, "getInstalledPackagesAll: " + userId); - packages.addAll(getInstalledPackagesAsUser(flags, userId)); - } - return packages; - } - - List getInstalledPackagesAsUser(int flags, int userId) { - try { - PackageManager pm = getPackageManager(); - Method getInstalledPackagesAsUser = pm.getClass().getDeclaredMethod("getInstalledPackagesAsUser", int.class, int.class); - return (List) getInstalledPackagesAsUser.invoke(pm, flags, userId); - } catch (Throwable e) { - Log.e(TAG, "err", e); - } - - return new ArrayList<>(); - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt deleted file mode 100644 index a06e8fa6..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt +++ /dev/null @@ -1,249 +0,0 @@ -package com.sukisu.ultra.ui - -import android.database.ContentObserver -import android.os.Build -import android.os.Bundle -import android.os.Handler -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.compose.animation.* -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -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 androidx.navigation.compose.rememberNavController -import com.ramcosta.composedestinations.DestinationsNavHost -import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationStyle -import com.ramcosta.composedestinations.generated.NavGraphs -import com.ramcosta.composedestinations.spec.NavHostGraphSpec -import com.ramcosta.composedestinations.spec.RouteOrDirection -import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState -import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator -import io.sukisu.ultra.UltraToolInstall -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ksuApp -import com.sukisu.ultra.ui.screen.BottomBarDestination -import com.sukisu.ultra.ui.theme.* -import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha -import com.sukisu.ultra.ui.util.* -import androidx.core.content.edit -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation - -class MainActivity : ComponentActivity() { - private inner class ThemeChangeContentObserver( - handler: Handler, - private val onThemeChanged: () -> Unit - ) : ContentObserver(handler) { - override fun onChange(selfChange: Boolean) { - super.onChange(selfChange) - onThemeChanged() - } - } - - 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 prefs = getSharedPreferences("settings", MODE_PRIVATE) - val isFirstRun = prefs.getBoolean("is_first_run", true) - - if (isFirstRun) { - ThemeConfig.preventBackgroundRefresh = false - getSharedPreferences("theme_prefs", MODE_PRIVATE).edit { - putBoolean("prevent_background_refresh", false) - } - prefs.edit { putBoolean("is_first_run", false) } - } - - // 加载保存的背景设置 - loadThemeMode() - loadThemeColors() - loadDynamicColorState() - CardConfig.load(applicationContext) - - val contentObserver = ThemeChangeContentObserver(Handler(mainLooper)) { - runOnUiThread { - if (!ThemeConfig.preventBackgroundRefresh) { - ThemeConfig.backgroundImageLoaded = false - loadCustomBackground() - } - } - } - - contentResolver.registerContentObserver( - android.provider.Settings.System.getUriFor("ui_night_mode"), - false, - contentObserver - ) - - val destroyListeners = mutableListOf<() -> Unit>() - destroyListeners.add { - contentResolver.unregisterContentObserver(contentObserver) - } - - val isManager = Natives.becomeManager(ksuApp.packageName) - if (isManager) { - install() - UltraToolInstall.tryToInstall() - } - - setContent { - KernelSUTheme { - val navController = rememberNavController() - val snackBarHostState = remember { SnackbarHostState() } - - Scaffold( - bottomBar = { BottomBar(navController) }, - contentWindowInsets = WindowInsets(0, 0, 0, 0) - ) { innerPadding -> - CompositionLocalProvider( - LocalSnackbarHost provides snackBarHostState - ) { - DestinationsNavHost( - modifier = Modifier.padding(innerPadding), - navGraph = NavGraphs.root as NavHostGraphSpec, - 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)) } - } - ) - } - } - } - } - } - - override fun onPause() { - super.onPause() - CardConfig.save(applicationContext) - getSharedPreferences("theme_prefs", MODE_PRIVATE).edit() { - putBoolean("prevent_background_refresh", true) - } - ThemeConfig.preventBackgroundRefresh = true - } - - override fun onResume() { - super.onResume() - if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) { - loadCustomBackground() - } - } - - private val destroyListeners = mutableListOf<() -> Unit>() - - override fun onDestroy() { - destroyListeners.forEach { it() } - super.onDestroy() - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun BottomBar(navController: NavHostController) { - val navigator = navController.rememberDestinationsNavigator() - val isManager = Natives.becomeManager(ksuApp.packageName) - val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() - val kpmVersion = getKpmVersion() - val containerColor = MaterialTheme.colorScheme.surfaceVariant - val cardColor = MaterialTheme.colorScheme.surfaceVariant - - NavigationBar( - modifier = Modifier.windowInsetsPadding( - WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal) - ), - containerColor = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = containerColor.copy(alpha = cardAlpha) - ).containerColor, - tonalElevation = cardElevation - ) { - BottomBarDestination.entries.forEach { destination -> - if (destination == BottomBarDestination.Kpm) { - if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) { - if (!fullFeatured && destination.rootRequired) return@forEach - val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction) - NavigationBarItem( - selected = isCurrentDestOnBackStack, - onClick = { - if (!isCurrentDestOnBackStack) { - navigator.navigate(destination.direction) { - popUpTo(NavGraphs.root as RouteOrDirection) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - } - }, - icon = { - Icon( - imageVector = if (isCurrentDestOnBackStack) { - destination.iconSelected - } else { - destination.iconNotSelected - }, - contentDescription = stringResource(destination.label), - tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - label = { - Text( - text = stringResource(destination.label), - style = MaterialTheme.typography.labelMedium - ) - } - ) - } - } else { - if (!fullFeatured && destination.rootRequired) return@forEach - val isCurrentDestOnBackStack by navController.isRouteOnBackStackAsState(destination.direction) - NavigationBarItem( - selected = isCurrentDestOnBackStack, - onClick = { - if (!isCurrentDestOnBackStack) { - navigator.navigate(destination.direction) { - popUpTo(NavGraphs.root as RouteOrDirection) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - } - }, - icon = { - Icon( - imageVector = if (isCurrentDestOnBackStack) { - destination.iconSelected - } else { - destination.iconNotSelected - }, - contentDescription = stringResource(destination.label), - tint = if (isCurrentDestOnBackStack) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - label = { - Text( - text = stringResource(destination.label), - style = MaterialTheme.typography.labelMedium - ) - } - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/AboutCard.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/AboutCard.kt deleted file mode 100644 index b2f5f25a..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/AboutCard.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.sukisu.ultra.ui.component - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.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.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -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.window.Dialog -import com.sukisu.ultra.BuildConfig -import com.sukisu.ultra.R - -@Preview -@Composable -fun AboutCard() { - ElevatedCard( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(24.dp) - ) { - AboutCardContent() - } - } -} - -@Composable -fun AboutDialog(dismiss: () -> Unit) { - Dialog( - onDismissRequest = { dismiss() } - ) { - AboutCard() - } -} - -@Composable -private fun AboutCardContent() { - Column( - modifier = Modifier.fillMaxWidth() - ) { - Row { - Surface( - modifier = Modifier.size(40.dp), - color = colorResource(id = R.color.ic_launcher_background), - shape = CircleShape - ) { - Image( - painter = painterResource(id = R.drawable.ic_launcher_monochrome), - contentDescription = "icon", - modifier = Modifier.scale(1.4f) - ) - } - - Spacer(modifier = Modifier.width(12.dp)) - - Column { - - Text( - stringResource(id = R.string.app_name), - style = MaterialTheme.typography.titleSmall, - fontSize = 18.sp - ) - Text( - BuildConfig.VERSION_NAME, - style = MaterialTheme.typography.bodySmall, - fontSize = 14.sp - ) - - Spacer(modifier = Modifier.height(8.dp)) - - 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 - ) - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt deleted file mode 100644 index f398c1b4..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt +++ /dev/null @@ -1,454 +0,0 @@ -package com.sukisu.ultra.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 -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.* -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 -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -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.* -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 - -private const val TAG = "DialogComponent" - -interface ConfirmDialogVisuals : Parcelable { - val title: String - val content: String - val isMarkdown: Boolean - val confirm: String? - val dismiss: String? -} - -@Parcelize -private data class ConfirmDialogVisualsImpl( - override val title: String, - override val content: String, - override val isMarkdown: Boolean, - override val confirm: String?, - override val dismiss: String?, -) : ConfirmDialogVisuals { - companion object { - val Empty: ConfirmDialogVisuals = ConfirmDialogVisualsImpl("", "", false, null, null) - } -} - -interface DialogHandle { - val isShown: Boolean - val dialogType: String - fun show() - fun hide() -} - -interface LoadingDialogHandle : DialogHandle { - suspend fun withLoading(block: suspend () -> R): R - fun showLoading() -} - -sealed interface ConfirmResult { - object Confirmed : ConfirmResult - object Canceled : ConfirmResult -} - -interface ConfirmDialogHandle : DialogHandle { - val visuals: ConfirmDialogVisuals - - fun showConfirm( - title: String, - content: String, - markdown: Boolean = false, - confirm: String? = null, - dismiss: String? = null - ) - - suspend fun awaitConfirm( - - title: String, - content: String, - markdown: Boolean = false, - confirm: String? = null, - dismiss: String? = null - ): ConfirmResult -} - -private abstract class DialogHandleBase( - val visible: MutableState, - val coroutineScope: CoroutineScope -) : DialogHandle { - override val isShown: Boolean - get() = visible.value - - override fun show() { - coroutineScope.launch { - visible.value = true - } - } - - final override fun hide() { - coroutineScope.launch { - visible.value = false - } - } - - override fun toString(): String { - return dialogType - } -} - -private class LoadingDialogHandleImpl( - visible: MutableState, - coroutineScope: CoroutineScope -) : LoadingDialogHandle, DialogHandleBase(visible, coroutineScope) { - override suspend fun withLoading(block: suspend () -> R): R { - return coroutineScope.async { - try { - visible.value = true - block() - } finally { - visible.value = false - } - }.await() - } - - override fun showLoading() { - show() - } - - override val dialogType: String get() = "LoadingDialog" -} - -typealias NullableCallback = (() -> Unit)? - -interface ConfirmCallback { - - val onConfirm: NullableCallback - - val onDismiss: NullableCallback - - val isEmpty: Boolean get() = onConfirm == null && onDismiss == null - - companion object { - operator fun invoke(onConfirmProvider: () -> NullableCallback, onDismissProvider: () -> NullableCallback): ConfirmCallback { - return object : ConfirmCallback { - override val onConfirm: NullableCallback - get() = onConfirmProvider() - override val onDismiss: NullableCallback - get() = onDismissProvider() - } - } - } -} - -private class ConfirmDialogHandleImpl( - visible: MutableState, - coroutineScope: CoroutineScope, - callback: ConfirmCallback, - override var visuals: ConfirmDialogVisuals = ConfirmDialogVisualsImpl.Empty, - private val resultFlow: ReceiveChannel -) : ConfirmDialogHandle, DialogHandleBase(visible, coroutineScope) { - private class ResultCollector( - private val callback: ConfirmCallback - ) : FlowCollector { - fun handleResult(result: ConfirmResult) { - Log.d(TAG, "handleResult: ${result.javaClass.simpleName}") - when (result) { - ConfirmResult.Confirmed -> onConfirm() - ConfirmResult.Canceled -> onDismiss() - } - } - - fun onConfirm() { - callback.onConfirm?.invoke() - } - - fun onDismiss() { - callback.onDismiss?.invoke() - } - - override suspend fun emit(value: ConfirmResult) { - handleResult(value) - } - } - - private val resultCollector = ResultCollector(callback) - - private var awaitContinuation: CancellableContinuation? = null - - private val isCallbackEmpty = callback.isEmpty - - init { - coroutineScope.launch { - resultFlow - .consumeAsFlow() - .onEach { result -> - awaitContinuation?.let { - awaitContinuation = null - if (it.isActive) { - it.resume(result) - } - } - } - .onEach { hide() } - .collect(resultCollector) - } - } - - private suspend fun awaitResult(): ConfirmResult { - return suspendCancellableCoroutine { - awaitContinuation = it.apply { - if (isCallbackEmpty) { - invokeOnCancellation { - visible.value = false - } - } - } - } - } - - fun updateVisuals(visuals: ConfirmDialogVisuals) { - this.visuals = visuals - } - - override fun show() { - if (visuals !== ConfirmDialogVisualsImpl.Empty) { - super.show() - } else { - throw UnsupportedOperationException("can't show confirm dialog with the Empty visuals") - } - } - - override fun showConfirm( - title: String, - content: String, - markdown: Boolean, - 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 rememberLoadingDialog(): LoadingDialogHandle { - val visible = remember { - mutableStateOf(false) - } - val coroutineScope = rememberCoroutineScope() - - if (visible.value) { - LoadingDialog() - } - - return remember { - LoadingDialogHandleImpl(visible, coroutineScope) - } -} - -@Composable -private fun rememberConfirmDialog(visuals: ConfirmDialogVisuals, callback: ConfirmCallback): ConfirmDialogHandle { - val visible = rememberSaveable { - mutableStateOf(false) - } - val coroutineScope = rememberCoroutineScope() - val resultChannel = remember { - Channel() - } - - val handle = rememberSaveable( - saver = ConfirmDialogHandleImpl.Saver(visible, coroutineScope, callback, resultChannel), - init = { - ConfirmDialogHandleImpl(visible, coroutineScope, callback, visuals, resultChannel) - } - ) - - if (visible.value) { - ConfirmDialog( - handle.visuals, - confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } }, - dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } } - ) - } - - return handle -} - -@Composable -fun rememberConfirmCallback(onConfirm: NullableCallback, onDismiss: NullableCallback): ConfirmCallback { - val currentOnConfirm by rememberUpdatedState(newValue = onConfirm) - val currentOnDismiss by rememberUpdatedState(newValue = onDismiss) - return remember { - ConfirmCallback({ currentOnConfirm }, { currentOnDismiss }) - } -} - -@Composable -fun rememberConfirmDialog(onConfirm: NullableCallback = null, onDismiss: NullableCallback = null): ConfirmDialogHandle { - return rememberConfirmDialog(rememberConfirmCallback(onConfirm, onDismiss)) -} - -@Composable -fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle { - return rememberConfirmDialog(ConfirmDialogVisualsImpl.Empty, callback) -} - -@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) - ) { - Box( - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } - } - } -} - -@Composable -private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit) { - AlertDialog( - onDismissRequest = { - dismiss() - }, - title = { - Text(text = visuals.title) - }, - text = { - if (visuals.isMarkdown) { - MarkdownContent(content = visuals.content) - } else { - Text(text = visuals.content) - } - }, - confirmButton = { - TextButton(onClick = confirm) { - Text(text = visuals.confirm ?: stringResource(id = android.R.string.ok)) - } - }, - dismissButton = { - TextButton(onClick = dismiss) { - Text(text = visuals.dismiss ?: stringResource(id = android.R.string.cancel)) - } - }, - ) -} - -@Composable -private fun MarkdownContent(content: String) { - val contentColor = LocalContentColor.current - - AndroidView( - factory = { context -> - TextView(context).apply { - movementMethod = LinkMovementMethod.getInstance() - setSpannableFactory(NoCopySpannableFactory.getInstance()) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE - } - hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - }, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(), - 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/com/sukisu/ultra/ui/component/ImageEditorDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt deleted file mode 100644 index d4e4b074..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt +++ /dev/null @@ -1,224 +0,0 @@ -package com.sukisu.ultra.ui.component - -import android.net.Uri -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTransformGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Fullscreen -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import coil.compose.AsyncImage -import coil.request.ImageRequest -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.util.BackgroundTransformation -import com.sukisu.ultra.ui.util.saveTransformedBackground -import kotlinx.coroutines.launch -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.layout.onSizeChanged -import kotlin.math.max - -@Composable -fun ImageEditorDialog( - imageUri: Uri, - onDismiss: () -> Unit, - onConfirm: (Uri) -> Unit -) { - var scale by remember { mutableFloatStateOf(1f) } - var offsetX by remember { mutableFloatStateOf(0f) } - var offsetY by remember { mutableFloatStateOf(0f) } - val context = LocalContext.current - val scope = rememberCoroutineScope() - val density = LocalDensity.current - var lastScale by remember { mutableFloatStateOf(1f) } - var lastOffsetX by remember { mutableFloatStateOf(0f) } - var lastOffsetY by remember { mutableFloatStateOf(0f) } - var imageSize by remember { mutableStateOf(Size.Zero) } - var screenSize by remember { mutableStateOf(Size.Zero) } - val animatedScale by animateFloatAsState( - targetValue = scale, - label = "ScaleAnimation" - ) - val animatedOffsetX by animateFloatAsState( - targetValue = offsetX, - label = "OffsetXAnimation" - ) - val animatedOffsetY by animateFloatAsState( - targetValue = offsetY, - label = "OffsetYAnimation" - ) - val updateTransformation = remember { - { newScale: Float, newOffsetX: Float, newOffsetY: Float -> - val scaleDiff = kotlin.math.abs(newScale - lastScale) - val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX) - val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY) - if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) { - scale = newScale - offsetX = newOffsetX - offsetY = newOffsetY - lastScale = newScale - lastOffsetX = newOffsetX - lastOffsetY = newOffsetY - } - } - } - val scaleToFullScreen = remember { - { - if (imageSize.height > 0 && screenSize.height > 0) { - val newScale = screenSize.height / imageSize.height - updateTransformation(newScale, 0f, 0f) - } - } - } - - Dialog( - onDismissRequest = onDismiss, - properties = DialogProperties( - dismissOnBackPress = true, - dismissOnClickOutside = false, - usePlatformDefaultWidth = false - ) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.9f)) - .onSizeChanged { size -> - screenSize = Size(size.width.toFloat(), size.height.toFloat()) - } - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUri) - .crossfade(true) - .build(), - contentDescription = stringResource(R.string.settings_custom_background), - contentScale = ContentScale.Fit, - modifier = Modifier - .fillMaxSize() - .graphicsLayer( - scaleX = animatedScale, - scaleY = animatedScale, - translationX = animatedOffsetX, - translationY = animatedOffsetY - ) - .pointerInput(Unit) { - detectTransformGestures { _, pan, zoom, _ -> - scope.launch { - try { - val newScale = (scale * zoom).coerceIn(0.5f, 3f) - val maxOffsetX = max(0f, size.width * (newScale - 1) / 2) - val maxOffsetY = max(0f, size.height * (newScale - 1) / 2) - val newOffsetX = if (maxOffsetX > 0) { - (offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX) - } else { - 0f - } - val newOffsetY = if (maxOffsetY > 0) { - (offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY) - } else { - 0f - } - updateTransformation(newScale, newOffsetX, newOffsetY) - } catch (e: Exception) { - updateTransformation(lastScale, lastOffsetX, lastOffsetY) - } - } - } - } - .onSizeChanged { size -> - imageSize = Size(size.width.toFloat(), size.height.toFloat()) - } - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .align(Alignment.TopCenter), - horizontalArrangement = Arrangement.SpaceBetween - ) { - IconButton( - onClick = onDismiss, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.cancel), - tint = Color.White - ) - } - IconButton( - onClick = { scaleToFullScreen() }, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - ) { - Icon( - imageVector = Icons.Default.Fullscreen, - contentDescription = stringResource(R.string.reprovision), - tint = Color.White - ) - } - IconButton( - onClick = { - scope.launch { - try { - val transformation = BackgroundTransformation(scale, offsetX, offsetY) - val savedUri = context.saveTransformedBackground(imageUri, transformation) - savedUri?.let { onConfirm(it) } - } catch (e: Exception) { - "" - } - } - }, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - ) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = stringResource(R.string.confirm), - tint = Color.White - ) - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - .padding(16.dp) - .align(Alignment.BottomCenter) - ) { - Text( - text = stringResource(id = R.string.image_editor_hint), - color = Color.White, - style = MaterialTheme.typography.bodyMedium - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/KeyEventBlocker.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/KeyEventBlocker.kt deleted file mode 100644 index 3c1b3580..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/KeyEventBlocker.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.sukisu.ultra.ui.component - -import androidx.compose.foundation.focusable -import androidx.compose.foundation.layout.Box -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.input.key.KeyEvent -import androidx.compose.ui.input.key.onKeyEvent - -@Composable -fun KeyEventBlocker(predicate: (KeyEvent) -> Boolean) { - val requester = remember { FocusRequester() } - Box( - Modifier - .onKeyEvent { - predicate(it) - } - .focusRequester(requester) - .focusable() - ) - LaunchedEffect(Unit) { - requester.requestFocus() - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SearchBar.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SearchBar.kt deleted file mode 100644 index 83ab9644..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SearchBar.kt +++ /dev/null @@ -1,169 +0,0 @@ -package com.sukisu.ultra.ui.component - -import android.util.Log -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.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -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.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.sukisu.ultra.ui.theme.CardConfig - -private const val TAG = "SearchBar" - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SearchAppBar( - title: @Composable () -> Unit, - searchText: String, - onSearchTextChange: (String) -> Unit, - onClearClick: () -> Unit, - onBackClick: (() -> Unit)? = null, - onConfirm: (() -> Unit)? = null, - dropdownContent: @Composable (() -> Unit)? = null, - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val keyboardController = LocalSoftwareKeyboardController.current - val focusRequester = remember { FocusRequester() } - var onSearch by remember { mutableStateOf(false) } - - // 获取卡片颜色和透明度 - val cardColor = MaterialTheme.colorScheme.surfaceVariant - val cardAlpha = CardConfig.cardAlpha - - if (onSearch) { - LaunchedEffect(Unit) { focusRequester.requestFocus() } - } - DisposableEffect(Unit) { - onDispose { - keyboardController?.hide() - } - } - - TopAppBar( - title = { - Box { - AnimatedVisibility( - modifier = Modifier.align(Alignment.CenterStart), - visible = !onSearch, - enter = fadeIn(), - exit = fadeOut(), - content = { title() } - ) - - AnimatedVisibility( - visible = onSearch, - enter = fadeIn(), - exit = fadeOut() - ) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(top = 2.dp, bottom = 2.dp, end = if (onBackClick != null) 0.dp else 14.dp) - .focusRequester(focusRequester) - .onFocusChanged { focusState -> - if (focusState.isFocused) onSearch = true - Log.d(TAG, "onFocusChanged: $focusState") - }, - value = searchText, - onValueChange = onSearchTextChange, - trailingIcon = { - IconButton( - onClick = { - onSearch = false - keyboardController?.hide() - onClearClick() - }, - content = { Icon(Icons.Filled.Close, null) } - ) - }, - maxLines = 1, - singleLine = true, - keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - onConfirm?.invoke() - }) - ) - } - } - }, - navigationIcon = { - if (onBackClick != null) { - IconButton( - onClick = onBackClick, - content = { Icon(Icons.AutoMirrored.Outlined.ArrowBack, null) } - ) - } - }, - actions = { - AnimatedVisibility( - visible = !onSearch - ) { - IconButton( - onClick = { onSearch = true }, - content = { Icon(Icons.Filled.Search, null) } - ) - } - - if (dropdownContent != null) { - dropdownContent() - } - - }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ) - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Preview -@Composable -private fun SearchAppBarPreview() { - var searchText by remember { mutableStateOf("") } - SearchAppBar( - title = { Text("Search text") }, - searchText = searchText, - onSearchTextChange = { searchText = it }, - onClearClick = { searchText = "" } - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt deleted file mode 100644 index 16bb5506..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SettingsItem.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.sukisu.ultra.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( - icon: ImageVector? = null, - title: String, - summary: String? = null, - checked: Boolean, - enabled: Boolean = true, - onCheckedChange: (Boolean) -> Unit -) { - val interactionSource = remember { MutableInteractionSource() } - - ListItem( - modifier = Modifier - .toggleable( - value = checked, - interactionSource = interactionSource, - role = Role.Switch, - enabled = enabled, - indication = LocalIndication.current, - onValueChange = onCheckedChange - ), - headlineContent = { - Text(title) - }, - leadingContent = icon?.let { - { Icon(icon, title) } - }, - trailingContent = { - Switch( - checked = checked, - enabled = enabled, - onCheckedChange = onCheckedChange, - interactionSource = interactionSource - ) - }, - supportingContent = { - if (summary != null) { - Text(summary) - } - } - ) -} - -@Composable -fun RadioItem( - title: String, - selected: Boolean, - onClick: () -> Unit, -) { - ListItem( - headlineContent = { - Text(title) - }, - leadingContent = { - RadioButton(selected = selected, onClick = onClick) - } - ) -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt deleted file mode 100644 index c9f75fde..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt +++ /dev/null @@ -1,248 +0,0 @@ -package com.sukisu.ultra.ui.component - -import android.content.Context -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.theme.ThemeConfig -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.SdStorage -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector - -/** - * 槽位选择对话框组件 - * 用于HorizonKernel刷写时选择目标槽位 - */ -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SlotSelectionDialog( - show: Boolean, - onDismiss: () -> Unit, - onSlotSelected: (String) -> Unit -) { - val context = LocalContext.current - var currentSlot by remember { mutableStateOf(null) } - var errorMessage by remember { mutableStateOf(null) } - - LaunchedEffect(Unit) { - try { - currentSlot = getCurrentSlot(context) - errorMessage = null - } catch (e: Exception) { - errorMessage = e.message - currentSlot = null - } - } - - if (show) { - val cardColor = if (!ThemeConfig.useDynamicColor) { - ThemeConfig.currentTheme.ButtonContrast - } else { - MaterialTheme.colorScheme.surfaceContainerHigh - } - - AlertDialog( - onDismissRequest = onDismiss, - title = { - Text( - text = stringResource(id = R.string.select_slot_title), - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - }, - text = { - Column( - modifier = Modifier.padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - if (errorMessage != null) { - Text( - text = "Error: $errorMessage", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error, - textAlign = TextAlign.Center - ) - } else { - Text( - text = stringResource( - id = R.string.current_slot, - currentSlot ?: "Unknown" - ), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - } - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = stringResource(id = R.string.select_slot_description), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(24.dp)) - - // Horizontal arrangement for slot options with highlighted current slot - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.SpaceBetween - ) { - val slotOptions = listOf( - ListOption( - titleText = stringResource(id = R.string.slot_a), - subtitleText = if (currentSlot == "a" || currentSlot == "_a") stringResource(id = R.string.currently_selected) else null, - icon = Icons.Filled.SdStorage - ), - ListOption( - titleText = stringResource(id = R.string.slot_b), - subtitleText = if (currentSlot == "b" || currentSlot == "_b") stringResource(id = R.string.currently_selected) else null, - icon = Icons.Filled.SdStorage - ) - ) - - slotOptions.forEachIndexed { index, option -> - Column( - modifier = Modifier - .weight(1f) - .padding(horizontal = 8.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .background( - color = if (option.subtitleText != null) { - MaterialTheme.colorScheme.primary.copy(alpha = 0.9f) - } else { - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - } - ) - .clickable { - onSlotSelected( - when (index) { - 0 -> "a" - else -> "b" - } - ) - } - .padding(vertical = 12.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = option.icon, - contentDescription = null, - tint = if (option.subtitleText != null) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.primary - }, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = option.titleText, - style = MaterialTheme.typography.titleMedium, - color = if (option.subtitleText != null) { - MaterialTheme.colorScheme.onPrimary - } else { - MaterialTheme.colorScheme.primary - } - ) - option.subtitleText?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = if (true) { - MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.8f) - } else { - MaterialTheme.colorScheme.onSurfaceVariant - } - ) - } - } - } - } - } - } - } - }, - confirmButton = { - TextButton( - onClick = { - currentSlot?.let { onSlotSelected(it) } - onDismiss() - } - ) { - Text( - text = stringResource(android.R.string.ok), - color = MaterialTheme.colorScheme.primary - ) - } - }, - dismissButton = { - TextButton( - onClick = onDismiss - ) { - Text( - text = stringResource(android.R.string.cancel), - color = MaterialTheme.colorScheme.primary - ) - } - }, - containerColor = cardColor, - shape = MaterialTheme.shapes.extraLarge, - tonalElevation = 4.dp - ) - } -} - -// Data class for list options -data class ListOption( - val titleText: String, - val subtitleText: String?, - val icon: ImageVector -) - -// Utility function to get current slot -private fun getCurrentSlot(context: Context): String? { - return runCommandGetOutput(true, "getprop ro.boot.slot_suffix")?.let { - if (it.startsWith("_")) it.substring(1) else it - } -} - -private fun runCommandGetOutput(su: Boolean, cmd: String): String? { - return try { - val process = ProcessBuilder(if (su) "su" else "sh").start() - process.outputStream.bufferedWriter().use { writer -> - writer.write("$cmd\n") - writer.write("exit\n") - writer.flush() - } - process.inputStream.bufferedReader().use { reader -> - reader.readText().trim() - } - } catch (_: Exception) { - null - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SwitchItem.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SwitchItem.kt deleted file mode 100644 index c95cbfd3..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SwitchItem.kt +++ /dev/null @@ -1,101 +0,0 @@ -package com.sukisu.ultra.ui.component - -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Switch -import androidx.compose.material3.SwitchDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp - -@Composable -fun SwitchItem( - icon: ImageVector, - title: String, - summary: String? = null, - checked: Boolean, - enabled: Boolean = true, - onCheckedChange: (Boolean) -> Unit -) { - // 颜色动画 - val iconTint by animateColorAsState( - targetValue = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f), - animationSpec = tween(300), - label = "iconTint" - ) - - // 开关颜色 - val switchColors = SwitchDefaults.colors( - checkedThumbColor = MaterialTheme.colorScheme.primary, - checkedTrackColor = MaterialTheme.colorScheme.primaryContainer, - checkedBorderColor = MaterialTheme.colorScheme.primary, - checkedIconColor = MaterialTheme.colorScheme.onPrimary, - uncheckedThumbColor = MaterialTheme.colorScheme.outline, - uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant, - uncheckedBorderColor = MaterialTheme.colorScheme.outline, - uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant, - disabledCheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), - disabledCheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), - disabledCheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), - disabledCheckedIconColor = MaterialTheme.colorScheme.surfaceVariant, - disabledUncheckedThumbColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), - disabledUncheckedTrackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), - disabledUncheckedBorderColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), - disabledUncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant - ) - - ListItem( - headlineContent = { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - }, - supportingContent = summary?.let { - { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - } - }, - leadingContent = { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(24.dp), - tint = iconTint - ) - }, - trailingContent = { - Switch( - checked = checked, - onCheckedChange = null, - enabled = enabled, - colors = switchColors - ) - }, - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = enabled) { - onCheckedChange(!checked) - } - .padding(vertical = 4.dp) - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/AppProfileConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/AppProfileConfig.kt deleted file mode 100644 index 563497c7..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/AppProfileConfig.kt +++ /dev/null @@ -1,63 +0,0 @@ -package com.sukisu.ultra.ui.component.profile - -import androidx.compose.foundation.layout.Column -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.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.SwitchItem - -@Composable -fun AppProfileConfig( - modifier: Modifier = Modifier, - fixedName: Boolean, - enabled: Boolean, - profile: Natives.Profile, - onProfileChange: (Natives.Profile) -> Unit, -) { - Column(modifier = modifier) { - if (!fixedName) { - OutlinedTextField( - label = { Text(stringResource(R.string.profile_name)) }, - value = profile.name, - onValueChange = { onProfileChange(profile.copy(name = it)) } - ) - } - - SwitchItem( - title = stringResource(R.string.profile_umount_modules), - summary = stringResource(R.string.profile_umount_modules_summary), - checked = if (enabled) { - profile.umountModules - } else { - Natives.isDefaultUmountModules() - }, - enabled = enabled, - onCheckedChange = { - onProfileChange( - profile.copy( - umountModules = it, - nonRootUseDefault = false - ) - ) - } - ) - } -} - -@Preview -@Composable -private fun AppProfileConfigPreview() { - var profile by remember { mutableStateOf(Natives.Profile("")) } - AppProfileConfig(fixedName = true, enabled = false, profile = profile) { - profile = it - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/RootProfileConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/RootProfileConfig.kt deleted file mode 100644 index 9e0cac19..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/RootProfileConfig.kt +++ /dev/null @@ -1,480 +0,0 @@ -package com.sukisu.ultra.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 -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.AssistChip -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.Text -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.Modifier -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 androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.core.text.isDigitsOnly -import com.maxkeppeker.sheets.core.models.base.Header -import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState -import com.maxkeppeler.sheets.input.InputDialog -import com.maxkeppeler.sheets.input.models.InputHeader -import com.maxkeppeler.sheets.input.models.InputSelection -import com.maxkeppeler.sheets.input.models.InputTextField -import com.maxkeppeler.sheets.input.models.InputTextFieldType -import com.maxkeppeler.sheets.input.models.ValidationResult -import com.maxkeppeler.sheets.list.ListDialog -import com.maxkeppeler.sheets.list.models.ListOption -import com.maxkeppeler.sheets.list.models.ListSelection -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.profile.Capabilities -import com.sukisu.ultra.profile.Groups -import com.sukisu.ultra.ui.component.rememberCustomDialog -import com.sukisu.ultra.ui.util.isSepolicyValid - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun RootProfileConfig( - modifier: Modifier = Modifier, - fixedName: Boolean, - profile: Natives.Profile, - onProfileChange: (Natives.Profile) -> Unit, -) { - Column(modifier = modifier) { - if (!fixedName) { - OutlinedTextField( - label = { Text(stringResource(R.string.profile_name)) }, - value = profile.name, - onValueChange = { onProfileChange(profile.copy(name = it)) } - ) - } - - /* - 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) - else -> stringResource(R.string.profile_namespace_inherited) - } - ListItem(headlineContent = { - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded } - ) { - OutlinedTextField( - modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) - .fillMaxWidth(), - readOnly = true, - label = { Text(stringResource(R.string.profile_namespace)) }, - value = currentNamespace, - onValueChange = {}, - trailingIcon = { - if (expanded) Icon(Icons.Filled.ArrowDropUp, null) - else Icon(Icons.Filled.ArrowDropDown, null) - }, - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.profile_namespace_inherited)) }, - onClick = { - 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)) - expanded = false - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.profile_namespace_individual)) }, - onClick = { - onProfileChange(profile.copy(namespace = Natives.Profile.Namespace.INDIVIDUAL.ordinal)) - expanded = false - }, - ) - } - } - }) - */ - - UidPanel(uid = profile.uid, label = "uid", onUidChange = { - onProfileChange( - profile.copy( - uid = it, - rootUseDefault = false - ) - ) - }) - - UidPanel(uid = profile.gid, label = "gid", onUidChange = { - onProfileChange( - profile.copy( - gid = it, - rootUseDefault = false - ) - ) - }) - - val selectedGroups = profile.groups.ifEmpty { listOf(0) }.let { e -> - e.mapNotNull { g -> - Groups.entries.find { it.gid == g } - } - } - GroupsPanel(selectedGroups) { - onProfileChange( - profile.copy( - groups = it.map { group -> group.gid }.ifEmpty { listOf(0) }, - rootUseDefault = false - ) - ) - } - - val selectedCaps = profile.capabilities.mapNotNull { e -> - Capabilities.entries.find { it.cap == e } - } - - CapsPanel(selectedCaps) { - onProfileChange( - profile.copy( - capabilities = it.map { cap -> cap.cap }, - rootUseDefault = false - ) - ) - } - - SELinuxPanel(profile = profile, onSELinuxChange = { domain, rules -> - onProfileChange( - profile.copy( - context = domain, - rules = rules, - rootUseDefault = false - ) - ) - }) - - } -} - -@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 }) - - ) - val options = groups.map { value -> - ListOption( - titleText = value.display, - subtitleText = value.desc, - selected = selected.contains(value), - ) - } - - val selection = HashSet(selected) - ListDialog( - state = rememberUseCaseState(visible = true, onFinishedRequest = { - closeSelection(selection) - }, onCloseRequest = { - dismiss() - }), - header = Header.Default( - title = stringResource(R.string.profile_groups), - ), - selection = ListSelection.Multiple( - showCheckBoxes = true, - options = options, - maxChoices = 32, // Kernel only supports 32 groups at most - ) { indecies, _ -> - // Handle selection - selection.clear() - indecies.forEach { index -> - val group = groups[index] - selection.add(group) - } - } - ) - } - - OutlinedCard( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - - Column( - modifier = Modifier - .fillMaxSize() - .clickable { - selectGroupsDialog.show() - } - .padding(16.dp) - ) { - Text(stringResource(R.string.profile_groups)) - FlowRow { - selected.forEach { group -> - AssistChip( - modifier = Modifier.padding(3.dp), - onClick = { /*TODO*/ }, - label = { Text(group.display) }) - } - } - } - - } -} - -@OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) -@Composable -fun CapsPanel( - selected: Collection, - closeSelection: (selection: Set) -> Unit -) { - 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, - subtitleText = value.desc, - selected = selected.contains(value), - ) - } - - val selection = HashSet(selected) - ListDialog( - state = rememberUseCaseState(visible = true, onFinishedRequest = { - closeSelection(selection) - }, onCloseRequest = { - dismiss() - }), - header = Header.Default( - title = stringResource(R.string.profile_capabilities), - ), - selection = ListSelection.Multiple( - showCheckBoxes = true, - options = options - ) { indecies, _ -> - // Handle selection - selection.clear() - indecies.forEach { index -> - val group = caps[index] - selection.add(group) - } - } - ) - } - - OutlinedCard( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - - Column( - modifier = Modifier - .fillMaxSize() - .clickable { - selectCapabilitiesDialog.show() - } - .padding(16.dp) - ) { - Text(stringResource(R.string.profile_capabilities)) - FlowRow { - selected.forEach { group -> - AssistChip( - modifier = Modifier.padding(3.dp), - onClick = { /*TODO*/ }, - label = { Text(group.display) }) - } - } - } - - } -} - -@Composable -private fun UidPanel(uid: Int, label: String, onUidChange: (Int) -> Unit) { - - ListItem(headlineContent = { - var isError by remember { - mutableStateOf(false) - } - var lastValidUid by remember { - mutableIntStateOf(uid) - } - val keyboardController = LocalSoftwareKeyboardController.current - - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - label = { Text(label) }, - value = uid.toString(), - isError = isError, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Done - ), - keyboardActions = KeyboardActions(onDone = { - keyboardController?.hide() - }), - onValueChange = { - if (it.isEmpty()) { - onUidChange(0) - return@OutlinedTextField - } - val valid = isTextValidUid(it) - - val targetUid = if (valid) it.toInt() else lastValidUid - if (valid) { - lastValidUid = it.toInt() - } - - onUidChange(targetUid) - - isError = !valid - } - ) - }) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun SELinuxPanel( - profile: Natives.Profile, - onSELinuxChange: (domain: String, rules: String) -> Unit -) { - val editSELinuxDialog = rememberCustomDialog { dismiss -> - var domain by remember { mutableStateOf(profile.context) } - var rules by remember { mutableStateOf(profile.rules) } - - val inputOptions = listOf( - InputTextField( - text = domain, - header = InputHeader( - title = stringResource(id = R.string.profile_selinux_domain), - ), - type = InputTextFieldType.OUTLINED, - required = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Ascii, - imeAction = ImeAction.Next - ), - resultListener = { - domain = it ?: "" - }, - validationListener = { value -> - // value can be a-zA-Z0-9_ - val regex = Regex("^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$") - if (value?.matches(regex) == true) ValidationResult.Valid - else ValidationResult.Invalid("Domain must be in the format of \"user:role:type:level\"") - } - ), - InputTextField( - text = rules, - header = InputHeader( - title = stringResource(id = R.string.profile_selinux_rules), - ), - type = InputTextFieldType.OUTLINED, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Ascii, - ), - singleLine = false, - resultListener = { - rules = it ?: "" - }, - validationListener = { value -> - if (isSepolicyValid(value)) ValidationResult.Valid - else ValidationResult.Invalid("SELinux rules is invalid!") - } - ) - ) - - InputDialog( - state = rememberUseCaseState(visible = true, - onFinishedRequest = { - onSELinuxChange(domain, rules) - }, - onCloseRequest = { - dismiss() - }), - header = Header.Default( - title = stringResource(R.string.profile_selinux_context), - ), - selection = InputSelection( - input = inputOptions, - onPositiveClick = { result -> - // Handle selection - }, - ) - ) - } - - ListItem(headlineContent = { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .clickable { - editSELinuxDialog.show() - }, - enabled = false, - colors = OutlinedTextFieldDefaults.colors( - disabledTextColor = MaterialTheme.colorScheme.onSurface, - disabledBorderColor = MaterialTheme.colorScheme.outline, - disabledPlaceholderColor = MaterialTheme.colorScheme.onSurfaceVariant, - disabledLabelColor = MaterialTheme.colorScheme.onSurfaceVariant - ), - label = { Text(text = stringResource(R.string.profile_selinux_context)) }, - value = profile.context, - onValueChange = { } - ) - }) -} - -@Preview -@Composable -private fun RootProfileConfigPreview() { - var profile by remember { mutableStateOf(Natives.Profile("")) } - RootProfileConfig(fixedName = true, profile = profile) { - profile = it - } -} - -private fun isTextValidUid(text: String): Boolean { - return text.isNotEmpty() && text.isDigitsOnly() && text.toInt() >= 0 && text.toInt() <= Int.MAX_VALUE -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt deleted file mode 100644 index 502b2932..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.sukisu.ultra.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 com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.util.listAppProfileTemplates -import com.sukisu.ultra.ui.util.setSepolicy -import com.sukisu.ultra.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/com/sukisu/ultra/ui/screen/AppProfile.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt deleted file mode 100644 index e28a4cea..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt +++ /dev/null @@ -1,593 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.annotation.SuppressLint -import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.gestures.detectTapGestures -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.Security -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilterChip -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarColors -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 -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.shadow -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 -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.dropUnlessResumed -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 com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.SwitchItem -import com.sukisu.ultra.ui.component.profile.AppProfileConfig -import com.sukisu.ultra.ui.component.profile.RootProfileConfig -import com.sukisu.ultra.ui.component.profile.TemplateConfig -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.util.LocalSnackbarHost -import com.sukisu.ultra.ui.util.forceStopApp -import com.sukisu.ultra.ui.util.getSepolicy -import com.sukisu.ultra.ui.util.launchApp -import com.sukisu.ultra.ui.util.restartApp -import com.sukisu.ultra.ui.util.setSepolicy -import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel -import com.sukisu.ultra.ui.viewmodel.getTemplateInfoById -import kotlinx.coroutines.launch - -/** - * @author weishu - * @date 2023/5/16. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun AppProfileScreen( - navigator: DestinationsNavigator, - appInfo: SuperUserViewModel.AppInfo, -) { - val context = LocalContext.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 suNotAllowed = stringResource(R.string.su_not_allowed).format(appInfo.label) - - val packageName = appInfo.packageName - val initialProfile = Natives.getAppProfile(packageName, appInfo.uid) - if (initialProfile.allowSu) { - initialProfile.rules = getSepolicy(packageName) - } - var profile by rememberSaveable { - mutableStateOf(initialProfile) - } - - val cardColor = MaterialTheme.colorScheme.surfaceVariant - val cardAlpha = CardConfig.cardAlpha - - Scaffold( - topBar = { - TopBar( - title = appInfo.label, - packageName = packageName, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - onBack = dropUnlessResumed { 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(), - contentDescription = appInfo.label, - modifier = Modifier - .padding(4.dp) - .width(48.dp) - .height(48.dp) - ) - }, - profile = profile, - onViewTemplate = { - getTemplateInfoById(it)?.let { info -> - navigator.navigate(TemplateEditorScreenDestination(info)) - } - }, - onManageTemplate = { - navigator.navigate(AppProfileTemplateScreenDestination()) - }, - onProfileChange = { - scope.launch { - if (it.allowSu) { - // sync with allowlist.c - forbid_system_uid - if (appInfo.uid < 2000 && appInfo.uid != 1000) { - snackBarHost.showSnackbar(suNotAllowed) - return@launch - } - if (!it.rootUseDefault && it.rules.isNotEmpty() && !setSepolicy(profile.name, it.rules)) { - snackBarHost.showSnackbar(failToUpdateSepolicy) - return@launch - } - } - if (!Natives.setAppProfile(it)) { - snackBarHost.showSnackbar(failToUpdateAppProfile.format(appInfo.uid)) - } else { - profile = it - } - } - }, - ) - } -} - -@Composable -private fun AppProfileInner( - modifier: Modifier = Modifier, - packageName: String, - appLabel: String, - appIcon: @Composable () -> Unit, - profile: Natives.Profile, - onViewTemplate: (id: String) -> Unit = {}, - onManageTemplate: () -> Unit = {}, - onProfileChange: (Natives.Profile) -> Unit, -) { - val isRootGranted = profile.allowSu - - Column(modifier = modifier) { - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium - ) { - AppMenuBox(packageName) { - ListItem( - headlineContent = { - Text( - text = appLabel, - style = MaterialTheme.typography.titleMedium - ) - }, - supportingContent = { - Text( - text = packageName, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - leadingContent = appIcon, - ) - } - } - - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium - ) { - SwitchItem( - icon = Icons.Filled.Security, - title = stringResource(id = R.string.superuser), - checked = isRootGranted, - onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, - ) - } - - Crossfade( - targetState = isRootGranted, - label = "RootAccess" - ) { current -> - Column( - modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */) - ) { - if (current) { - val initialMode = if (profile.rootUseDefault) { - Mode.Default - } else if (profile.rootTemplate != null) { - Mode.Template - } else { - Mode.Custom - } - var mode by rememberSaveable { - mutableStateOf(initialMode) - } - - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium - ) { - ProfileBox(mode, true) { - // template mode shouldn't change profile here! - if (it == Mode.Default || it == Mode.Custom) { - onProfileChange(profile.copy(rootUseDefault = it == Mode.Default)) - } - mode = it - } - } - - AnimatedVisibility( - visible = mode != Mode.Default, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - Crossfade(targetState = mode, label = "ProfileMode") { currentMode -> - when (currentMode) { - Mode.Template -> { - TemplateConfig( - profile = profile, - onViewTemplate = onViewTemplate, - onManageTemplate = onManageTemplate, - onProfileChange = onProfileChange - ) - } - Mode.Custom -> { - RootProfileConfig( - fixedName = true, - profile = profile, - onProfileChange = onProfileChange - ) - } - else -> {} - } - } - } - } - } - } else { - val mode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom - - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium - ) { - ProfileBox(mode, false) { - onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default))) - } - } - - AnimatedVisibility( - visible = mode == Mode.Custom, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - ElevatedCard( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - shape = MaterialTheme.shapes.medium - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - AppProfileConfig( - fixedName = true, - profile = profile, - enabled = mode == Mode.Custom, - onProfileChange = onProfileChange - ) - } - } - } - } - } - } - } -} - -private enum class Mode(@StringRes private val res: Int) { - Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom); - - val text: String - @Composable get() = stringResource(res) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar( - title: String, - packageName: String, - onBack: () -> Unit, - colors: TopAppBarColors, - scrollBehavior: TopAppBarScrollBehavior? = null -) { - TopAppBar( - title = { - Column { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = packageName, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.alpha(0.8f) - ) - } - }, - colors = colors, - navigationIcon = { - IconButton( - onClick = onBack, - colors = IconButtonDefaults.iconButtonColors( - contentColor = MaterialTheme.colorScheme.onSurface - ) - ) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back) - ) - } - }, - windowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), - scrollBehavior = scrollBehavior, - modifier = Modifier.shadow( - elevation = if ((scrollBehavior?.state?.overlappedFraction ?: 0f) > 0.01f) - 4.dp else 0.dp, - spotColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) - ) - ) -} - -@Composable -private fun ProfileBox( - mode: Mode, - hasTemplate: Boolean, - onModeChange: (Mode) -> Unit, -) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - ListItem( - headlineContent = { - Text( - text = stringResource(R.string.profile), - style = MaterialTheme.typography.titleMedium - ) - }, - supportingContent = { - Text( - text = mode.text, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - leadingContent = { - Icon( - imageVector = Icons.Filled.AccountCircle, - contentDescription = null, - ) - }, - ) - - HorizontalDivider( - thickness = Dp.Hairline, - color = MaterialTheme.colorScheme.outlineVariant - ) - - ListItem( - headlineContent = { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally) - ) { - FilterChip( - selected = mode == Mode.Default, - onClick = { onModeChange(Mode.Default) }, - label = { - Text( - text = stringResource(R.string.profile_default), - style = MaterialTheme.typography.bodyMedium - ) - }, - shape = MaterialTheme.shapes.small - ) - - if (hasTemplate) { - FilterChip( - selected = mode == Mode.Template, - onClick = { onModeChange(Mode.Template) }, - label = { - Text( - text = stringResource(R.string.profile_template), - style = MaterialTheme.typography.bodyMedium - ) - }, - shape = MaterialTheme.shapes.small - ) - } - - FilterChip( - selected = mode == Mode.Custom, - onClick = { onModeChange(Mode.Custom) }, - label = { - Text( - text = stringResource(R.string.profile_custom), - style = MaterialTheme.typography.bodyMedium - ) - }, - shape = MaterialTheme.shapes.small - ) - } - } - ) - } -} - -@SuppressLint("UnusedBoxWithConstraintsScope") -@Composable -private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) { - var expanded by remember { mutableStateOf(false) } - var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) } - val density = LocalDensity.current - - BoxWithConstraints( - Modifier - .fillMaxSize() - .pointerInput(Unit) { - detectTapGestures( - onLongPress = { - touchPoint = it - expanded = true - } - ) - } - ) { - content() - - val (offsetX, offsetY) = with(density) { - (touchPoint.x.toDp()) to (touchPoint.y.toDp()) - } - - DropdownMenu( - expanded = expanded, - offset = DpOffset(offsetX, -offsetY), - onDismissRequest = { - expanded = false - }, - ) { - AppMenuOption( - text = stringResource(id = R.string.launch_app), - onClick = { - expanded = false - launchApp(packageName) - } - ) - - AppMenuOption( - text = stringResource(id = R.string.force_stop_app), - onClick = { - expanded = false - forceStopApp(packageName) - } - ) - - AppMenuOption( - text = stringResource(id = R.string.restart_app), - onClick = { - expanded = false - restartApp(packageName) - } - ) - } - } -} - -@Composable -private fun AppMenuOption(text: String, onClick: () -> Unit) { - DropdownMenuItem( - text = { - Text( - text = text, - style = MaterialTheme.typography.bodyMedium - ) - }, - onClick = onClick - ) -} - -@Preview -@Composable -private fun AppProfilePreview() { - var profile by remember { mutableStateOf(Natives.Profile("")) } - Surface { - AppProfileInner( - packageName = "icu.nullptr.test", - appLabel = "Test", - appIcon = { - Icon( - imageVector = Icons.Filled.Android, - contentDescription = null, - ) - }, - profile = profile, - onProfileChange = { - profile = it - }, - ) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/BottomBarDestination.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/BottomBarDestination.kt deleted file mode 100644 index cbcdc292..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/BottomBarDestination.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import androidx.annotation.StringRes -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.generated.destinations.SettingScreenDestination -import com.ramcosta.composedestinations.generated.destinations.KpmScreenDestination -import com.ramcosta.composedestinations.spec.DirectionDestinationSpec -import com.sukisu.ultra.R - -enum class BottomBarDestination( - val direction: DirectionDestinationSpec, - @StringRes val label: Int, - val iconSelected: ImageVector, - val iconNotSelected: ImageVector, - val rootRequired: Boolean, -) { - Home(HomeScreenDestination, R.string.home, Icons.Filled.Home, Icons.Outlined.Home, false), - Kpm(KpmScreenDestination, R.string.kpm_title, Icons.Filled.Build, Icons.Outlined.Build, true), - SuperUser(SuperUserScreenDestination, R.string.superuser, Icons.Filled.Security, Icons.Outlined.Security, true), - Module(ModuleScreenDestination, R.string.module, Icons.Filled.Apps, Icons.Outlined.Apps, true), - Settings(SettingScreenDestination, R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings, false), -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/ExecuteModuleAction.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/ExecuteModuleAction.kt deleted file mode 100644 index c43c4f8a..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/ExecuteModuleAction.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.sukisu.ultra.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 androidx.lifecycle.compose.dropUnlessResumed -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 com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.KeyEventBlocker -import com.sukisu.ultra.ui.util.LocalSnackbarHost -import com.sukisu.ultra.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 = dropUnlessResumed { - 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/com/sukisu/ultra/ui/screen/Flash.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt deleted file mode 100644 index ccc65e19..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt +++ /dev/null @@ -1,236 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.net.Uri -import android.os.Environment -import android.os.Parcelable -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.automirrored.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.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 androidx.lifecycle.compose.dropUnlessResumed -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 com.sukisu.ultra.ui.component.KeyEventBlocker -import com.sukisu.ultra.ui.util.* -import com.sukisu.ultra.R -import java.io.File -import java.text.SimpleDateFormat -import java.util.* - -enum class FlashingStatus { - FLASHING, - SUCCESS, - FAILED -} - -private var currentFlashingStatus = mutableStateOf(FlashingStatus.FLASHING) - -fun setFlashingStatus(status: FlashingStatus) { - currentFlashingStatus.value = status -} - -@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()) - - LaunchedEffect(Unit) { - if (text.isNotEmpty()) { - return@LaunchedEffect - } - withContext(Dispatchers.IO) { - setFlashingStatus(FlashingStatus.FLASHING) - flashIt(flashIt, onFinish = { showReboot, code -> - if (code != 0) { - text += "Error: exit code = $code.\nPlease save and check the log.\n" - setFlashingStatus(FlashingStatus.FAILED) - } else { - setFlashingStatus(FlashingStatus.SUCCESS) - } - if (showReboot) { - text += "\n\n\n" - showFloatAction = true - } - }, onStdout = { - tempText = "$it\n" - if (tempText.startsWith("[H[J")) { // clear command - text = tempText.substring(6) - } else { - text += tempText - } - logContent.append(it).append("\n") - }, onStderr = { - logContent.append(it).append("\n") - }) - } - } - - Scaffold( - topBar = { - TopBar( - currentFlashingStatus.value, - onBack = dropUnlessResumed { - 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 cardColor = MaterialTheme.colorScheme.secondaryContainer - 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) }, - containerColor = cardColor.copy(alpha = 1f), - contentColor = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - }, - 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 FlashScreenPreview() { - FlashScreen(EmptyDestinationsNavigator, FlashIt.FlashUninstall) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt deleted file mode 100644 index 39bea950..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt +++ /dev/null @@ -1,1022 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.annotation.SuppressLint -import android.content.Context -import android.os.Build -import android.os.PowerManager -import android.system.Os -import androidx.annotation.StringRes -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Android -import androidx.compose.material.icons.filled.Archive -import androidx.compose.material.icons.filled.Code -import androidx.compose.material.icons.filled.Info -import androidx.compose.material.icons.filled.Memory -import androidx.compose.material.icons.filled.PhoneAndroid -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Security -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.Storage -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material.icons.outlined.Block -import androidx.compose.material.icons.outlined.CheckCircle -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.Warning -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.TopAppBarScrollBehavior -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableLongStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.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.edit -import androidx.core.content.pm.PackageInfoCompat -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.KernelVersion -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.getKernelVersion -import com.sukisu.ultra.ksuApp -import com.sukisu.ultra.ui.component.rememberConfirmDialog -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation -import com.sukisu.ultra.ui.theme.getCardColors -import com.sukisu.ultra.ui.util.checkNewVersion -import com.sukisu.ultra.ui.util.getKpmModuleCount -import com.sukisu.ultra.ui.util.getKpmVersion -import com.sukisu.ultra.ui.util.getModuleCount -import com.sukisu.ultra.ui.util.getSELinuxStatus -import com.sukisu.ultra.ui.util.getSuSFS -import com.sukisu.ultra.ui.util.getSuSFSFeatures -import com.sukisu.ultra.ui.util.getSuSFSVariant -import com.sukisu.ultra.ui.util.getSuSFSVersion -import com.sukisu.ultra.ui.util.getSuperuserCount -import com.sukisu.ultra.ui.util.module.LatestVersionInfo -import com.sukisu.ultra.ui.util.reboot -import com.sukisu.ultra.ui.util.rootAvailable -import com.sukisu.ultra.ui.util.susfsSUS_SU_Mode -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.withContext -import java.io.BufferedReader -import java.io.InputStreamReader -import java.util.zip.GZIPInputStream -import kotlin.random.Random - -@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) -@Destination(start = true) -@Composable -fun HomeScreen(navigator: DestinationsNavigator) { - val context = LocalContext.current - var isSimpleMode by rememberSaveable { mutableStateOf(false) } - var isHideVersion by rememberSaveable { mutableStateOf(false) } - var isHideOtherInfo by rememberSaveable { mutableStateOf(false) } - var isHideSusfsStatus by rememberSaveable { mutableStateOf(false) } - var isHideLinkCard by rememberSaveable { mutableStateOf(false) } - - // 从 SharedPreferences 加载简洁模式状态 - LaunchedEffect(Unit) { - isSimpleMode = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("is_simple_mode", false) - - isHideVersion = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("is_hide_version", false) - - isHideOtherInfo = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("is_hide_other_info", false) - - isHideSusfsStatus = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("is_hide_susfs_status", false) - - isHideLinkCard = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("is_hide_link_card", false) - } - - val kernelVersion = getKernelVersion() - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - - val isManager = Natives.becomeManager(ksuApp.packageName) - val deviceModel = getDeviceModel() - val ksuVersion = if (isManager) Natives.version else null - val zako = "一.*加.*A.*c.*e.*5.*P.*r.*o".toRegex().matches(deviceModel) - val isVersion = ksuVersion == 12777 - val shouldTriggerRestart = zako && kernelVersion.isGKI() && (isVersion) - - LaunchedEffect(shouldTriggerRestart) { - if (shouldTriggerRestart) { - val random = Random.nextInt(0, 100) - if (random <= 95) { - reboot() - } else { - "" - } - } - } - - val scrollState = rememberScrollState() - val debounceTime = 100L - var lastScrollTime by remember { mutableLongStateOf(0L) } - - Scaffold( - topBar = { - TopBar( - kernelVersion, - onInstallClick = { navigator.navigate(InstallScreenDestination) }, - scrollBehavior = scrollBehavior - ) - }, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ) - ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .disableOverscroll() - .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(scrollState) - .padding(top = 12.dp) - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - if (shouldTriggerRestart) { - WarningCard(message = "zakozako") - return@Column - } - val isManager = Natives.becomeManager(ksuApp.packageName) - 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, lkmMode) { - navigator.navigate(InstallScreenDestination) - } - - if (isManager && Natives.requireNewKernel()) { - WarningCard( - stringResource(id = R.string.require_kernel_version).format( - ksuVersion, Natives.MINIMAL_SUPPORTED_KERNEL - ) - ) - } - - 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() - } - - val prefs = remember { context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE) } - var clickCount by rememberSaveable { mutableIntStateOf(prefs.getInt("click_count", 0)) } - - if (!isSimpleMode && clickCount < 3) { - AnimatedVisibility( - visible = clickCount < 3, - enter = fadeIn() + expandVertically(), - exit = shrinkVertically() + fadeOut() - ) { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.secondaryContainer), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - clickCount++ - prefs.edit { putInt("click_count", clickCount) } - } - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Outlined.Info, - contentDescription = null, - modifier = Modifier.padding(end = 12.dp) - ) - Text( - text = stringResource(R.string.using_mksu_manager), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } - } - } - } - - InfoCard() - - if (!isSimpleMode) { - if (!isHideLinkCard) { - ContributionCard() - DonateCard() - LearnMoreCard() - } - } - Spacer(Modifier.height(16.dp)) - } - } - - LaunchedEffect(scrollState) { - snapshotFlow { scrollState.isScrollInProgress } - .debounce(debounceTime) - .collect { isScrolling -> - if (isScrolling) { - val currentTime = System.currentTimeMillis() - if (currentTime - lastScrollTime > debounceTime) { - lastScrollTime = currentTime - } - } - } - } -} - -@Composable -fun UpdateCard() { - val context = LocalContext.current - val latestVersionInfo = LatestVersionInfo() - val newVersion by produceState(initialValue = latestVersionInfo) { - value = withContext(Dispatchers.IO) { - checkNewVersion() - } - } - - val currentVersionCode = getManagerVersion(context).second - val newVersionCode = newVersion.versionCode - val newVersionUrl = newVersion.downloadUrl - val changelog = newVersion.changelog - - val uriHandler = LocalUriHandler.current - val title = stringResource(id = R.string.module_changelog) - val updateText = stringResource(id = R.string.module_update) - - AnimatedVisibility( - visible = newVersionCode > currentVersionCode, - enter = fadeIn() + expandVertically( - animationSpec = spring( - dampingRatio = Spring.DampingRatioMediumBouncy, - stiffness = Spring.StiffnessLow - ) - ), - exit = shrinkVertically() + fadeOut() - ) { - val updateDialog = rememberConfirmDialog(onConfirm = { uriHandler.openUri(newVersionUrl) }) - WarningCard( - message = stringResource(id = R.string.new_version_available).format(newVersionCode), - color = MaterialTheme.colorScheme.tertiaryContainer, - onClick = { - if (changelog.isEmpty()) { - uriHandler.openUri(newVersionUrl) - } else { - updateDialog.showConfirm( - title = title, - content = changelog, - markdown = true, - confirm = updateText - ) - } - } - ) - } -} - -@Composable -fun RebootDropdownItem(@StringRes id: Int, reason: String = "") { - DropdownMenuItem( - text = { Text(stringResource(id)) }, - onClick = { reboot(reason) }, - leadingIcon = { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = null, - ) - } - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar( - kernelVersion: KernelVersion, - onInstallClick: () -> Unit, - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val cardColor = MaterialTheme.colorScheme.surfaceVariant - val cardAlpha = CardConfig.cardAlpha - - TopAppBar( - title = { - Text( - text = stringResource(R.string.app_name), - style = MaterialTheme.typography.titleLarge - ) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - actions = { - if (rootAvailable() || kernelVersion.isGKI()) { - IconButton(onClick = onInstallClick) { - Icon( - Icons.Filled.Archive, - contentDescription = stringResource(R.string.install), - ) - } - } - - var showDropdown by remember { mutableStateOf(false) } - if (Natives.isKsuValid(ksuApp.packageName)) { - IconButton(onClick = { showDropdown = true }) { - Icon( - Icons.Filled.Refresh, - contentDescription = stringResource(R.string.reboot), - ) - - DropdownMenu( - expanded = showDropdown, - onDismissRequest = { showDropdown = false } - ) { - RebootDropdownItem(id = R.string.reboot) - - val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? - @Suppress("DEPRECATION") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { - RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace") - } - RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery") - RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader") - RebootDropdownItem(id = R.string.reboot_download, reason = "download") - RebootDropdownItem(id = R.string.reboot_edl, reason = "edl") - } - } - } - }, - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) -} - -@Composable -private fun StatusCard( - kernelVersion: KernelVersion, - ksuVersion: Int?, - lkmMode: Boolean?, - onClickInstall: () -> Unit = {} -) { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.large) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.large, - spotColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.1f) - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - if (rootAvailable() || kernelVersion.isGKI()) { - onClickInstall() - } - } - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - when { - ksuVersion != null -> { - 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" - - val isHideVersion = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("is_hide_version", false) - - val isHideOtherInfo = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("is_hide_other_info", false) - - val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("is_hide_susfs_status", false) - - Icon( - Icons.Outlined.CheckCircle, - contentDescription = stringResource(R.string.home_working), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - - Column(Modifier.padding(start = 20.dp)) { - Text( - text = workingText, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - - if (!isHideVersion) { - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_working_version, ksuVersion), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - if (!isHideOtherInfo) { - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_superuser_count, getSuperuserCount()), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_module_count, getModuleCount()), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - val kpmVersion = getKpmVersion() - if (kpmVersion.isNotEmpty() && !kpmVersion.startsWith("Error")) { - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_kpm_module, getKpmModuleCount()), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - if (!isHideSusfsStatus) { - Spacer(modifier = Modifier.height(4.dp)) - - val suSFS = getSuSFS() - if (lkmMode != true) { - val translatedStatus = when (suSFS) { - "Supported" -> stringResource(R.string.status_supported) - "Not Supported" -> stringResource(R.string.status_not_supported) - else -> stringResource(R.string.status_unknown) - } - - Text( - text = stringResource(R.string.home_susfs, translatedStatus), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - - kernelVersion.isGKI() -> { - Icon( - Icons.Outlined.Warning, - contentDescription = stringResource(R.string.home_not_installed), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(24.dp) - ) - - Column(Modifier.padding(start = 20.dp)) { - Text( - text = stringResource(R.string.home_not_installed), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.error - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_click_to_install), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - else -> { - Icon( - Icons.Outlined.Block, - contentDescription = stringResource(R.string.home_unsupported), - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(24.dp) - ) - - Column(Modifier.padding(start = 20.dp)) { - Text( - text = stringResource(R.string.home_unsupported), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.error - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_unsupported_reason), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - } -} - -@Composable -fun WarningCard( - message: String, - color: Color = MaterialTheme.colorScheme.errorContainer, - onClick: (() -> Unit)? = null -) { - ElevatedCard( - colors = getCardColors(color), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.large) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.large, - spotColor = MaterialTheme.colorScheme.error.copy(alpha = 0.1f) - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .then(onClick?.let { Modifier.clickable { it() } } ?: Modifier) - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Filled.Warning, - contentDescription = null, - tint = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier - .padding(end = 16.dp) - .size(28.dp) - ) - Text( - text = message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer - ) - } - } -} - -@Composable -fun ContributionCard() { - val uriHandler = LocalUriHandler.current - val links = listOf("https://github.com/zako", "https://github.com/udochina") - - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .clip(MaterialTheme.shapes.large) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.large, - spotColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f) - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - val randomIndex = Random.nextInt(links.size) - uriHandler.openUri(links[randomIndex]) - } - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = stringResource(R.string.home_ContributionCard_kernelsu), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_click_to_ContributionCard_kernelsu), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) - ) - } - } - } -} - -@Composable -fun LearnMoreCard() { - val uriHandler = LocalUriHandler.current - val url = stringResource(R.string.home_learn_kernelsu_url) - - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.large) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.large, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - uriHandler.openUri(url) - } - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = stringResource(R.string.home_learn_kernelsu), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_click_to_learn_kernelsu), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) - ) - } - } - } -} - -@Composable -fun DonateCard() { - val uriHandler = LocalUriHandler.current - - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.large) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.large, - spotColor = MaterialTheme.colorScheme.secondary.copy(alpha = 0.1f) - ) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - uriHandler.openUri("https://patreon.com/weishu") - } - .padding(24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = stringResource(R.string.home_support_title), - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(Modifier.height(4.dp)) - Text( - text = stringResource(R.string.home_support_content), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) - ) - } - } - } -} - -@Composable -private fun InfoCard() { - val lkmMode = Natives.isLkmMode - val context = LocalContext.current - val isSimpleMode = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("is_simple_mode", false) - - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHighest), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.large) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.large, - spotColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.05f) - ) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp), - ) withContext@{ - val contents = StringBuilder() - val uname = Os.uname() - - @Composable - fun InfoCardItem( - label: String, - content: String, - icon: ImageVector = Icons.Default.Info - ) { - contents.appendLine(label).appendLine(content).appendLine() - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Icon( - imageVector = icon, - contentDescription = label, - modifier = Modifier.size(24.dp), - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.7f), - ) - Spacer(modifier = Modifier.width(16.dp)) - Column( - modifier = Modifier - .fillMaxWidth() - .weight(1f) - ){ - Text( - text = label, - style = MaterialTheme.typography.labelLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = content, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface, - softWrap = true - ) - } - } - } - - InfoCardItem( - stringResource(R.string.home_kernel), - uname.release, - icon = Icons.Default.Memory, - ) - - if (!isSimpleMode) { - val androidVersion = Build.VERSION.RELEASE - InfoCardItem( - stringResource(R.string.home_android_version), - androidVersion, - icon = Icons.Default.Android, - ) - } - - val deviceModel = getDeviceModel() - InfoCardItem( - stringResource(R.string.home_device_model), - deviceModel, - icon = Icons.Default.PhoneAndroid, - ) - - val managerVersion = getManagerVersion(context) - InfoCardItem( - stringResource(R.string.home_manager_version), - "${managerVersion.first} (${managerVersion.second})", - icon = Icons.Default.Settings, - ) - - InfoCardItem( - stringResource(R.string.home_selinux_status), - getSELinuxStatus(), - icon = Icons.Default.Security, - ) - - if (!isSimpleMode) { - if (lkmMode != true) { - val kpmVersion = getKpmVersion() - val isKpmConfigured = checkKpmConfigured() - - val displayVersion = if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) { - val statusText = if (isKpmConfigured) { - stringResource(R.string.kernel_patched) - } else { - stringResource(R.string.kernel_not_enabled) - } - "${stringResource(R.string.not_supported)} ($statusText)" - } else { - "${stringResource(R.string.supported)} ($kpmVersion)" - } - - InfoCardItem( - stringResource(R.string.home_kpm_version), - displayVersion, - icon = Icons.Default.Code - ) - } - } - - val isHideSusfsStatus = LocalContext.current.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("is_hide_susfs_status", false) - - if ((!isSimpleMode) && (!isHideSusfsStatus)) { - val suSFS = getSuSFS() - if (suSFS == "Supported") { - val suSFSVersion = getSuSFSVersion() - if (suSFSVersion.isNotEmpty()) { - val isSUS_SU = getSuSFSFeatures() == "CONFIG_KSU_SUSFS_SUS_SU" - val infoText = buildString { - append(suSFSVersion) - append(if (isSUS_SU) " (${getSuSFSVariant()})" else " (${stringResource(R.string.manual_hook)})") - if (isSUS_SU) { - val susSUMode = try { susfsSUS_SU_Mode().toString() } catch (_: Exception) { "" } - if (susSUMode.isNotEmpty()) { - append(" ${stringResource(R.string.sus_su_mode)} $susSUMode") - } - } - } - - InfoCardItem( - stringResource(R.string.home_susfs_version), - infoText, - icon = Icons.Default.Storage - ) - } - } - } - } - } -} - -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(verticalArrangement = Arrangement.spacedBy(8.dp)) { - 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) - } -} - -@Preview -@Composable -private fun WarningCardPreview() { - Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - WarningCard(message = "Warning message") - WarningCard( - message = "Warning message ", - MaterialTheme.colorScheme.tertiaryContainer, - onClick = {}) - } -} - -@SuppressLint("PrivateApi") -private fun getDeviceModel(): String { - return try { - val systemProperties = Class.forName("android.os.SystemProperties") - val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java) - val marketNameKeys = listOf( - "ro.product.marketname", // Xiaomi - "ro.vendor.oplus.market.name", // Oppo, OnePlus, Realme - "ro.vivo.market.name", // Vivo - "ro.config.marketing_name" // Huawei - ) - for (key in marketNameKeys) { - val marketName = getMethod.invoke(null, key, "") as String - if (marketName.isNotEmpty()) { - return marketName - } - } - Build.DEVICE - } catch (_: Exception) { - Build.DEVICE - } -} - -private fun checkKpmConfigured(): Boolean { - try { - val process = Runtime.getRuntime().exec("su -c cat /proc/config.gz") - val inputStream = process.inputStream - val gzipInputStream = GZIPInputStream(inputStream) - val reader = BufferedReader(InputStreamReader(gzipInputStream)) - - var line: String? - while (reader.readLine().also { line = it } != null) { - if (line?.contains("CONFIG_KPM=y") == true) { - return true - } - } - reader.close() - } catch (e: Exception) { - e.printStackTrace() - } - return false -} - -@SuppressLint("UnnecessaryComposedModifier") -fun Modifier.disableOverscroll(): Modifier = composed { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - this - } else { - this - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt deleted file mode 100644 index 3b32dbee..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt +++ /dev/null @@ -1,788 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.app.Activity -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.annotation.StringRes -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.LocalIndication -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -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.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.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.AutoFixHigh -import androidx.compose.material.icons.filled.FileUpload -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ElevatedCard -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.RadioButton -import androidx.compose.material3.RadioButtonDefaults -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -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.collectAsState -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.draw.clip -import androidx.compose.ui.draw.shadow -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.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 com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator -import com.sukisu.ultra.R -import com.sukisu.ultra.flash.HorizonKernelFlashProgress -import com.sukisu.ultra.flash.HorizonKernelState -import com.sukisu.ultra.flash.HorizonKernelWorker -import com.sukisu.ultra.ui.component.DialogHandle -import com.sukisu.ultra.ui.component.SlotSelectionDialog -import com.sukisu.ultra.ui.component.rememberConfirmDialog -import com.sukisu.ultra.ui.component.rememberCustomDialog -import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation -import com.sukisu.ultra.ui.theme.getCardColors -import com.sukisu.ultra.ui.util.LkmSelection -import com.sukisu.ultra.ui.util.getCurrentKmi -import com.sukisu.ultra.ui.util.getSupportedKmis -import com.sukisu.ultra.ui.util.isAbDevice -import com.sukisu.ultra.ui.util.isInitBoot -import com.sukisu.ultra.ui.util.rootAvailable -import com.sukisu.ultra.getKernelVersion - -/** - * @author weishu - * @date 2024/3/12. - */ -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun InstallScreen(navigator: DestinationsNavigator) { - var installMethod by remember { mutableStateOf(null) } - var lkmSelection by remember { mutableStateOf(LkmSelection.KmiNone) } - val context = LocalContext.current - var showRebootDialog by remember { mutableStateOf(false) } - var showSlotSelectionDialog by remember { mutableStateOf(false) } - var tempKernelUri by remember { mutableStateOf(null) } - val horizonKernelState = remember { HorizonKernelState() } - val flashState by horizonKernelState.state.collectAsState() - val summary = stringResource(R.string.horizon_kernel_summary) - val kernelVersion = getKernelVersion() - val isGKI = kernelVersion.isGKI() - val isAbDevice = isAbDevice() - - val onFlashComplete = { - showRebootDialog = true - } - - if (showRebootDialog) { - RebootDialog( - show = true, - onDismiss = { showRebootDialog = false }, - onConfirm = { - showRebootDialog = false - try { - val process = Runtime.getRuntime().exec("su") - process.outputStream.bufferedWriter().use { writer -> - writer.write("svc power reboot\n") - writer.write("exit\n") - } - } catch (_: Exception) { - Toast.makeText(context, R.string.failed_reboot, Toast.LENGTH_SHORT).show() - } - } - ) - } - - val onInstall = { - installMethod?.let { method -> - when (method) { - is InstallMethod.HorizonKernel -> { - method.uri?.let { uri -> - val worker = HorizonKernelWorker( - context = context, - state = horizonKernelState, - slot = method.slot - ) - worker.uri = uri - worker.setOnFlashCompleteListener(onFlashComplete) - worker.start() - } - } - else -> { - 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)) - } - } - } - Unit - } - - // 槽位选择 - SlotSelectionDialog( - show = showSlotSelectionDialog && isAbDevice, - onDismiss = { showSlotSelectionDialog = false }, - onSlotSelected = { slot -> - showSlotSelectionDialog = false - val horizonMethod = InstallMethod.HorizonKernel( - uri = tempKernelUri, - slot = slot, - summary = summary - ) - installMethod = horizonMethod - } - ) - - val currentKmi by produceState(initialValue = "") { - value = getCurrentKmi() - } - - val selectKmiDialog = rememberSelectKmiDialog { kmi -> - kmi?.let { - lkmSelection = LkmSelection.KmiString(it) - onInstall() - } - } - - val onClickNext = { - if (isGKI && lkmSelection == LkmSelection.KmiNone && currentKmi.isBlank()) { - 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) - } - } - } - - 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() }, - onLkmUpload = onLkmUpload, - scrollBehavior = scrollBehavior - ) - }, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ) - ) { innerPadding -> - Column( - modifier = Modifier - .padding(innerPadding) - .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()) - .padding(top = 12.dp) - ) { - SelectInstallMethod( - isGKI = isGKI, - isAbDevice = isAbDevice, - onSelected = { method -> - if (method is InstallMethod.HorizonKernel && method.uri != null) { - if (isAbDevice) { - tempKernelUri = method.uri - showSlotSelectionDialog = true - } else { - installMethod = method - } - } else { - installMethod = method - } - horizonKernelState.reset() - } - ) - - AnimatedVisibility( - visible = flashState.isFlashing && installMethod is InstallMethod.HorizonKernel, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - HorizonKernelFlashProgress(flashState) - } - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - (lkmSelection as? LkmSelection.LkmUri)?.let { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) - ) { - Text( - text = stringResource( - id = R.string.selected_lkm, - it.uri.lastPathSegment ?: "(file)" - ), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(16.dp) - ) - } - } - - (installMethod as? InstallMethod.HorizonKernel)?.let { method -> - if (method.slot != null) { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) - ) { - Text( - text = stringResource( - id = R.string.selected_slot, - if (method.slot == "a") stringResource(id = R.string.slot_a) - else stringResource(id = R.string.slot_b) - ), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(16.dp) - ) - } - } - } - - Button( - modifier = Modifier.fillMaxWidth(), - enabled = installMethod != null && !flashState.isFlashing, - onClick = onClickNext, - shape = MaterialTheme.shapes.medium, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), - disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) - ) - ) { - Text( - stringResource(id = R.string.install_next), - style = MaterialTheme.typography.bodyMedium - ) - } - } - } - } -} - -@Composable -private fun RebootDialog( - show: Boolean, - onDismiss: () -> Unit, - onConfirm: () -> Unit -) { - if (show) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(id = R.string.reboot_complete_title)) }, - text = { Text(stringResource(id = R.string.reboot_complete_msg)) }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(stringResource(id = R.string.yes)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(id = R.string.no)) - } - } - ) - } -} - -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 - } - - data class HorizonKernel( - val uri: Uri? = null, - val slot: String? = null, - @StringRes override val label: Int = R.string.horizon_kernel, - override val summary: String? = null - ) : InstallMethod() - - abstract val label: Int - open val summary: String? = null -} - -@Composable -private fun SelectInstallMethod( - isGKI: Boolean = false, - isAbDevice: Boolean = false, - onSelected: (InstallMethod) -> Unit = {} -) { - val rootAvailable = rootAvailable() - val isAbDevice = isAbDevice() - val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary) - 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) - } - radioOptions.add(InstallMethod.HorizonKernel(summary = horizonKernelSummary)) - } - - var selectedOption by remember { mutableStateOf(null) } - var currentSelectingMethod by remember { mutableStateOf(null) } - - val selectImageLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { - if (it.resultCode == Activity.RESULT_OK) { - it.data?.data?.let { uri -> - val option = when (currentSelectingMethod) { - is InstallMethod.SelectFile -> InstallMethod.SelectFile( - uri, - summary = selectFileTip - ) - - is InstallMethod.HorizonKernel -> InstallMethod.HorizonKernel( - uri, - summary = horizonKernelSummary - ) - - else -> null - } - option?.let { - selectedOption = it - onSelected(it) - } - } - } - } - - 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 -> - currentSelectingMethod = option - when (option) { - is InstallMethod.SelectFile, is InstallMethod.HorizonKernel -> { - selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { - type = "application/*" - putExtra( - Intent.EXTRA_MIME_TYPES, - arrayOf("application/octet-stream", "application/zip") - ) - }) - } - - is InstallMethod.DirectInstall -> { - selectedOption = option - onSelected(option) - } - - is InstallMethod.DirectInstallToInactiveSlot -> { - confirmDialog.showConfirm(dialogTitle, dialogContent) - } - } - } - - var LKMExpanded by remember { mutableStateOf(false) } - var GKIExpanded by remember { mutableStateOf(false) } - - Column( - modifier = Modifier.padding(horizontal = 16.dp) - ) { - // LKM 安装/修补 - if (isGKI) { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.large) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.large, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) - ) { - ListItem( - leadingContent = { - Icon( - Icons.Filled.AutoFixHigh, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - }, - headlineContent = { - Text( - stringResource(R.string.Lkm_install_methods), - style = MaterialTheme.typography.titleMedium - ) - }, - modifier = Modifier.clickable { - LKMExpanded = !LKMExpanded - } - ) - - AnimatedVisibility( - visible = LKMExpanded, - enter = fadeIn() + expandVertically(), - exit = shrinkVertically() + fadeOut() - ) { - Column( - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp - ) - ) { - radioOptions.take(3).forEach { option -> - val interactionSource = remember { MutableInteractionSource() } - Surface( - color = if (option.javaClass == selectedOption?.javaClass) - MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha) - else - MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .clip(MaterialTheme.shapes.medium) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .toggleable( - value = option.javaClass == selectedOption?.javaClass, - onValueChange = { onClick(option) }, - role = Role.RadioButton, - indication = LocalIndication.current, - interactionSource = interactionSource - ) - .padding(vertical = 8.dp, horizontal = 12.dp) - ) { - RadioButton( - selected = option.javaClass == selectedOption?.javaClass, - onClick = null, - interactionSource = interactionSource, - colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary, - unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) - Column( - modifier = Modifier - .padding(start = 10.dp) - .weight(1f) - ) { - Text( - text = stringResource(id = option.label), - style = MaterialTheme.typography.bodyLarge - ) - option.summary?.let { - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - } - } - } - } - } - - // anykernel3 刷写 - if (rootAvailable) { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.large) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.large, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) - ) { - ListItem( - leadingContent = { - Icon( - Icons.Filled.FileUpload, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - }, - headlineContent = { - Text( - stringResource(R.string.GKI_install_methods), - style = MaterialTheme.typography.titleMedium - ) - }, - modifier = Modifier.clickable { - GKIExpanded = !GKIExpanded - } - ) - - AnimatedVisibility( - visible = GKIExpanded, - enter = fadeIn() + expandVertically(), - exit = shrinkVertically() + fadeOut() - ) { - Column( - modifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp - ) - ) { - radioOptions.filterIsInstance().forEach { option -> - val interactionSource = remember { MutableInteractionSource() } - Surface( - color = if (option.javaClass == selectedOption?.javaClass) - MaterialTheme.colorScheme.secondaryContainer.copy(alpha = cardAlpha) - else - MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = cardAlpha), - shape = MaterialTheme.shapes.medium, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp) - .clip(MaterialTheme.shapes.medium) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .toggleable( - value = option.javaClass == selectedOption?.javaClass, - onValueChange = { onClick(option) }, - role = Role.RadioButton, - indication = LocalIndication.current, - interactionSource = interactionSource - ) - .padding(vertical = 8.dp, horizontal = 12.dp) - ) { - RadioButton( - selected = option.javaClass == selectedOption?.javaClass, - onClick = null, - interactionSource = interactionSource, - colors = RadioButtonDefaults.colors( - selectedColor = MaterialTheme.colorScheme.primary, - unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) - Column( - modifier = Modifier - .padding(start = 10.dp) - .weight(1f) - ) { - Text( - text = stringResource(id = option.label), - style = MaterialTheme.typography.bodyLarge - ) - option.summary?.let { - Text( - text = it, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - } - } - } - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun rememberSelectKmiDialog(onSelected: (String?) -> Unit): DialogHandle { - return rememberCustomDialog { dismiss -> - val supportedKmi by produceState(initialValue = emptyList()) { - value = getSupportedKmis() - } - val options = supportedKmi.map { value -> - ListOption( - titleText = value - ) - } - - var selection by remember { mutableStateOf(null) } - val backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest - - MaterialTheme( - colorScheme = MaterialTheme.colorScheme.copy( - surface = backgroundColor - ) - ) { - 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 = {}, - onLkmUpload: () -> Unit = {}, - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val cardColor = MaterialTheme.colorScheme.surfaceVariant - val cardAlpha = cardAlpha - - TopAppBar( - title = { - Text( - stringResource(R.string.install), - style = MaterialTheme.typography.titleLarge - ) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - navigationIcon = { - IconButton(onClick = onBack) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back) - ) - } - }, - windowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), - scrollBehavior = scrollBehavior - ) -} - -@Preview -@Composable -fun SelectInstallPreview() { - InstallScreen(EmptyDestinationsNavigator) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt deleted file mode 100644 index 7e99f340..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Kpm.kt +++ /dev/null @@ -1,783 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.content.Context -import android.content.Intent -import android.util.Log -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -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.navigation.DestinationsNavigator -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import com.sukisu.ultra.ui.component.* -import com.sukisu.ultra.ui.theme.* -import com.sukisu.ultra.ui.viewmodel.KpmViewModel -import com.sukisu.ultra.ui.util.* -import java.io.File -import androidx.core.content.edit -import com.sukisu.ultra.R -import java.io.BufferedReader -import java.io.FileInputStream -import java.io.InputStreamReader -import java.net.* -import android.app.Activity -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation - -/** - * KPM 管理界面 - * 以下内核模块功能由KernelPatch开发,经过修改后加入SukiSU Ultra的内核模块功能 - * 开发者:ShirkNeko, Liaokong - */ -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun KpmScreen( - navigator: DestinationsNavigator, - viewModel: KpmViewModel = viewModel() -) { - val context = LocalContext.current - val scope = rememberCoroutineScope() - val snackBarHost = remember { SnackbarHostState() } - val confirmDialog = rememberConfirmDialog() - - val moduleConfirmContentMap = viewModel.moduleList.associate { module -> - val moduleFileName = module.id - module.id to stringResource(R.string.confirm_uninstall_content, moduleFileName) - } - - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - - val kpmInstallSuccess = stringResource(R.string.kpm_install_success) - val kpmInstallFailed = stringResource(R.string.kpm_install_failed) - val cancel = stringResource(R.string.cancel) - val uninstall = stringResource(R.string.uninstall) - val failedToCheckModuleFile = stringResource(R.string.snackbar_failed_to_check_module_file) - val kpmUninstallSuccess = stringResource(R.string.kpm_uninstall_success) - val kpmUninstallFailed = stringResource(R.string.kpm_uninstall_failed) - val kpmInstallMode = stringResource(R.string.kpm_install_mode) - val kpmInstallModeLoad = stringResource(R.string.kpm_install_mode_load) - val kpmInstallModeEmbed = stringResource(R.string.kpm_install_mode_embed) - val invalidFileTypeMessage = stringResource(R.string.invalid_file_type) - val confirmTitle = stringResource(R.string.confirm_uninstall_title_with_filename) - - var tempFileForInstall by remember { mutableStateOf(null) } - val installModeDialog = rememberCustomDialog { dismiss -> - var moduleName by remember { mutableStateOf(null) } - - LaunchedEffect(tempFileForInstall) { - tempFileForInstall?.let { tempFile -> - try { - val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='") - val process = Runtime.getRuntime().exec(command) - val inputStream = process.inputStream - val reader = BufferedReader(InputStreamReader(inputStream)) - var line: String? - while (reader.readLine().also { line = it } != null) { - if (line!!.startsWith("name=")) { - moduleName = line.substringAfter("name=").trim() - break - } - } - process.waitFor() - } catch (e: Exception) { - Log.e("KsuCli", "Failed to get module name: ${e.message}", e) - } - } - } - - AlertDialog( - onDismissRequest = { - dismiss() - tempFileForInstall?.delete() - tempFileForInstall = null - }, - title = { - Text( - text = kpmInstallMode, - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - }, - text = { - Column { - moduleName?.let { - Text( - text = stringResource(R.string.kpm_install_mode_description, it), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Spacer(modifier = Modifier.height(16.dp)) - Column( - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = { - scope.launch { - dismiss() - tempFileForInstall?.let { tempFile -> - handleModuleInstall( - tempFile = tempFile, - isEmbed = false, - viewModel = viewModel, - snackBarHost = snackBarHost, - kpmInstallSuccess = kpmInstallSuccess, - kpmInstallFailed = kpmInstallFailed - ) - } - tempFileForInstall = null - } - }, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon( - imageVector = Icons.Filled.Download, - contentDescription = null, - modifier = Modifier.size(18.dp).padding(end = 4.dp) - ) - Text(kpmInstallModeLoad) - } - - Button( - onClick = { - scope.launch { - dismiss() - tempFileForInstall?.let { tempFile -> - handleModuleInstall( - tempFile = tempFile, - isEmbed = true, - viewModel = viewModel, - snackBarHost = snackBarHost, - kpmInstallSuccess = kpmInstallSuccess, - kpmInstallFailed = kpmInstallFailed - ) - } - tempFileForInstall = null - } - }, - modifier = Modifier.fillMaxWidth(), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondary - ) - ) { - Icon( - imageVector = Icons.Filled.Inventory, - contentDescription = null, - modifier = Modifier.size(18.dp).padding(end = 4.dp) - ) - Text(kpmInstallModeEmbed) - } - } - } - }, - confirmButton = { - }, - dismissButton = { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.height(16.dp)) - TextButton( - onClick = { - dismiss() - tempFileForInstall?.delete() - tempFileForInstall = null - } - ) { - Text(cancel) - } - } - }, - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = MaterialTheme.shapes.extraLarge - ) - } - - val selectPatchLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode != Activity.RESULT_OK) return@rememberLauncherForActivityResult - - val uri = result.data?.data ?: return@rememberLauncherForActivityResult - - scope.launch { - val fileName = uri.lastPathSegment ?: "unknown.kpm" - val encodedFileName = URLEncoder.encode(fileName, "UTF-8") - val tempFile = File(context.cacheDir, encodedFileName) - - context.contentResolver.openInputStream(uri)?.use { input -> - tempFile.outputStream().use { output -> - input.copyTo(output) - } - } - - val mimeType = context.contentResolver.getType(uri) - val isCorrectMimeType = mimeType == null || mimeType.contains("application/octet-stream") - - if (!isCorrectMimeType) { - var shouldShowSnackbar = true - try { - val matchCount = checkStringsCommand(tempFile) - val isElf = isElfFile(tempFile) - - if (matchCount >= 1 || isElf) { - shouldShowSnackbar = false - } - } catch (e: Exception) { - Log.e("KsuCli", "Failed to execute checks: ${e.message}", e) - } - if (shouldShowSnackbar) { - snackBarHost.showSnackbar( - message = invalidFileTypeMessage, - duration = SnackbarDuration.Short - ) - } - tempFile.delete() - return@launch - } - tempFileForInstall = tempFile - installModeDialog.show() - } - } - - LaunchedEffect(Unit) { - while(true) { - viewModel.fetchModuleList() - delay(5000) - } - } - - val sharedPreferences = context.getSharedPreferences("app_preferences", Context.MODE_PRIVATE) - var isNoticeClosed by remember { mutableStateOf(sharedPreferences.getBoolean("is_notice_closed", false)) } - - Scaffold( - topBar = { - SearchAppBar( - title = { Text(stringResource(R.string.kpm_title)) }, - searchText = viewModel.search, - onSearchTextChange = { viewModel.search = it }, - onClearClick = { viewModel.search = "" }, - scrollBehavior = scrollBehavior, - dropdownContent = { - IconButton( - onClick = { viewModel.fetchModuleList() } - ) { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = stringResource(R.string.refresh), - ) - } - } - ) - }, - floatingActionButton = { - ExtendedFloatingActionButton( - onClick = { - selectPatchLauncher.launch( - Intent(Intent.ACTION_GET_CONTENT).apply { - type = "application/octet-stream" - } - ) - }, - icon = { - Icon( - imageVector = Icons.Filled.Add, - contentDescription = stringResource(R.string.kpm_install), - ) - }, - text = { - Text( - text = stringResource(R.string.kpm_install), - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - }, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - expanded = true, - ) - }, - snackbarHost = { SnackbarHost(snackBarHost) } - ) { padding -> - Column(modifier = Modifier.padding(padding)) { - if (!isNoticeClosed) { - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer - ), - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(MaterialTheme.shapes.medium) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Filled.Info, - contentDescription = null, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - - Text( - text = stringResource(R.string.kernel_module_notice), - modifier = Modifier.weight(1f), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSecondaryContainer - ) - - IconButton( - onClick = { - isNoticeClosed = true - sharedPreferences.edit { putBoolean("is_notice_closed", true) } - }, - modifier = Modifier.size(24.dp), - colors = IconButtonDefaults.iconButtonColors( - contentColor = MaterialTheme.colorScheme.onSecondaryContainer - ) - ) { - Icon( - imageVector = Icons.Filled.Close, - contentDescription = stringResource(R.string.close_notice) - ) - } - } - } - } - - if (viewModel.moduleList.isEmpty()) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Filled.Code, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), - modifier = Modifier - .size(96.dp) - .padding(bottom = 16.dp) - ) - Text( - stringResource(R.string.kpm_empty), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - items(viewModel.moduleList) { module -> - KpmModuleItem( - module = module, - onUninstall = { - scope.launch { - val confirmContent = moduleConfirmContentMap[module.id] ?: "" - handleModuleUninstall( - module = module, - viewModel = viewModel, - snackBarHost = snackBarHost, - kpmUninstallSuccess = kpmUninstallSuccess, - kpmUninstallFailed = kpmUninstallFailed, - failedToCheckModuleFile = failedToCheckModuleFile, - uninstall = uninstall, - cancel = cancel, - confirmDialog = confirmDialog, - confirmTitle = confirmTitle, - confirmContent = confirmContent - ) - } - }, - onControl = { - viewModel.loadModuleDetail(module.id) - } - ) - } - } - } - } - } -} - -private suspend fun handleModuleInstall( - tempFile: File, - isEmbed: Boolean, - viewModel: KpmViewModel, - snackBarHost: SnackbarHostState, - kpmInstallSuccess: String, - kpmInstallFailed: String -) { - var moduleId: String? = null - try { - val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep 'name='") - val process = Runtime.getRuntime().exec(command) - val inputStream = process.inputStream - val reader = BufferedReader(InputStreamReader(inputStream)) - var line: String? - while (reader.readLine().also { line = it } != null) { - if (line!!.startsWith("name=")) { - moduleId = line.substringAfter("name=").trim() - break - } - } - process.waitFor() - } catch (e: Exception) { - Log.e("KsuCli", "Failed to get module ID from strings command: ${e.message}", e) - } - - if (moduleId == null || moduleId.isEmpty()) { - Log.e("KsuCli", "Failed to extract module ID from file: ${tempFile.name}") - snackBarHost.showSnackbar( - message = kpmInstallFailed, - duration = SnackbarDuration.Short - ) - tempFile.delete() - return - } - - val targetPath = "/data/adb/kpm/$moduleId.kpm" - - try { - if (isEmbed) { - Runtime.getRuntime().exec(arrayOf("su", "-c", "mkdir -p /data/adb/kpm")).waitFor() - Runtime.getRuntime().exec(arrayOf("su", "-c", "cp ${tempFile.absolutePath} $targetPath")).waitFor() - } - - val loadResult = loadKpmModule(tempFile.absolutePath) - if (loadResult.startsWith("Error")) { - Log.e("KsuCli", "Failed to load KPM module: $loadResult") - snackBarHost.showSnackbar( - message = kpmInstallFailed, - duration = SnackbarDuration.Short - ) - } else { - viewModel.fetchModuleList() - snackBarHost.showSnackbar( - message = kpmInstallSuccess, - duration = SnackbarDuration.Short - ) - } - } catch (e: Exception) { - Log.e("KsuCli", "Failed to load KPM module: ${e.message}", e) - snackBarHost.showSnackbar( - message = kpmInstallFailed, - duration = SnackbarDuration.Short - ) - } - tempFile.delete() -} - -private suspend fun handleModuleUninstall( - module: KpmViewModel.ModuleInfo, - viewModel: KpmViewModel, - snackBarHost: SnackbarHostState, - kpmUninstallSuccess: String, - kpmUninstallFailed: String, - failedToCheckModuleFile: String, - uninstall: String, - cancel: String, - confirmTitle : String, - confirmContent : String, - confirmDialog: ConfirmDialogHandle -) { - val moduleFileName = "${module.id}.kpm" - val moduleFilePath = "/data/adb/kpm/$moduleFileName" - - val fileExists = try { - val result = Runtime.getRuntime().exec(arrayOf("su", "-c", "ls /data/adb/kpm/$moduleFileName")).waitFor() == 0 - result - } catch (e: Exception) { - Log.e("KsuCli", "Failed to check module file existence: ${e.message}", e) - snackBarHost.showSnackbar( - message = failedToCheckModuleFile, - duration = SnackbarDuration.Short - ) - false - } - val confirmResult = confirmDialog.awaitConfirm( - title = confirmTitle, - content = confirmContent, - confirm = uninstall, - dismiss = cancel - ) - - if (confirmResult == ConfirmResult.Confirmed) { - try { - val unloadResult = unloadKpmModule(module.id) - if (unloadResult.startsWith("Error")) { - Log.e("KsuCli", "Failed to unload KPM module: $unloadResult") - snackBarHost.showSnackbar( - message = kpmUninstallFailed, - duration = SnackbarDuration.Short - ) - return - } - - if (fileExists) { - Runtime.getRuntime().exec(arrayOf("su", "-c", "rm $moduleFilePath")).waitFor() - } - - viewModel.fetchModuleList() - snackBarHost.showSnackbar( - message = kpmUninstallSuccess, - duration = SnackbarDuration.Short - ) - } catch (e: Exception) { - Log.e("KsuCli", "Failed to unload KPM module: ${e.message}", e) - snackBarHost.showSnackbar( - message = kpmUninstallFailed, - duration = SnackbarDuration.Short - ) - } - } -} - -@Composable -private fun KpmModuleItem( - module: KpmViewModel.ModuleInfo, - onUninstall: () -> Unit, - onControl: () -> Unit -) { - val viewModel: KpmViewModel = viewModel() - val scope = rememberCoroutineScope() - val snackBarHost = remember { SnackbarHostState() } - val successMessage = stringResource(R.string.kpm_control_success) - val failureMessage = stringResource(R.string.kpm_control_failed) - - if (viewModel.showInputDialog && viewModel.selectedModuleId == module.id) { - AlertDialog( - onDismissRequest = { viewModel.hideInputDialog() }, - title = { - Text( - text = stringResource(R.string.kpm_control), - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - }, - text = { - OutlinedTextField( - value = viewModel.inputArgs, - onValueChange = { viewModel.updateInputArgs(it) }, - label = { - Text( - text = stringResource(R.string.kpm_args), - color = MaterialTheme.colorScheme.primary - ) - }, - placeholder = { - Text( - text = module.args, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) - ) - }, - modifier = Modifier.fillMaxWidth(), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - unfocusedBorderColor = MaterialTheme.colorScheme.outline - ) - ) - }, - confirmButton = { - TextButton( - onClick = { - scope.launch { - val result = viewModel.executeControl() - val message = when (result) { - 0 -> successMessage - else -> failureMessage - } - snackBarHost.showSnackbar(message) - onControl() - } - } - ) { - Text( - text = stringResource(R.string.confirm), - color = MaterialTheme.colorScheme.primary - ) - } - }, - dismissButton = { - TextButton(onClick = { viewModel.hideInputDialog() }) { - Text( - text = stringResource(R.string.cancel), - color = MaterialTheme.colorScheme.primary - ) - } - }, - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = MaterialTheme.shapes.extraLarge - ) - } - - Card( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.large) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.large, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) - ) { - Column( - modifier = Modifier.padding(20.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = module.name, - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.onSurface - ) - - Spacer(modifier = Modifier.height(4.dp)) - - Text( - text = "${stringResource(R.string.kpm_version)}: ${module.version}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - text = "${stringResource(R.string.kpm_author)}: ${module.author}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - text = "${stringResource(R.string.kpm_args)}: ${module.args}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = module.description, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Button( - onClick = { viewModel.showInputDialog(module.id) }, - enabled = module.hasAction, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary, - disabledContainerColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) { - Icon( - imageVector = Icons.Filled.Settings, - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.kpm_control)) - } - - Button( - onClick = onUninstall, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) - ) { - Icon( - imageVector = Icons.Filled.Delete, - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.kpm_uninstall)) - } - } - } - } -} - -private fun checkStringsCommand(tempFile: File): Int { - val command = arrayOf("su", "-c", "strings ${tempFile.absolutePath} | grep -E 'name=|version=|license=|author='") - val process = Runtime.getRuntime().exec(command) - val inputStream = process.inputStream - val reader = BufferedReader(InputStreamReader(inputStream)) - var line: String? - var matchCount = 0 - val keywords = listOf("name=", "version=", "license=", "author=") - var nameExists = false - - while (reader.readLine().also { line = it } != null) { - if (!nameExists && line!!.startsWith("name=")) { - nameExists = true - matchCount++ - } else if (nameExists) { - for (keyword in keywords) { - if (line!!.startsWith(keyword)) { - matchCount++ - break - } - } - } - } - process.waitFor() - - return if (nameExists) matchCount else 0 -} - -private fun isElfFile(tempFile: File): Boolean { - val elfMagic = byteArrayOf(0x7F, 'E'.code.toByte(), 'L'.code.toByte(), 'F'.code.toByte()) - val fileBytes = ByteArray(4) - FileInputStream(tempFile).use { input -> - input.read(fileBytes) - } - return fileBytes.contentEquals(elfMagic) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt deleted file mode 100644 index 409577cb..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt +++ /dev/null @@ -1,937 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.app.Activity.* -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.* -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.selection.toggleable -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.outlined.* -import androidx.compose.material.icons.filled.* -import androidx.compose.material.icons.outlined.* -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.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.* -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 -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import 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 com.sukisu.ultra.Natives -import com.sukisu.ultra.ui.component.ConfirmResult -import com.sukisu.ultra.ui.component.SearchAppBar -import com.sukisu.ultra.ui.component.rememberConfirmDialog -import com.sukisu.ultra.ui.component.rememberLoadingDialog -import com.sukisu.ultra.ui.util.DownloadListener -import com.sukisu.ultra.ui.util.* -import com.sukisu.ultra.ui.util.download -import com.sukisu.ultra.ui.util.hasMagisk -import com.sukisu.ultra.ui.util.reboot -import com.sukisu.ultra.ui.util.restoreModule -import com.sukisu.ultra.ui.util.toggleModule -import com.sukisu.ultra.ui.util.uninstallModule -import com.sukisu.ultra.ui.webui.WebUIActivity -import okhttp3.OkHttpClient -import com.sukisu.ultra.ui.util.ModuleModify -import com.sukisu.ultra.ui.theme.getCardColors -import com.sukisu.ultra.ui.viewmodel.ModuleViewModel -import java.io.BufferedReader -import java.io.InputStreamReader -import java.util.zip.ZipInputStream -import androidx.core.content.edit -import androidx.core.net.toUri -import com.sukisu.ultra.ui.theme.ThemeConfig -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation - - -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun ModuleScreen(navigator: DestinationsNavigator) { - val viewModel = viewModel() - val context = LocalContext.current - val snackBarHost = LocalSnackbarHost.current - val scope = rememberCoroutineScope() - val confirmDialog = rememberConfirmDialog() - - val selectZipLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { - if (it.resultCode != RESULT_OK) { - return@rememberLauncherForActivityResult - } - val data = it.data ?: return@rememberLauncherForActivityResult - - scope.launch { - val clipData = data.clipData - if (clipData != null) { - // 处理多选结果 - val selectedModules = mutableSetOf() - val selectedModuleNames = mutableMapOf() - - suspend fun processUri(uri: Uri) { - val moduleName = withContext(Dispatchers.IO) { - try { - val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri)) - var entry = zipInputStream.nextEntry - var name = context.getString(R.string.unknown_module) - - while (entry != null) { - if (entry.name == "module.prop") { - val reader = BufferedReader(InputStreamReader(zipInputStream)) - var line: String? - while (reader.readLine().also { line = it } != null) { - if (line?.startsWith("name=") == true) { - name = line.substringAfter("=") - break - } - } - break - } - entry = zipInputStream.nextEntry - } - name - } catch (e: Exception) { - context.getString(R.string.unknown_module) - } - } - selectedModules.add(uri) - selectedModuleNames[uri] = moduleName - } - - for (i in 0 until clipData.itemCount) { - val uri = clipData.getItemAt(i).uri - processUri(uri) - } - - // 显示确认对话框 - val modulesList = selectedModuleNames.values.joinToString("\n• ", "• ") - val confirmResult = confirmDialog.awaitConfirm( - title = context.getString(R.string.module_install), - content = context.getString(R.string.module_install_multiple_confirm_with_names, selectedModules.size, modulesList), - confirm = context.getString(R.string.install), - dismiss = context.getString(R.string.cancel) - ) - - if (confirmResult == ConfirmResult.Confirmed) { - // 批量安装模块 - selectedModules.forEach { uri -> - navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri))) - } - viewModel.markNeedRefresh() - } - } else { - // 单个文件安装逻辑 - val uri = data.data ?: return@launch - val moduleName = withContext(Dispatchers.IO) { - try { - val zipInputStream = ZipInputStream(context.contentResolver.openInputStream(uri)) - var entry = zipInputStream.nextEntry - var name = context.getString(R.string.unknown_module) - - while (entry != null) { - if (entry.name == "module.prop") { - val reader = BufferedReader(InputStreamReader(zipInputStream)) - var line: String? - while (reader.readLine().also { line = it } != null) { - if (line?.startsWith("name=") == true) { - name = line.substringAfter("=") - break - } - } - break - } - entry = zipInputStream.nextEntry - } - name - } catch (e: Exception) { - context.getString(R.string.unknown_module) - } - } - - val confirmResult = confirmDialog.awaitConfirm( - title = context.getString(R.string.module_install), - content = context.getString(R.string.module_install_confirm, moduleName), - confirm = context.getString(R.string.install), - dismiss = context.getString(R.string.cancel) - ) - - if (confirmResult == ConfirmResult.Confirmed) { - navigator.navigate(FlashScreenDestination(FlashIt.FlashModule(uri))) - viewModel.markNeedRefresh() - } - } - } - } - - val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost) - val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost) - - val prefs = context.getSharedPreferences("settings", 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() - } - } - - val isSafeMode = Natives.isSafeMode - val hasMagisk = hasMagisk() - - val hideInstallButton = isSafeMode || hasMagisk - - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - - val webUILauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { viewModel.fetchModuleList() } - - 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) } - - IconButton( - onClick = { showDropdown = true }, - ) { - Icon( - imageVector = Icons.Filled.MoreVert, - contentDescription = stringResource(id = R.string.settings), - ) - - DropdownMenu( - expanded = showDropdown, - onDismissRequest = { showDropdown = false } - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.module_sort_action_first)) }, - trailingIcon = { - Checkbox( - checked = viewModel.sortActionFirst, - onCheckedChange = null, - colors = CheckboxDefaults.colors( - checkedColor = MaterialTheme.colorScheme.primary, - uncheckedColor = MaterialTheme.colorScheme.outline - ) - ) - }, - onClick = { - viewModel.sortActionFirst = !viewModel.sortActionFirst - prefs.edit { - putBoolean( - "module_sort_action_first", - viewModel.sortActionFirst - ) - } - scope.launch { - viewModel.fetchModuleList() - } - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.module_sort_enabled_first)) }, - trailingIcon = { - Checkbox( - checked = viewModel.sortEnabledFirst, - onCheckedChange = null, - colors = CheckboxDefaults.colors( - checkedColor = MaterialTheme.colorScheme.primary, - uncheckedColor = MaterialTheme.colorScheme.outline - ) - ) - }, - onClick = { - viewModel.sortEnabledFirst = !viewModel.sortEnabledFirst - prefs.edit { - putBoolean("module_sort_enabled_first", viewModel.sortEnabledFirst) - } - scope.launch { - viewModel.fetchModuleList() - } - } - ) - HorizontalDivider(thickness = Dp.Hairline, modifier = Modifier.padding(vertical = 4.dp)) - DropdownMenuItem( - text = { Text(stringResource(R.string.backup_modules)) }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.Download, - contentDescription = stringResource(R.string.backup), - ) - }, - onClick = { - showDropdown = false - backupLauncher.launch(ModuleModify.createBackupIntent()) - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.restore_modules)) }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.Refresh, - contentDescription = stringResource(R.string.restore), - ) - }, - onClick = { - showDropdown = false - restoreLauncher.launch(ModuleModify.createRestoreIntent()) - } - ) - } - } - }, - scrollBehavior = scrollBehavior, - ) - }, - floatingActionButton = { - if (!hideInstallButton) { - val moduleInstall = stringResource(id = R.string.module_install) - ExtendedFloatingActionButton( - onClick = { - selectZipLauncher.launch( - Intent(Intent.ACTION_GET_CONTENT).apply { - type = "application/zip" - putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - } - ) - }, - icon = { - Icon( - imageVector = Icons.Filled.Add, - contentDescription = moduleInstall, - ) - }, - text = { - Text( - text = moduleInstall, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - }, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer, - expanded = true, - ) - } - }, - contentWindowInsets = WindowInsets.safeDrawing.only( - WindowInsetsSides.Top + WindowInsetsSides.Horizontal - ), - snackbarHost = { SnackbarHost(hostState = snackBarHost) } - ) { innerPadding -> - when { - hasMagisk -> { - Box( - modifier = Modifier - .fillMaxSize() - .padding(24.dp), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Outlined.Warning, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - modifier = Modifier - .size(64.dp) - .padding(bottom = 16.dp) - ) - Text( - stringResource(R.string.module_magisk_conflict), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - else -> { - ModuleList( - navigator = 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("kernelsu://webui/$id".toUri()) - .putExtra("id", id) - .putExtra("name", name) - ) - } - }, - context = context, - snackBarHost = snackBarHost - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ModuleList( - 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(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 loadingDialog = rememberLoadingDialog() - val confirmDialog = rememberConfirmDialog() - - suspend fun onModuleUpdate( - module: ModuleViewModel.ModuleInfo, - changelogUrl: String, - downloadUrl: String, - fileName: String - ) { - val changelogResult = loadingDialog.withLoading { - withContext(Dispatchers.IO) { - runCatching { - OkHttpClient().newCall( - okhttp3.Request.Builder().url(changelogUrl).build() - ).execute().body!!.string() - } - } - } - - val showToast: suspend (String) -> Unit = { msg -> - withContext(Dispatchers.Main) { - Toast.makeText( - context, - msg, - 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( - context, - downloadUrl, - fileName, - downloading, - onDownloaded = onInstallModule, - onDownloading = { - launch(Dispatchers.Main) { - Toast.makeText(context, downloading, Toast.LENGTH_SHORT).show() - } - } - ) - } - } - - suspend fun onModuleUninstallClicked(module: ModuleViewModel.ModuleInfo) { - val isUninstall = !module.remove - if (isUninstall) { - val confirmResult = confirmDialog.awaitConfirm( - moduleStr, - content = moduleUninstallConfirm.format(module.name), - confirm = uninstall, - dismiss = cancel - ) - if (confirmResult != ConfirmResult.Confirmed) { - return - } - } - - val success = loadingDialog.withLoading { - withContext(Dispatchers.IO) { - if (isUninstall) { - uninstallModule(module.dirId) - } else { - restoreModule(module.dirId) - } - } - } - - if (success) { - viewModel.fetchModuleList() - } - if (!isUninstall) return - val message = if (success) { - successUninstall.format(module.name) - } else { - failedUninstall.format(module.name) - } - val actionLabel = if (success) { - reboot - } else { - null - } - val result = snackBarHost.showSnackbar( - message = message, - actionLabel = actionLabel, - duration = SnackbarDuration.Long - ) - if (result == SnackbarResult.ActionPerformed) { - reboot() - } - } - PullToRefreshBox( - modifier = boxModifier, - onRefresh = { - viewModel.fetchModuleList() - }, - isRefreshing = viewModel.isRefreshing - ) { - LazyColumn( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = remember { - PaddingValues( - start = 16.dp, - top = 16.dp, - end = 16.dp, - bottom = 16.dp + 56.dp + 16.dp + 48.dp + 6.dp /* Scaffold Fab Spacing + Fab container height + SnackBar height */ - ) - }, - ) { - when { - viewModel.moduleList.isEmpty() -> { - item { - Box( - modifier = Modifier.fillParentMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Outlined.Extension, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), - modifier = Modifier - .size(96.dp) - .padding(bottom = 16.dp) - ) - Text( - text = stringResource(R.string.module_empty), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - - else -> { - items(viewModel.moduleList) { module -> - val scope = rememberCoroutineScope() - val updatedModule by produceState(initialValue = Triple("", "", "")) { - scope.launch(Dispatchers.IO) { - value = viewModel.checkUpdate(module) - } - } - - ModuleItem( - navigator = navigator, - module = module, - updateUrl = updatedModule.first, - onUninstallClicked = { - scope.launch { onModuleUninstallClicked(module) } - }, - onCheckChanged = { - scope.launch { - val success = loadingDialog.withLoading { - withContext(Dispatchers.IO) { - toggleModule(module.dirId, !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)) - } - } - }, - onUpdate = { - scope.launch { - onModuleUpdate( - module, - updatedModule.third, - updatedModule.first, - "${module.name}-${updatedModule.second}.zip" - ) - } - }, - onClick = { - onClickModule(it.dirId, it.name, it.hasWebUi) - } - ) - - // fix last item shadow incomplete in LazyColumn - Spacer(Modifier.height(1.dp)) - } - } - } - } - - DownloadListener(context, onInstallModule) - - } -} - -@Composable -fun ModuleItem( - navigator: DestinationsNavigator, - module: ModuleViewModel.ModuleInfo, - updateUrl: String, - onUninstallClicked: (ModuleViewModel.ModuleInfo) -> Unit, - onCheckChanged: (Boolean) -> Unit, - onUpdate: (ModuleViewModel.ModuleInfo) -> Unit, - onClick: (ModuleViewModel.ModuleInfo) -> Unit -) { - ElevatedCard( - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation), - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.large) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.large, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) - ) { - val textDecoration = if (!module.remove) null else TextDecoration.LineThrough - 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, - verticalAlignment = Alignment.CenterVertically - ) { - val moduleVersion = stringResource(id = R.string.module_version) - val moduleAuthor = stringResource(id = R.string.module_author) - - Column( - modifier = Modifier.fillMaxWidth(0.8f) - ) { - Text( - text = module.name, - fontSize = MaterialTheme.typography.titleMedium.fontSize, - fontWeight = FontWeight.SemiBold, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontFamily = MaterialTheme.typography.titleMedium.fontFamily, - textDecoration = textDecoration, - color = MaterialTheme.colorScheme.onSurface - ) - - Text( - text = "$moduleVersion: ${module.version}", - fontSize = MaterialTheme.typography.bodySmall.fontSize, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - textDecoration = textDecoration, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Text( - text = "$moduleAuthor: ${module.author}", - fontSize = MaterialTheme.typography.bodySmall.fontSize, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - textDecoration = textDecoration, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Spacer(modifier = Modifier.weight(1f)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - ) { - Switch( - enabled = !module.update, - checked = module.enabled, - onCheckedChange = onCheckChanged, - interactionSource = if (!module.hasWebUi) interactionSource else null, - colors = SwitchDefaults.colors( - checkedThumbColor = MaterialTheme.colorScheme.onPrimary, - checkedTrackColor = MaterialTheme.colorScheme.primary, - checkedIconColor = MaterialTheme.colorScheme.primary, - uncheckedThumbColor = MaterialTheme.colorScheme.outline, - uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant, - uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - - Text( - text = module.description, - fontSize = MaterialTheme.typography.bodySmall.fontSize, - fontFamily = MaterialTheme.typography.bodySmall.fontFamily, - lineHeight = MaterialTheme.typography.bodySmall.lineHeight, - fontWeight = MaterialTheme.typography.bodySmall.fontWeight, - overflow = TextOverflow.Ellipsis, - maxLines = 4, - textDecoration = textDecoration, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(16.dp)) - - HorizontalDivider(thickness = Dp.Hairline) - - Spacer(modifier = Modifier.height(8.dp)) - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - if (module.hasActionScript) { - FilledTonalButton( - modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp), - enabled = !module.remove && module.enabled, - onClick = { - navigator.navigate(ExecuteModuleActionScreenDestination(module.dirId)) - viewModel.markNeedRefresh() - }, - contentPadding = ButtonDefaults.TextButtonContentPadding, - colors = ButtonDefaults.filledTonalButtonColors() - ) { - 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 - //) - //} - } - } - - if (module.hasWebUi) { - FilledTonalButton( - modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp), - enabled = !module.remove && module.enabled, - onClick = { onClick(module) }, - interactionSource = interactionSource, - contentPadding = ButtonDefaults.TextButtonContentPadding, - colors = ButtonDefaults.filledTonalButtonColors() - - ) { - 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.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp), - enabled = !module.remove, - onClick = { onUpdate(module) }, - shape = ButtonDefaults.textShape, - contentPadding = ButtonDefaults.TextButtonContentPadding, - ) { - Icon( - modifier = Modifier.size(20.dp), - imageVector = Icons.Outlined.Download, - contentDescription = null - ) - //if (!module.hasActionScript || !module.hasWebUi) { - //Text( - // modifier = Modifier.padding(start = 7.dp), - // fontFamily = MaterialTheme.typography.labelMedium.fontFamily, - // fontSize = MaterialTheme.typography.labelMedium.fontSize, - // text = stringResource(R.string.module_update) - //) - //} - } - } - - FilledTonalButton( - modifier = Modifier.defaultMinSize(minWidth = 52.dp, minHeight = 32.dp), - onClick = { onUninstallClicked(module) }, - contentPadding = ButtonDefaults.TextButtonContentPadding, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = if (!module.remove) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.errorContainer) - ) { - if (!module.remove) { - Icon( - modifier = Modifier.size(20.dp), - imageVector = Icons.Outlined.Delete, - contentDescription = null, - ) - } else { - Icon( - modifier = Modifier.size(20.dp).rotate(180f), - imageVector = Icons.Outlined.Refresh, - contentDescription = null - ) - } - //if (!module.hasActionScript && !module.hasWebUi && updateUrl.isEmpty()) { - //Text( - // modifier = Modifier.padding(start = 7.dp), - // fontFamily = MaterialTheme.typography.labelMedium.fontFamily, - // fontSize = MaterialTheme.typography.labelMedium.fontSize, - // text = stringResource(if (!module.remove) R.string.uninstall else R.string.restore), - // color = if (!module.remove) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onSecondaryContainer - //) - //} - } - } - } - } -} - -@Preview -@Composable -fun ModuleItemPreview() { - val module = ModuleViewModel.ModuleInfo( - id = "id", - name = "name", - version = "version", - versionCode = 1, - author = "author", - description = "I am a test module and i do nothing but show a very long description", - enabled = true, - update = true, - remove = false, - updateJson = "", - hasWebUi = false, - hasActionScript = false, - dirId = "dirId" - ) - ModuleItem(EmptyDestinationsNavigator, module, "", {}, {}, {}, {}) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt deleted file mode 100644 index 787fc55d..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/MoreSettings.kt +++ /dev/null @@ -1,925 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.content.Context -import android.net.Uri -import android.os.Build -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.NavigateNext -import androidx.compose.material.icons.filled.Brush -import androidx.compose.material.icons.filled.ColorLens -import androidx.compose.material.icons.filled.DarkMode -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material.icons.filled.Opacity -import androidx.compose.material.icons.filled.Palette -import androidx.compose.material.icons.filled.Security -import androidx.compose.material.icons.filled.VisibilityOff -import androidx.compose.material.icons.filled.Wallpaper -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Slider -import androidx.compose.material3.SliderDefaults -import androidx.compose.material3.Surface -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.core.content.edit -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ksuApp -import com.sukisu.ultra.ui.component.ImageEditorDialog -import com.sukisu.ultra.ui.component.SwitchItem -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.theme.ThemeColors -import com.sukisu.ultra.ui.theme.ThemeConfig -import com.sukisu.ultra.ui.theme.saveAndApplyCustomBackground -import com.sukisu.ultra.ui.theme.saveCustomBackground -import com.sukisu.ultra.ui.theme.saveDynamicColorState -import com.sukisu.ultra.ui.theme.saveThemeColors -import com.sukisu.ultra.ui.theme.saveThemeMode -import com.sukisu.ultra.ui.util.getSuSFS -import com.sukisu.ultra.ui.util.getSuSFSFeatures -import com.sukisu.ultra.ui.util.susfsSUS_SU_0 -import com.sukisu.ultra.ui.util.susfsSUS_SU_2 -import com.sukisu.ultra.ui.util.susfsSUS_SU_Mode -import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlin.math.roundToInt - -fun saveCardConfig(context: Context) { - CardConfig.save(context) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun MoreSettingsScreen(navigator: DestinationsNavigator) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } - val systemIsDark = isSystemInDarkTheme() - - // 主题模式选择 - var themeMode by remember { - mutableIntStateOf( - when(ThemeConfig.forceDarkMode) { - true -> 2 // 深色 - false -> 1 // 浅色 - null -> 0 // 跟随系统 - } - ) - } - - // 动态颜色开关状态 - var useDynamicColor by remember { - mutableStateOf(ThemeConfig.useDynamicColor) - } - - var showThemeModeDialog by remember { mutableStateOf(false) } - // 主题模式选项 - val themeOptions = listOf( - stringResource(R.string.theme_follow_system), - stringResource(R.string.theme_light), - stringResource(R.string.theme_dark) - ) - - // 简洁模式开关状态 - var isSimpleMode by remember { - mutableStateOf(prefs.getBoolean("is_simple_mode", false)) - } - - // 更新简洁模式开关状态 - val onSimpleModeChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_simple_mode", newValue) } - isSimpleMode = newValue - } - - // 隐藏内核版本号开关状态 - var isHideVersion by remember { - mutableStateOf(prefs.getBoolean("is_hide_version", false)) - } - - // 隐藏内核版本号开关状态 - val onHideVersionChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_version", newValue) } - isHideVersion = newValue - } - - // 隐藏模块数量等信息开关状态 - var isHideOtherInfo by remember { - mutableStateOf(prefs.getBoolean("is_hide_other_info", false)) - } - - // 隐藏模块数量等信息开关状态 - val onHideOtherInfoChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_other_info", newValue) } - isHideOtherInfo = newValue - } - - // 隐藏SuSFS状态开关状态 - var isHideSusfsStatus by remember { - mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false)) - } - - // 隐藏SuSFS状态开关状态 - val onHideSusfsStatusChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_susfs_status", newValue) } - isHideSusfsStatus = newValue - } - - // 隐藏链接状态开关状态 - var isHideLinkCard by remember { - mutableStateOf(prefs.getBoolean("is_hide_link_card", false)) - } - - val onHideLinkCardChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_link_card", newValue) } - isHideLinkCard = newValue - } - - // SELinux状态 - var selinuxEnabled by remember { - mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing") - } - - // 卡片配置状态 - var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) } - var isCustomBackgroundEnabled by rememberSaveable { - mutableStateOf(ThemeConfig.customBackgroundUri != null) - } - - // 图片编辑状态 - var showImageEditor by remember { mutableStateOf(false) } - var selectedImageUri by remember { mutableStateOf(null) } - - // 展开/折叠状态 - var isCustomizeExpanded by remember { mutableStateOf(false) } - var isAppearanceExpanded by remember { mutableStateOf(true) } - var isAdvancedExpanded by remember { mutableStateOf(false) } - - // 初始化卡片配置 - LaunchedEffect(Unit) { - // 加载设置 - CardConfig.load(context) - cardAlpha = CardConfig.cardAlpha - isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null - - // 设置主题模式 - themeMode = when (ThemeConfig.forceDarkMode) { - true -> 2 - false -> 1 - null -> 0 - } - - // 确保卡片样式跟随主题模式 - when (themeMode) { - 2 -> { // 深色 - CardConfig.isUserDarkModeEnabled = true - CardConfig.isUserLightModeEnabled = false - } - 1 -> { // 浅色 - CardConfig.isUserDarkModeEnabled = false - CardConfig.isUserLightModeEnabled = true - } - 0 -> { // 跟随系统 - CardConfig.isUserDarkModeEnabled = false - CardConfig.isUserLightModeEnabled = false - } - } - - // 如果启用了系统跟随且系统是深色模式,应用深色模式默认值 - if (themeMode == 0 && systemIsDark) { - CardConfig.setDarkModeDefaults() - } - - // 保存设置 - CardConfig.save(context) - } - - // 主题色选项 - val themeColorOptions = listOf( - stringResource(R.string.color_default) to ThemeColors.Default, - stringResource(R.string.color_green) to ThemeColors.Green, - stringResource(R.string.color_purple) to ThemeColors.Purple, - stringResource(R.string.color_orange) to ThemeColors.Orange, - stringResource(R.string.color_pink) to ThemeColors.Pink, - stringResource(R.string.color_gray) to ThemeColors.Gray, - stringResource(R.string.color_yellow) to ThemeColors.Yellow - ) - - var showThemeColorDialog by remember { mutableStateOf(false) } - val ksuIsValid = Natives.isKsuValid(ksuApp.packageName) - - // 图片选择器 - val pickImageLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.GetContent() - ) { uri: Uri? -> - uri?.let { - selectedImageUri = it - showImageEditor = true - } - } - - // 显示图片编辑对话框 - if (showImageEditor && selectedImageUri != null) { - ImageEditorDialog( - imageUri = selectedImageUri!!, - onDismiss = { - showImageEditor = false - selectedImageUri = null - }, - onConfirm = { transformedUri -> - context.saveAndApplyCustomBackground(transformedUri) - isCustomBackgroundEnabled = true - CardConfig.cardElevation = 0.dp - CardConfig.isCustomBackgroundEnabled = true - saveCardConfig(context) - showImageEditor = false - selectedImageUri = null - - // 显示成功提示 - Toast.makeText( - context, - context.getString(R.string.background_set_success), - Toast.LENGTH_SHORT - ).show() - } - ) - } - - val cardColor = MaterialTheme.colorScheme.surfaceVariant - val cardAlphaUse = CardConfig.cardAlpha - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopAppBar( - title = { Text(stringResource(R.string.more_settings)) }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlphaUse), - scrolledContainerColor = cardColor.copy(alpha = cardAlphaUse)), - navigationIcon = { - IconButton(onClick = { navigator.popBackStack() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back)) - } - }, - scrollBehavior = scrollBehavior, - ) - } - ) { paddingValues -> - Column( - modifier = Modifier - .padding(paddingValues) - .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - // 外观设置部分 - SectionHeader( - title = stringResource(R.string.appearance_settings), - expanded = isAppearanceExpanded, - onToggle = { isAppearanceExpanded = !isAppearanceExpanded } - ) - - AnimatedVisibility( - visible = isAppearanceExpanded, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Surface( - shape = RoundedCornerShape(16.dp), - tonalElevation = 1.dp, - modifier = Modifier.padding(bottom = 16.dp) - ) { - Column { - // 主题模式 - ListItem( - headlineContent = { Text(stringResource(R.string.theme_mode)) }, - supportingContent = { Text(themeOptions[themeMode]) }, - leadingContent = { - Icon( - Icons.Default.DarkMode, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - }, - trailingContent = { - Icon( - Icons.AutoMirrored.Filled.NavigateNext, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - colors = ListItemDefaults.colors( - containerColor = Color.Transparent - ), - modifier = Modifier.clickable { showThemeModeDialog = true } - ) - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - - // 动态颜色开关 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - SwitchItem( - icon = Icons.Filled.ColorLens, - title = stringResource(R.string.dynamic_color_title), - summary = stringResource(R.string.dynamic_color_summary), - checked = useDynamicColor - ) { enabled -> - useDynamicColor = enabled - context.saveDynamicColorState(enabled) - } - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - } - - // 只在未启用动态颜色时显示主题色选择 - AnimatedVisibility( - visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !useDynamicColor, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Column { - ListItem( - headlineContent = { Text(stringResource(R.string.theme_color)) }, - supportingContent = { - val currentThemeName = when (ThemeConfig.currentTheme) { - is ThemeColors.Default -> stringResource(R.string.color_default) - is ThemeColors.Green -> stringResource(R.string.color_green) - is ThemeColors.Purple -> stringResource(R.string.color_purple) - is ThemeColors.Orange -> stringResource(R.string.color_orange) - is ThemeColors.Pink -> stringResource(R.string.color_pink) - is ThemeColors.Gray -> stringResource(R.string.color_gray) - is ThemeColors.Yellow -> stringResource(R.string.color_yellow) - else -> stringResource(R.string.color_default) - } - Text(currentThemeName) - }, - leadingContent = { - Icon( - Icons.Default.Palette, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - }, - trailingContent = { - Icon( - Icons.AutoMirrored.Filled.NavigateNext, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - colors = ListItemDefaults.colors( - containerColor = Color.Transparent - ), - modifier = Modifier.clickable { showThemeColorDialog = true } - ) - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - } - } - - // 自定义背景开关 - ListItem( - headlineContent = { Text(stringResource(id = R.string.settings_custom_background)) }, - supportingContent = { Text(stringResource(id = R.string.settings_custom_background_summary)) }, - leadingContent = { - Icon( - Icons.Filled.Wallpaper, - contentDescription = null, - tint = if (isCustomBackgroundEnabled) - MaterialTheme.colorScheme.primary - else - MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - trailingContent = { - Switch( - checked = isCustomBackgroundEnabled, - onCheckedChange = { isChecked -> - if (isChecked) { - pickImageLauncher.launch("image/*") - } else { - context.saveCustomBackground(null) - isCustomBackgroundEnabled = false - CardConfig.cardElevation - CardConfig.cardAlpha = 1f - CardConfig.isCustomAlphaSet = false - CardConfig.isCustomBackgroundEnabled = false - saveCardConfig(context) - cardAlpha = 1f - - // 重置其他相关设置 - ThemeConfig.needsResetOnThemeChange = true - ThemeConfig.preventBackgroundRefresh = false - - context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putBoolean( - "prevent_background_refresh", - false - ) - } - - Toast.makeText( - context, - context.getString(R.string.background_removed), - Toast.LENGTH_SHORT - ).show() - } - } - ) - }, - colors = ListItemDefaults.colors( - containerColor = Color.Transparent - ) - ) - - // 透明度 Slider - AnimatedVisibility( - visible = ThemeConfig.customBackgroundUri != null, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically(), - modifier = Modifier.padding(horizontal = 32.dp) - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 4.dp) - ) { - Icon( - Icons.Filled.Opacity, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.settings_card_alpha), - style = MaterialTheme.typography.titleSmall - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "${(cardAlpha * 100).roundToInt()}%", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Slider( - value = cardAlpha, - onValueChange = { newValue -> - cardAlpha = newValue - CardConfig.cardAlpha = newValue - CardConfig.isCustomAlphaSet = true - prefs.edit { - putBoolean("is_custom_alpha_set", true) - putFloat("card_alpha", newValue) - } - }, - onValueChangeFinished = { - coroutineScope.launch(Dispatchers.IO) { - saveCardConfig(context) - } - }, - valueRange = 0f..1f, - steps = 20, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - } - } - } - } - } - - // 自定义设置部分 - SectionHeader( - title = stringResource(R.string.custom_settings), - expanded = isCustomizeExpanded, - onToggle = { isCustomizeExpanded = !isCustomizeExpanded } - ) - - AnimatedVisibility( - visible = isCustomizeExpanded, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Surface( - shape = RoundedCornerShape(16.dp), - tonalElevation = 1.dp, - modifier = Modifier.padding(bottom = 16.dp) - ) { - Column { - // 添加简洁模式开关 - SwitchItem( - icon = Icons.Filled.Brush, - title = stringResource(R.string.simple_mode), - summary = stringResource(R.string.simple_mode_summary), - checked = isSimpleMode - ) { - onSimpleModeChange(it) - } - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - - // 隐藏内核部分版本号 - SwitchItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_kernel_kernelsu_version), - summary = stringResource(R.string.hide_kernel_kernelsu_version_summary), - checked = isHideVersion - ) { - onHideVersionChange(it) - } - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - - // 模块数量等信息 - SwitchItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_other_info), - summary = stringResource(R.string.hide_other_info_summary), - checked = isHideOtherInfo - ) { - onHideOtherInfoChange(it) - } - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - - // SuSFS 状态信息 - SwitchItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_susfs_status), - summary = stringResource(R.string.hide_susfs_status_summary), - checked = isHideSusfsStatus - ) { - onHideSusfsStatusChange(it) - } - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - - // 隐藏链接信息 - SwitchItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_link_card), - summary = stringResource(R.string.hide_link_card_summary), - checked = isHideLinkCard - ) { - onHideLinkCardChange(it) - } - } - } - } - - // 高级设置部分 - SectionHeader( - title = stringResource(R.string.advanced_settings), - expanded = isAdvancedExpanded, - onToggle = { isAdvancedExpanded = !isAdvancedExpanded } - ) - - AnimatedVisibility( - visible = isAdvancedExpanded, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Surface( - shape = RoundedCornerShape(16.dp), - tonalElevation = 1.dp, - modifier = Modifier.padding(bottom = 16.dp) - ) { - Column { - // SELinux 开关 - if (ksuIsValid) { - SwitchItem( - icon = Icons.Filled.Security, - title = stringResource(R.string.selinux), - summary = if (selinuxEnabled) - stringResource(R.string.selinux_enabled) else - stringResource(R.string.selinux_disabled), - checked = selinuxEnabled - ) { enabled -> - val command = if (enabled) "setenforce 1" else "setenforce 0" - Shell.getShell().newJob().add(command).exec().let { result -> - if (result.isSuccess) { - selinuxEnabled = enabled - // 显示成功提示 - val message = if (enabled) - context.getString(R.string.selinux_enabled_toast) - else - context.getString(R.string.selinux_disabled_toast) - - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - } else { - // 显示失败提示 - Toast.makeText( - context, - context.getString(R.string.selinux_change_failed), - Toast.LENGTH_SHORT - ).show() - } - } - } - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 16.dp), - color = MaterialTheme.colorScheme.outlineVariant - ) - } - - // SuSFS 配置(仅在支持时显示) - val suSFS = getSuSFS() - val isSUS_SU = getSuSFSFeatures() - if (suSFS == "Supported" && isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") { - // 初始化时,默认启用 - var isEnabled by rememberSaveable { - mutableStateOf(true) // 默认启用 - } - - // 在启动时检查状态 - LaunchedEffect(Unit) { - // 如果当前模式不是2就强制启用 - val currentMode = susfsSUS_SU_Mode() - val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true) - if (currentMode != "2" && wasManuallyDisabled) { - susfsSUS_SU_2() // 强制切换到模式2 - prefs.edit { putBoolean("enable_sus_su", true) } - } - isEnabled = currentMode == "2" - } - - SwitchItem( - icon = Icons.Filled.Security, - title = stringResource(id = R.string.settings_susfs_toggle), - summary = stringResource(id = R.string.settings_susfs_toggle_summary), - checked = isEnabled - ) { - if (it) { - // 手动启用 - susfsSUS_SU_2() - prefs.edit { putBoolean("enable_sus_su", true) } - Toast.makeText( - context, - context.getString(R.string.susfs_enabled), - Toast.LENGTH_SHORT - ).show() - } else { - // 手动关闭 - susfsSUS_SU_0() - prefs.edit { putBoolean("enable_sus_su", false) } - Toast.makeText( - context, - context.getString(R.string.susfs_disabled), - Toast.LENGTH_SHORT - ).show() - } - isEnabled = it - } - } - } - } - } - } - } - - // 主题模式选择对话框 - if (showThemeModeDialog) { - AlertDialog( - onDismissRequest = { showThemeModeDialog = false }, - title = { Text(stringResource(R.string.theme_mode)) }, - text = { - Column { - themeOptions.forEachIndexed { index, option -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - themeMode = index - val newThemeMode = when(index) { - 0 -> null // 跟随系统 - 1 -> false // 浅色 - 2 -> true // 深色 - else -> null - } - context.saveThemeMode(newThemeMode) - when (index) { - 2 -> { // 深色 - ThemeConfig.forceDarkMode = true - CardConfig.isUserDarkModeEnabled = true - CardConfig.isUserLightModeEnabled = false - if (!CardConfig.isCustomAlphaSet) { - CardConfig.cardAlpha = 1f - } - CardConfig.save(context) - } - 1 -> { // 浅色 - ThemeConfig.forceDarkMode = false - CardConfig.isUserLightModeEnabled = true - CardConfig.isUserDarkModeEnabled = false - if (!CardConfig.isCustomAlphaSet) { - CardConfig.cardAlpha = 1f - } - CardConfig.save(context) - } - 0 -> { // 跟随系统 - ThemeConfig.forceDarkMode = null - CardConfig.isUserLightModeEnabled = false - CardConfig.isUserDarkModeEnabled = false - CardConfig.save(context) - } - } - showThemeModeDialog = false - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = themeMode == index, - onClick = null - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(option) - } - } - } - }, - confirmButton = { - TextButton( - onClick = { showThemeModeDialog = false } - ) { - Text(stringResource(R.string.cancel)) - } - } - ) - } - - // 主题色选择对话框 - if (showThemeColorDialog) { - AlertDialog( - onDismissRequest = { showThemeColorDialog = false }, - title = { Text(stringResource(R.string.choose_theme_color)) }, - text = { - Column { - themeColorOptions.forEach { (name, theme) -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - context.saveThemeColors(when (theme) { - ThemeColors.Default -> "default" - ThemeColors.Green -> "green" - ThemeColors.Purple -> "purple" - ThemeColors.Orange -> "orange" - ThemeColors.Pink -> "pink" - ThemeColors.Gray -> "gray" - ThemeColors.Yellow -> "yellow" - else -> "default" - }) - showThemeColorDialog = false - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = ThemeConfig.currentTheme::class == theme::class, - onClick = null - ) - Spacer(modifier = Modifier.width(12.dp)) - Box( - modifier = Modifier - .size(24.dp) - .clip(CircleShape) - .background(theme.Primary) - ) - Spacer(modifier = Modifier.width(12.dp)) - Text(name) - } - } - } - }, - confirmButton = { - TextButton( - onClick = { showThemeColorDialog = false } - ) { - Text(stringResource(R.string.cancel)) - } - } - ) - } -} - -@Composable -fun SectionHeader( - title: String, - expanded: Boolean, - onToggle: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onToggle() } - .padding(vertical = 12.dp, horizontal = 4.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium.copy( - fontWeight = FontWeight.Bold - ), - color = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.weight(1f)) - Icon( - imageVector = if (expanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, - contentDescription = if (expanded) - stringResource(R.string.collapse) - else - stringResource(R.string.expand), - tint = MaterialTheme.colorScheme.primary - ) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt deleted file mode 100644 index 04e9ba4b..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt +++ /dev/null @@ -1,722 +0,0 @@ -package com.sukisu.ultra.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.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Undo -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -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.draw.clip -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.unit.dp -import androidx.core.content.FileProvider -import androidx.core.content.edit -import com.maxkeppeker.sheets.core.models.base.IconSource -import com.maxkeppeler.sheets.list.models.ListOption -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination -import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination -import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import com.sukisu.ultra.BuildConfig -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.* -import com.sukisu.ultra.ui.component.* -import com.sukisu.ultra.ui.theme.* -import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation -import com.sukisu.ultra.ui.util.LocalSnackbarHost -import com.sukisu.ultra.ui.util.getBugreportFile -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - - -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun SettingScreen(navigator: DestinationsNavigator) { - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val snackBarHost = LocalSnackbarHost.current - val ksuIsValid = Natives.isKsuValid(ksuApp.packageName) - - Scaffold( - topBar = { - TopBar( - scrollBehavior = scrollBehavior - ) - }, - snackbarHost = { SnackbarHost(snackBarHost) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) { paddingValues -> - val aboutDialog = rememberCustomDialog { - AboutDialog(it) - } - val loadingDialog = rememberLoadingDialog() - // endregion - - Column( - modifier = Modifier - .padding(paddingValues) - .nestedScroll(scrollBehavior.nestedScrollConnection) - .verticalScroll(rememberScrollState()) - ) { - // region 上下文与协程 - val context = LocalContext.current - val scope = rememberCoroutineScope() - // endregion - - // region 日志导出功能 - 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)) - } - } - - // 设置分组卡片 - 配置 - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha) - ), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation) - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - Text( - text = stringResource(R.string.configuration), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - - // 配置文件模板入口 - val profileTemplate = stringResource(id = R.string.settings_profile_template) - if (ksuIsValid) { - SettingItem( - icon = Icons.Filled.Fence, - title = profileTemplate, - summary = stringResource(id = R.string.settings_profile_template_summary), - onClick = { - navigator.navigate(AppProfileTemplateScreenDestination) - } - ) - } - - // 卸载模块开关 - var umountChecked by rememberSaveable { - mutableStateOf(Natives.isDefaultUmountModules()) - } - - if (ksuIsValid) { - SwitchSettingItem( - icon = Icons.Filled.FolderDelete, - title = stringResource(id = R.string.settings_umount_modules_default), - summary = stringResource(id = R.string.settings_umount_modules_default_summary), - checked = umountChecked, - onCheckedChange = { - if (Natives.setDefaultUmountModules(it)) { - umountChecked = it - } - } - ) - } - - // SU 禁用开关(仅在兼容版本显示) - if (ksuIsValid) { - if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) { - var isSuDisabled by rememberSaveable { - mutableStateOf(!Natives.isSuEnabled()) - } - SwitchSettingItem( - icon = Icons.Filled.RemoveModerator, - title = stringResource(id = R.string.settings_disable_su), - summary = stringResource(id = R.string.settings_disable_su_summary), - checked = isSuDisabled, - onCheckedChange = { checked -> - val shouldEnable = !checked - if (Natives.setSuEnabled(shouldEnable)) { - isSuDisabled = !shouldEnable - } - } - ) - } - } - } - } - - // 设置分组卡片 - 应用设置 - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha) - ), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation) - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - Text( - text = stringResource(R.string.app_settings), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - - // 更新检查开关 - var checkUpdate by rememberSaveable { - mutableStateOf( - prefs.getBoolean("check_update", true) - ) - } - SwitchSettingItem( - icon = Icons.Filled.Update, - title = stringResource(id = R.string.settings_check_update), - summary = stringResource(id = R.string.settings_check_update_summary), - checked = checkUpdate, - onCheckedChange = { - prefs.edit {putBoolean("check_update", it) } - checkUpdate = it - } - ) - - // Web调试开关 - var enableWebDebugging by rememberSaveable { - mutableStateOf( - prefs.getBoolean("enable_web_debugging", false) - ) - } - if (Natives.isKsuValid(ksuApp.packageName)) { - SwitchSettingItem( - icon = Icons.Filled.DeveloperMode, - title = stringResource(id = R.string.enable_web_debugging), - summary = stringResource(id = R.string.enable_web_debugging_summary), - checked = enableWebDebugging, - onCheckedChange = { - prefs.edit { putBoolean("enable_web_debugging", it) } - enableWebDebugging = it - } - ) - } - - // 更多设置 - SettingItem( - icon = Icons.Filled.Settings, - title = stringResource(id = R.string.more_settings), - summary = stringResource(id = R.string.more_settings), - onClick = { - navigator.navigate(MoreSettingsScreenDestination) - } - ) - } - } - - // 设置分组卡片 - 工具 - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha) - ), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation) - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - Text( - text = stringResource(R.string.tools), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - - var showBottomsheet by remember { mutableStateOf(false) } - - SettingItem( - icon = Icons.Filled.BugReport, - title = stringResource(id = R.string.send_log), - onClick = { - showBottomsheet = true - } - ) - - if (showBottomsheet) { - ModalBottomSheet( - onDismissRequest = { showBottomsheet = false }, - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceEvenly - ) { - LogActionButton( - icon = Icons.Filled.Save, - text = stringResource(R.string.save_log), - onClick = { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") - val current = LocalDateTime.now().format(formatter) - exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz") - showBottomsheet = false - } - ) - - LogActionButton( - icon = Icons.Filled.Share, - text = stringResource(R.string.send_log), - onClick = { - 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) - ) - ) - - showBottomsheet = false - } - } - ) - } - Spacer(modifier = Modifier.height(16.dp)) - } - } - - val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode - if (lkmMode) { - UninstallItem(navigator) { - loadingDialog.withLoading(it) - } - } - } - } - - // 设置分组卡片 - 关于 - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = cardAlpha) - ), - elevation = CardDefaults.cardElevation(defaultElevation = cardElevation) - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - Text( - text = stringResource(R.string.about), - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - - SettingItem( - icon = Icons.Filled.Info, - title = stringResource(R.string.about), - onClick = { - aboutDialog.show() - } - ) - } - } - - Spacer(modifier = Modifier.height(16.dp)) - } - } -} - -@Composable -fun LogActionButton( - icon: ImageVector, - text: String, - onClick: () -> Unit -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .clickable(onClick = onClick) - .padding(8.dp) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(56.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer) - ) { - Icon( - imageVector = icon, - contentDescription = text, - tint = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.size(24.dp) - ) - } - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = text, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurface - ) - } -} - -@Composable -fun SettingItem( - icon: ImageVector, - title: String, - summary: String? = null, - onClick: () -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - if (summary != null) { - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Icon( - imageVector = Icons.Filled.ChevronRight, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp) - ) - } -} - -@Composable -fun SwitchSettingItem( - icon: ImageVector, - title: String, - summary: String? = null, - checked: Boolean, - onCheckedChange: (Boolean) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onCheckedChange(!checked) } - .padding(horizontal = 16.dp, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - if (summary != null) { - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - Switch( - checked = checked, - onCheckedChange = onCheckedChange, - colors = SwitchDefaults.colors( - checkedThumbColor = MaterialTheme.colorScheme.onPrimary, - checkedTrackColor = MaterialTheme.colorScheme.primary, - checkedIconColor = MaterialTheme.colorScheme.primary, - uncheckedThumbColor = MaterialTheme.colorScheme.outline, - uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant, - uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - } -} - -@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 - } - } - } - } - } - - SettingItem( - icon = Icons.Filled.Delete, - title = stringResource(id = R.string.settings_uninstall), - onClick = { - 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 -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 - val cardColor = if (!ThemeConfig.useDynamicColor) { - ThemeConfig.currentTheme.ButtonContrast - } else { - MaterialTheme.colorScheme.surfaceContainerHigh - } - - AlertDialog( - onDismissRequest = { - dismiss() - }, - title = { - Text( - text = stringResource(R.string.settings_uninstall), - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface - ) - }, - text = { - Column( - modifier = Modifier.padding(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - listOptions.forEachIndexed { index, option -> - Row( - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .clickable { - selection = options[index] - } - .padding(vertical = 12.dp, horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = options[index].icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - Column { - Text( - text = option.titleText, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface - ) - option.subtitleText?.let { - Text( - text = it, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - } - }, - confirmButton = { - TextButton( - onClick = { - if (selection != UninstallType.NONE) { - onSelected(selection) - } - dismiss() - } - ) { - Text( - text = stringResource(android.R.string.ok), - color = MaterialTheme.colorScheme.primary - ) - } - }, - dismissButton = { - TextButton( - onClick = { - dismiss() - } - ) { - Text( - text = stringResource(android.R.string.cancel), - color = MaterialTheme.colorScheme.primary - ) - } - }, - containerColor = cardColor, - shape = MaterialTheme.shapes.extraLarge, - tonalElevation = 4.dp - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar( - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val systemIsDark = isSystemInDarkTheme() - val cardColor = MaterialTheme.colorScheme.surfaceVariant - val cardAlpha = if (ThemeConfig.customBackgroundUri != null) { - cardAlpha - } else { - if (systemIsDark) 0.8f else 1f - } - - TopAppBar( - title = { - Text( - text = stringResource(R.string.settings), - style = MaterialTheme.typography.titleLarge - ) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt deleted file mode 100644 index 5970f0ba..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt +++ /dev/null @@ -1,594 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import androidx.compose.animation.* -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel -import com.sukisu.ultra.R -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 com.sukisu.ultra.Natives -import com.sukisu.ultra.ui.component.SearchAppBar -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation -import com.sukisu.ultra.ui.util.ModuleModify -import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel - -@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() - val context = LocalContext.current - val snackBarHostState = remember { SnackbarHostState() } - - // 添加备份和还原启动器 - val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState) - val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState) - - LaunchedEffect(key1 = navigator) { - viewModel.search = "" - if (viewModel.appList.isEmpty()) { - viewModel.fetchAppList() - } - } - - LaunchedEffect(viewModel.search) { - if (viewModel.search.isEmpty()) { - listState.scrollToItem(0) - } - } - - Scaffold( - topBar = { - SearchAppBar( - title = { Text(stringResource(R.string.superuser)) }, - searchText = viewModel.search, - onSearchTextChange = { viewModel.search = it }, - onClearClick = { viewModel.search = "" }, - dropdownContent = { - var showDropdown by remember { mutableStateOf(false) } - - IconButton( - onClick = { showDropdown = true }, - ) { - Icon( - imageVector = Icons.Filled.MoreVert, - contentDescription = stringResource(id = R.string.settings), - ) - - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false - }) { - DropdownMenuItem( - text = { Text(stringResource(R.string.refresh)) }, - leadingIcon = { - Icon( - imageVector = Icons.Filled.Refresh, - contentDescription = null, - ) - }, - onClick = { - scope.launch { - viewModel.fetchAppList() - } - showDropdown = false - } - ) - DropdownMenuItem( - text = { - Text( - if (viewModel.showSystemApps) { - stringResource(R.string.hide_system_apps) - } else { - stringResource(R.string.show_system_apps) - } - ) - }, - leadingIcon = { - Icon( - imageVector = if (viewModel.showSystemApps) - Icons.Filled.VisibilityOff else Icons.Filled.Visibility, - contentDescription = null, - ) - }, - onClick = { - viewModel.showSystemApps = !viewModel.showSystemApps - showDropdown = false - } - ) - HorizontalDivider(thickness = 0.5.dp, modifier = Modifier.padding(vertical = 4.dp)) - DropdownMenuItem( - text = { Text(stringResource(R.string.backup_allowlist)) }, - leadingIcon = { - Icon( - imageVector = Icons.Filled.Save, - contentDescription = null, - ) - }, - onClick = { - backupLauncher.launch(ModuleModify.createAllowlistBackupIntent()) - showDropdown = false - } - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.restore_allowlist)) }, - leadingIcon = { - Icon( - imageVector = Icons.Filled.RestoreFromTrash, - contentDescription = null, - ) - }, - onClick = { - restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent()) - showDropdown = false - } - ) - } - } - }, - scrollBehavior = scrollBehavior - ) - }, - snackbarHost = { SnackbarHost(snackBarHostState) }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - bottomBar = { - // 批量操作按钮,直接放在底部栏 - AnimatedVisibility( - visible = viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty(), - enter = slideInVertically(initialOffsetY = { it }), - exit = slideOutVertically(targetOffsetY = { it }) - ) { - Surface( - color = MaterialTheme.colorScheme.surfaceContainerHighest, - tonalElevation = cardElevation, - shadowElevation = cardElevation - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { - OutlinedButton( - onClick = { - // 修改为重新赋值为空集合 - viewModel.selectedApps = emptySet() - viewModel.showBatchActions = false - }, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon( - imageVector = Icons.Filled.Close, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(android.R.string.cancel)) - } - - Button( - onClick = { - scope.launch { - viewModel.updateBatchPermissions(true) - } - }, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primary - ) - ) { - Icon( - imageVector = Icons.Filled.Check, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.batch_authorization)) - } - - Button( - onClick = { - scope.launch { - viewModel.updateBatchPermissions(false) - } - }, - modifier = Modifier.weight(1f), - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error - ) - ) { - Icon( - imageVector = Icons.Filled.Block, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.batch_cancel_authorization)) - } - } - } - } - } - ) { innerPadding -> - PullToRefreshBox( - modifier = Modifier.padding(innerPadding), - onRefresh = { - scope.launch { viewModel.fetchAppList() } - }, - isRefreshing = viewModel.isRefreshing - ) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection), - contentPadding = PaddingValues( - top = 8.dp, - bottom = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) 88.dp else 16.dp - ) - ) { - // 获取分组后的应用列表 - val rootApps = viewModel.appList.filter { it.allowSu } - val customApps = viewModel.appList.filter { !it.allowSu && it.hasCustomProfile } - val otherApps = viewModel.appList.filter { !it.allowSu && !it.hasCustomProfile } - - // 显示ROOT权限应用组 - if (rootApps.isNotEmpty()) { - item { - GroupHeader(title = stringResource(R.string.apps_with_root)) - } - - items(rootApps, key = { "root_" + it.packageName + it.uid }) { app -> - AppItem( - app = app, - isSelected = viewModel.selectedApps.contains(app.packageName), - onToggleSelection = { viewModel.toggleAppSelection(app.packageName) }, - onSwitchChange = { allowSu -> - scope.launch { - val profile = Natives.getAppProfile(app.packageName, app.uid) - val updatedProfile = profile.copy(allowSu = allowSu) - if (Natives.setAppProfile(updatedProfile)) { - viewModel.fetchAppList() - } - } - }, - onClick = { - if (viewModel.showBatchActions) { - viewModel.toggleAppSelection(app.packageName) - } else { - navigator.navigate(AppProfileScreenDestination(app)) - } - }, - onLongClick = { - // 长按进入多选模式 - if (!viewModel.showBatchActions) { - viewModel.toggleBatchMode() - viewModel.toggleAppSelection(app.packageName) - } - }, - viewModel = viewModel - ) - } - } - - // 显示自定义配置应用组 - if (customApps.isNotEmpty()) { - item { - GroupHeader(title = stringResource(R.string.apps_with_custom_profile)) - } - - items(customApps, key = { "custom_" + it.packageName + it.uid }) { app -> - AppItem( - app = app, - isSelected = viewModel.selectedApps.contains(app.packageName), - onToggleSelection = { viewModel.toggleAppSelection(app.packageName) }, - onSwitchChange = { allowSu -> - scope.launch { - val profile = Natives.getAppProfile(app.packageName, app.uid) - val updatedProfile = profile.copy(allowSu = allowSu) - if (Natives.setAppProfile(updatedProfile)) { - viewModel.fetchAppList() - } - } - }, - onClick = { - if (viewModel.showBatchActions) { - viewModel.toggleAppSelection(app.packageName) - } else { - navigator.navigate(AppProfileScreenDestination(app)) - } - }, - onLongClick = { - // 长按进入多选模式 - if (!viewModel.showBatchActions) { - viewModel.toggleBatchMode() - viewModel.toggleAppSelection(app.packageName) - } - }, - viewModel = viewModel - ) - } - } - - // 显示其他应用组 - if (otherApps.isNotEmpty()) { - item { - GroupHeader(title = stringResource(R.string.other_apps)) - } - - items(otherApps, key = { "other_" + it.packageName + it.uid }) { app -> - AppItem( - app = app, - isSelected = viewModel.selectedApps.contains(app.packageName), - onToggleSelection = { viewModel.toggleAppSelection(app.packageName) }, - onSwitchChange = { allowSu -> - scope.launch { - val profile = Natives.getAppProfile(app.packageName, app.uid) - val updatedProfile = profile.copy(allowSu = allowSu) - if (Natives.setAppProfile(updatedProfile)) { - viewModel.fetchAppList() - } - } - }, - onClick = { - if (viewModel.showBatchActions) { - viewModel.toggleAppSelection(app.packageName) - } else { - navigator.navigate(AppProfileScreenDestination(app)) - } - }, - onLongClick = { - // 长按进入多选模式 - if (!viewModel.showBatchActions) { - viewModel.toggleBatchMode() - viewModel.toggleAppSelection(app.packageName) - } - }, - viewModel = viewModel - ) - } - } - - // 当没有应用显示时显示空状态 - if (viewModel.appList.isEmpty()) { - item { - Box( - modifier = Modifier - .fillMaxWidth() - .height(400.dp), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Filled.Apps, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), - modifier = Modifier - .size(96.dp) - .padding(bottom = 16.dp) - ) - Text( - text = stringResource(R.string.no_apps_found), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - } - } - } -} - -@Composable -fun GroupHeader(title: String) { - Box( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.7f)) - .padding(horizontal = 16.dp, vertical = 8.dp) - ) { - Text( - text = title, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary - ) - ) - } -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun AppItem( - app: SuperUserViewModel.AppInfo, - isSelected: Boolean, - onToggleSelection: () -> Unit, - onSwitchChange: (Boolean) -> Unit, - onClick: () -> Unit, - onLongClick: () -> Unit, - viewModel: SuperUserViewModel -) { - val cardColor = if (app.allowSu) - MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - else if (app.hasCustomProfile) - MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.3f) - else - MaterialTheme.colorScheme.surfaceContainerLow - - Card( - colors = CardDefaults.cardColors(containerColor = cardColor), - elevation = CardDefaults.cardElevation(defaultElevation = 0.dp), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp, vertical = 4.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = 0.dp, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) - .then( - if (isSelected) - Modifier.border( - width = 2.dp, - color = MaterialTheme.colorScheme.primary, - shape = MaterialTheme.shapes.medium - ) - else - Modifier - ) - .pointerInput(Unit) { - detectTapGestures( - onLongPress = { onLongClick() }, - onTap = { onClick() } - ) - } - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(app.packageInfo) - .crossfade(true) - .build(), - contentDescription = app.label, - modifier = Modifier - .padding(end = 16.dp) - .size(48.dp) - .clip(MaterialTheme.shapes.small) - ) - - Column( - modifier = Modifier - .weight(1f) - .padding(end = 8.dp) - ) { - Text( - text = app.label, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis - ) - - Text( - text = app.packageName, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis - ) - - FlowRow( - modifier = Modifier.padding(top = 4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (app.allowSu) { - LabelText(label = "ROOT", backgroundColor = MaterialTheme.colorScheme.primary) - } - if (Natives.uidShouldUmount(app.uid)) { - LabelText(label = "UMOUNT", backgroundColor = MaterialTheme.colorScheme.tertiary) - } - if (app.hasCustomProfile) { - LabelText(label = "CUSTOM", backgroundColor = MaterialTheme.colorScheme.secondary) - } - } - } - - if (!viewModel.showBatchActions) { - Switch( - checked = app.allowSu, - onCheckedChange = onSwitchChange, - colors = SwitchDefaults.colors( - checkedThumbColor = MaterialTheme.colorScheme.onPrimary, - checkedTrackColor = MaterialTheme.colorScheme.primary, - checkedIconColor = MaterialTheme.colorScheme.primary, - uncheckedThumbColor = MaterialTheme.colorScheme.outline, - uncheckedTrackColor = MaterialTheme.colorScheme.surfaceVariant, - uncheckedIconColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - } else { - Checkbox( - checked = isSelected, - onCheckedChange = { onToggleSelection() }, - colors = CheckboxDefaults.colors( - checkedColor = MaterialTheme.colorScheme.primary, - uncheckedColor = MaterialTheme.colorScheme.outline - ) - ) - } - } - } -} - -@Composable -fun LabelText(label: String, backgroundColor: Color) { - Box( - modifier = Modifier - .padding(top = 2.dp, end = 2.dp) - .background( - backgroundColor, - shape = RoundedCornerShape(4.dp) - ) - .clip(RoundedCornerShape(4.dp)) - ) { - Text( - text = label, - modifier = Modifier.padding(vertical = 2.dp, horizontal = 6.dp), - style = TextStyle( - fontSize = 10.sp, - color = Color.White, - fontWeight = FontWeight.Medium - ) - ) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt deleted file mode 100644 index 0c89211d..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt +++ /dev/null @@ -1,288 +0,0 @@ -package com.sukisu.ultra.ui.screen - -import android.content.ClipData -import android.content.ClipboardManager -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.TopAppBarColors -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.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.core.content.getSystemService -import androidx.lifecycle.compose.dropUnlessResumed -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 com.sukisu.ultra.R -import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.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() } - } - } - - val cardColorUse = MaterialTheme.colorScheme.surfaceVariant - val cardAlpha = CardConfig.cardAlpha - - Scaffold( - topBar = { - val context = LocalContext.current - val clipboardManager = context.getSystemService() - val showToast = fun(msg: String) { - scope.launch(Dispatchers.Main) { - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - } - } - TopBar( - onBack = dropUnlessResumed { navigator.popBackStack() }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColorUse.copy(alpha = cardAlpha), - scrolledContainerColor = cardColorUse.copy(alpha = cardAlpha) - ), - onSync = { - scope.launch { viewModel.fetchTemplates(true) } - }, - onImport = { - scope.launch { - val clipboardText = clipboardManager?.primaryClip?.getItemAt(0)?.text?.toString() - if (clipboardText.isNullOrEmpty()) { - showToast(context.getString(R.string.app_profile_template_import_empty)) - return@launch - } - viewModel.importTemplates( - clipboardText, - { - 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)) - } - ) { text -> - clipboardManager?.setPrimaryClip(ClipData.newPlainText("", text)) - } - } - }, - 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)) }, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer - ) - }, - 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}", backgroundColor = MaterialTheme.colorScheme.surface) - LabelText(label = "GID: ${template.gid}", backgroundColor = MaterialTheme.colorScheme.surface) - LabelText(label = template.context, backgroundColor = MaterialTheme.colorScheme.surface) - if (template.local) { - LabelText(label = "local", backgroundColor = MaterialTheme.colorScheme.surface) - } else { - LabelText(label = "remote", backgroundColor = MaterialTheme.colorScheme.surface) - } - } - } - } - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun TopBar( - onBack: () -> Unit, - onSync: () -> Unit = {}, - onImport: () -> Unit = {}, - onExport: () -> Unit = {}, - colors: TopAppBarColors, - scrollBehavior: TopAppBarScrollBehavior? = null -) { - val cardColor = MaterialTheme.colorScheme.surfaceVariant - val cardAlpha = CardConfig.cardAlpha - - TopAppBar( - title = { - Text(stringResource(R.string.settings_profile_template)) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = cardColor.copy(alpha = cardAlpha), - scrolledContainerColor = cardColor.copy(alpha = cardAlpha) - ), - 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/com/sukisu/ultra/ui/screen/TemplateEditor.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/TemplateEditor.kt deleted file mode 100644 index d00ad186..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/TemplateEditor.kt +++ /dev/null @@ -1,340 +0,0 @@ -package com.sukisu.ultra.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 com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.profile.RootProfileConfig -import com.sukisu.ultra.ui.util.deleteAppProfileTemplate -import com.sukisu.ultra.ui.util.getAppProfileTemplate -import com.sukisu.ultra.ui.util.setAppProfileTemplate -import com.sukisu.ultra.ui.viewmodel.TemplateViewModel -import com.sukisu.ultra.ui.viewmodel.toJSON -import androidx.lifecycle.compose.dropUnlessResumed - -/** - * @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 = dropUnlessResumed { 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/com/sukisu/ultra/ui/theme/CardManage.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt deleted file mode 100644 index 431ed164..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.sukisu.ultra.ui.theme - -import android.content.Context -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.CardDefaults -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -object CardConfig { - val settingElevation: Dp = 4.dp - val customBackgroundElevation: Dp = 0.dp - - var cardAlpha by mutableStateOf(1f) - var cardElevation by mutableStateOf(settingElevation) - var isShadowEnabled by mutableStateOf(true) - var isCustomAlphaSet by mutableStateOf(false) - var isUserDarkModeEnabled by mutableStateOf(false) - var isUserLightModeEnabled by mutableStateOf(false) - var isCustomBackgroundEnabled by mutableStateOf(false) - - /** - * 保存卡片配置到SharedPreferences - */ - fun save(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - prefs.edit().apply { - putFloat("card_alpha", cardAlpha) - putBoolean("custom_background_enabled", isCustomBackgroundEnabled) - putBoolean("is_shadow_enabled", isShadowEnabled) - putBoolean("is_custom_alpha_set", isCustomAlphaSet) - putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled) - putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled) - apply() - } - } - - /** - * 从SharedPreferences加载卡片配置 - */ - fun load(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - cardAlpha = prefs.getFloat("card_alpha", 1f) - isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false) - isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true) - isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false) - isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false) - isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false) - updateShadowEnabled(isShadowEnabled) - } - - /** - * 更新阴影启用状态 - */ - fun updateShadowEnabled(enabled: Boolean) { - isShadowEnabled = enabled - cardElevation = if (isCustomBackgroundEnabled && cardAlpha != 1f) { - customBackgroundElevation - } else if (enabled) { - settingElevation - } else { - customBackgroundElevation - } - } - - /** - * 设置深色模式默认值 - */ - fun setDarkModeDefaults() { - if (!isCustomAlphaSet) { - cardAlpha = 1f - } - updateShadowEnabled(isShadowEnabled) - } -} - -/** - * 获取卡片颜色配置 - */ -@Composable -fun getCardColors(originalColor: Color) = CardDefaults.cardColors( - containerColor = originalColor.copy(alpha = CardConfig.cardAlpha), - contentColor = determineContentColor(originalColor) -) - -/** - * 根据背景颜色、主题模式和用户设置确定内容颜色 - */ -@Composable -private fun determineContentColor(originalColor: Color): Color { - val isDarkTheme = isSystemInDarkTheme() - if (ThemeConfig.isThemeChanging) { - return if (isDarkTheme) Color.White else Color.Black - } - - return when { - CardConfig.isUserLightModeEnabled -> Color.Black - !isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black - isDarkTheme -> Color.White - else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt deleted file mode 100644 index f7eaffa1..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Color.kt +++ /dev/null @@ -1,273 +0,0 @@ -package com.sukisu.ultra.ui.theme - -import androidx.compose.ui.graphics.Color - -sealed class ThemeColors { - abstract val Primary: Color - abstract val Secondary: Color - abstract val Tertiary: Color - abstract val OnPrimary: Color - abstract val OnSecondary: Color - abstract val OnTertiary: Color - abstract val PrimaryContainer: Color - abstract val SecondaryContainer: Color - abstract val TertiaryContainer: Color - abstract val OnPrimaryContainer: Color - abstract val OnSecondaryContainer: Color - abstract val OnTertiaryContainer: Color - abstract val ButtonContrast: Color - - // 表面颜色 - abstract val Surface: Color - abstract val SurfaceVariant: Color - abstract val OnSurface: Color - abstract val OnSurfaceVariant: Color - - // 错误状态颜色 - abstract val Error: Color - abstract val OnError: Color - abstract val ErrorContainer: Color - abstract val OnErrorContainer: Color - - // 边框和背景色 - abstract val Outline: Color - abstract val OutlineVariant: Color - abstract val Background: Color - abstract val OnBackground: Color - - // 默认主题 (蓝色) - object Default : ThemeColors() { - override val Primary = Color(0xFF2196F3) - override val Secondary = Color(0xFF64B5F6) - override val Tertiary = Color(0xFF0D47A1) - override val OnPrimary = Color(0xFFFFFFFF) - override val OnSecondary = Color(0xFFFFFFFF) - override val OnTertiary = Color(0xFFFFFFFF) - override val PrimaryContainer = Color(0xFFD6EAFF) - override val SecondaryContainer = Color(0xFFE3F2FD) - override val TertiaryContainer = Color(0xFFCFD8DC) - override val OnPrimaryContainer = Color(0xFF0A3049) - override val OnSecondaryContainer = Color(0xFF0D3C61) - override val OnTertiaryContainer = Color(0xFF071D41) - override val ButtonContrast = Color(0xFF2196F3) - - override val Surface = Color(0xFFF5F9FF) - override val SurfaceVariant = Color(0xFFEDF5FE) - override val OnSurface = Color(0xFF1A1C1E) - override val OnSurfaceVariant = Color(0xFF42474E) - - override val Error = Color(0xFFB00020) - override val OnError = Color(0xFFFFFFFF) - override val ErrorContainer = Color(0xFFFDE7E9) - override val OnErrorContainer = Color(0xFF410008) - - override val Outline = Color(0xFFBAC3CF) - override val OutlineVariant = Color(0xFFDFE3EB) - override val Background = Color(0xFFFAFCFF) - override val OnBackground = Color(0xFF1A1C1E) - } - - // 绿色主题 - object Green : ThemeColors() { - override val Primary = Color(0xFF43A047) - override val Secondary = Color(0xFF66BB6A) - override val Tertiary = Color(0xFF1B5E20) - override val OnPrimary = Color(0xFFFFFFFF) - override val OnSecondary = Color(0xFFFFFFFF) - override val OnTertiary = Color(0xFFFFFFFF) - override val PrimaryContainer = Color(0xFFD8EFDB) - override val SecondaryContainer = Color(0xFFE8F5E9) - override val TertiaryContainer = Color(0xFFB9F6CA) - override val OnPrimaryContainer = Color(0xFF0A280D) - override val OnSecondaryContainer = Color(0xFF0E2912) - override val OnTertiaryContainer = Color(0xFF051B07) - override val ButtonContrast = Color(0xFF43A047) - - override val Surface = Color(0xFFF6FBF6) - override val SurfaceVariant = Color(0xFFEDF7EE) - override val OnSurface = Color(0xFF191C19) - override val OnSurfaceVariant = Color(0xFF414941) - - override val Error = Color(0xFFC62828) - override val OnError = Color(0xFFFFFFFF) - override val ErrorContainer = Color(0xFFF8D7DA) - override val OnErrorContainer = Color(0xFF4A0808) - - override val Outline = Color(0xFFBDC9BF) - override val OutlineVariant = Color(0xFFDDE6DE) - override val Background = Color(0xFFFBFDFB) - override val OnBackground = Color(0xFF191C19) - } - - // 紫色主题 - object Purple : ThemeColors() { - override val Primary = Color(0xFF9C27B0) - override val Secondary = Color(0xFFBA68C8) - override val Tertiary = Color(0xFF6A1B9A) - override val OnPrimary = Color(0xFFFFFFFF) - override val OnSecondary = Color(0xFFFFFFFF) - override val OnTertiary = Color(0xFFFFFFFF) - override val PrimaryContainer = Color(0xFFF3D8F8) - override val SecondaryContainer = Color(0xFFF5E9F7) - override val TertiaryContainer = Color(0xFFE1BEE7) - override val OnPrimaryContainer = Color(0xFF2A0934) - override val OnSecondaryContainer = Color(0xFF3C0F50) - override val OnTertiaryContainer = Color(0xFF1D0830) - override val ButtonContrast = Color(0xFF9C27B0) - - override val Surface = Color(0xFFFCF6FF) - override val SurfaceVariant = Color(0xFFF5EEFA) - override val OnSurface = Color(0xFF1D1B1E) - override val OnSurfaceVariant = Color(0xFF49454E) - - override val Error = Color(0xFFD50000) - override val OnError = Color(0xFFFFFFFF) - override val ErrorContainer = Color(0xFFFFDCD5) - override val OnErrorContainer = Color(0xFF480000) - - override val Outline = Color(0xFFC9B9D0) - override val OutlineVariant = Color(0xFFE8DAED) - override val Background = Color(0xFFFFFBFF) - override val OnBackground = Color(0xFF1D1B1E) - } - - // 橙色主题 - object Orange : ThemeColors() { - override val Primary = Color(0xFFFF9800) - override val Secondary = Color(0xFFFFB74D) - override val Tertiary = Color(0xFFE65100) - override val OnPrimary = Color(0xFFFFFFFF) - override val OnSecondary = Color(0xFF000000) - override val OnTertiary = Color(0xFFFFFFFF) - override val PrimaryContainer = Color(0xFFFFECCC) - override val SecondaryContainer = Color(0xFFFFF0D9) - override val TertiaryContainer = Color(0xFFFFD180) - override val OnPrimaryContainer = Color(0xFF351F00) - override val OnSecondaryContainer = Color(0xFF3D2800) - override val OnTertiaryContainer = Color(0xFF2E1500) - override val ButtonContrast = Color(0xFFFF9800) - - override val Surface = Color(0xFFFFF8F3) - override val SurfaceVariant = Color(0xFFFFF0E6) - override val OnSurface = Color(0xFF1F1B16) - override val OnSurfaceVariant = Color(0xFF4E4639) - - override val Error = Color(0xFFD32F2F) - override val OnError = Color(0xFFFFFFFF) - override val ErrorContainer = Color(0xFFFFDBC8) - override val OnErrorContainer = Color(0xFF490700) - - override val Outline = Color(0xFFD6C3AD) - override val OutlineVariant = Color(0xFFEFDFCC) - override val Background = Color(0xFFFFFBFF) - override val OnBackground = Color(0xFF1F1B16) - } - - // 粉色主题 - object Pink : ThemeColors() { - override val Primary = Color(0xFFE91E63) - override val Secondary = Color(0xFFF06292) - override val Tertiary = Color(0xFF880E4F) - override val OnPrimary = Color(0xFFFFFFFF) - override val OnSecondary = Color(0xFFFFFFFF) - override val OnTertiary = Color(0xFFFFFFFF) - override val PrimaryContainer = Color(0xFFFCE4EC) - override val SecondaryContainer = Color(0xFFFCE4EC) - override val TertiaryContainer = Color(0xFFF8BBD0) - override val OnPrimaryContainer = Color(0xFF3B0819) - override val OnSecondaryContainer = Color(0xFF3B0819) - override val OnTertiaryContainer = Color(0xFF2B0516) - override val ButtonContrast = Color(0xFFE91E63) - - override val Surface = Color(0xFFFFF7F9) - override val SurfaceVariant = Color(0xFFFCEEF2) - override val OnSurface = Color(0xFF201A1C) - override val OnSurfaceVariant = Color(0xFF534347) - - override val Error = Color(0xFFB71C1C) - override val OnError = Color(0xFFFFFFFF) - override val ErrorContainer = Color(0xFFFFDAD6) - override val OnErrorContainer = Color(0xFF410002) - - override val Outline = Color(0xFFD6BABF) - override val OutlineVariant = Color(0xFFEFDDE0) - override val Background = Color(0xFFFFFBFF) - override val OnBackground = Color(0xFF201A1C) - } - - // 灰色主题 - object Gray : ThemeColors() { - override val Primary = Color(0xFF607D8B) - override val Secondary = Color(0xFF90A4AE) - override val Tertiary = Color(0xFF455A64) - override val OnPrimary = Color(0xFFFFFFFF) - override val OnSecondary = Color(0xFFFFFFFF) - override val OnTertiary = Color(0xFFFFFFFF) - override val PrimaryContainer = Color(0xFFECEFF1) - override val SecondaryContainer = Color(0xFFECEFF1) - override val TertiaryContainer = Color(0xFFCFD8DC) - override val OnPrimaryContainer = Color(0xFF1A2327) - override val OnSecondaryContainer = Color(0xFF1A2327) - override val OnTertiaryContainer = Color(0xFF121A1D) - override val ButtonContrast = Color(0xFF607D8B) - - override val Surface = Color(0xFFF6F9FB) - override val SurfaceVariant = Color(0xFFEEF2F4) - override val OnSurface = Color(0xFF191C1E) - override val OnSurfaceVariant = Color(0xFF41484D) - - override val Error = Color(0xFFC62828) - override val OnError = Color(0xFFFFFFFF) - override val ErrorContainer = Color(0xFFFFDAD6) - override val OnErrorContainer = Color(0xFF410002) - - override val Outline = Color(0xFFBDC1C4) - override val OutlineVariant = Color(0xFFDDE1E3) - override val Background = Color(0xFFFBFCFE) - override val OnBackground = Color(0xFF191C1E) - } - - // 黄色主题 - object Yellow : ThemeColors() { - override val Primary = Color(0xFFFFC107) - override val Secondary = Color(0xFFFFD54F) - override val Tertiary = Color(0xFFFF8F00) - override val OnPrimary = Color(0xFF000000) - override val OnSecondary = Color(0xFF000000) - override val OnTertiary = Color(0xFFFFFFFF) - override val PrimaryContainer = Color(0xFFFFF8E1) - override val SecondaryContainer = Color(0xFFFFF8E1) - override val TertiaryContainer = Color(0xFFFFECB3) - override val OnPrimaryContainer = Color(0xFF332A00) - override val OnSecondaryContainer = Color(0xFF332A00) - override val OnTertiaryContainer = Color(0xFF221200) - override val ButtonContrast = Color(0xFFFFC107) - - override val Surface = Color(0xFFFFFAF3) - override val SurfaceVariant = Color(0xFFFFF7E6) - override val OnSurface = Color(0xFF1F1C17) - override val OnSurfaceVariant = Color(0xFF4E4A3C) - - override val Error = Color(0xFFB71C1C) - override val OnError = Color(0xFFFFFFFF) - override val ErrorContainer = Color(0xFFFFDAD6) - override val OnErrorContainer = Color(0xFF410002) - - override val Outline = Color(0xFFD1C8AF) - override val OutlineVariant = Color(0xFFEEE8D7) - override val Background = Color(0xFFFFFCF8) - override val OnBackground = Color(0xFF1F1C17) - } - - companion object { - fun fromName(name: String): ThemeColors = when (name.lowercase()) { - "green" -> Green - "purple" -> Purple - "orange" -> Orange - "pink" -> Pink - "gray" -> Gray - "yellow" -> Yellow - else -> Default - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt deleted file mode 100644 index 707b6109..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt +++ /dev/null @@ -1,538 +0,0 @@ -package com.sukisu.ultra.ui.theme - -import android.content.ContentResolver -import android.content.Context -import android.net.Uri -import android.os.Build -import android.util.Log -import androidx.annotation.RequiresApi -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.updateTransition -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -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.DisposableEffect -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.paint -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.zIndex -import coil.compose.AsyncImagePainter -import coil.compose.rememberAsyncImagePainter -import androidx.compose.foundation.background -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.unit.dp -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream -import androidx.core.content.edit -import androidx.core.net.toUri -import com.sukisu.ultra.ui.util.BackgroundTransformation -import com.sukisu.ultra.ui.util.saveTransformedBackground - -/** - * 主题配置对象,管理应用的主题相关状态 - */ -object ThemeConfig { - var customBackgroundUri by mutableStateOf(null) - var forceDarkMode by mutableStateOf(null) - var currentTheme by mutableStateOf(ThemeColors.Default) - var useDynamicColor by mutableStateOf(false) - var backgroundImageLoaded by mutableStateOf(false) - var needsResetOnThemeChange by mutableStateOf(false) - var isThemeChanging by mutableStateOf(false) - var preventBackgroundRefresh by mutableStateOf(false) - - private var lastDarkModeState: Boolean? = null - fun detectThemeChange(currentDarkMode: Boolean): Boolean { - val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode - lastDarkModeState = currentDarkMode - return isChanged - } - - fun resetBackgroundState() { - if (!preventBackgroundRefresh) { - backgroundImageLoaded = false - } - isThemeChanging = true - } -} - -/** - * 应用主题 - */ -@Composable -fun KernelSUTheme( - darkTheme: Boolean = when(ThemeConfig.forceDarkMode) { - true -> true - false -> false - null -> isSystemInDarkTheme() - }, - dynamicColor: Boolean = ThemeConfig.useDynamicColor, - content: @Composable () -> Unit -) { - val context = LocalContext.current - val systemIsDark = isSystemInDarkTheme() - - // 检测系统主题变化并保存状态 - val themeChanged = ThemeConfig.detectThemeChange(systemIsDark) - LaunchedEffect(systemIsDark, themeChanged) { - if (ThemeConfig.forceDarkMode == null && themeChanged) { - Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark") - ThemeConfig.resetBackgroundState() - - if (!ThemeConfig.preventBackgroundRefresh) { - context.loadCustomBackground() - } - - CardConfig.apply { - load(context) - if (!isCustomAlphaSet) { - cardAlpha = if (systemIsDark) 0.50f else 1f - } - save(context) - } - } - } - - // 初始加载配置 - LaunchedEffect(Unit) { - context.loadThemeMode() - context.loadThemeColors() - context.loadDynamicColorState() - CardConfig.load(context) - - if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) { - context.loadCustomBackground() - ThemeConfig.backgroundImageLoaded = false - } - - ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getBoolean("prevent_background_refresh", true) - } - - // 创建颜色方案 - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context) - } - darkTheme -> createDarkColorScheme() - else -> createLightColorScheme() - } - - // 根据暗色模式和自定义背景调整卡片配置 - val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null - if (darkTheme && !dynamicColor) { - CardConfig.setDarkModeDefaults() - } - CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground) - - val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) } - - LaunchedEffect(ThemeConfig.customBackgroundUri) { - backgroundUri.value = ThemeConfig.customBackgroundUri - } - - val bgImagePainter = backgroundUri.value?.let { - rememberAsyncImagePainter( - model = it, - onError = { - Log.e("ThemeSystem", "背景图加载失败: ${it.result.throwable.message}") - ThemeConfig.customBackgroundUri = null - context.saveCustomBackground(null) - }, - onSuccess = { - Log.d("ThemeSystem", "背景图加载成功") - ThemeConfig.backgroundImageLoaded = true - ThemeConfig.isThemeChanging = false - - ThemeConfig.preventBackgroundRefresh = true - context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { putBoolean("prevent_background_refresh", true) } - } - ) - } - - val transition = updateTransition( - targetState = ThemeConfig.backgroundImageLoaded, - label = "bgTransition" - ) - val bgAlpha by transition.animateFloat( - label = "bgAlpha", - transitionSpec = { - spring( - dampingRatio = 0.8f, - stiffness = 300f - ) - } - ) { loaded -> if (loaded) 1f else 0f } - - DisposableEffect(systemIsDark) { - onDispose { - if (ThemeConfig.isThemeChanging) { - ThemeConfig.isThemeChanging = false - } - } - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography - ) { - Box(modifier = Modifier.fillMaxSize()) { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(-2f) - .background(if (darkTheme) Color.Black else Color.White) - ) - - // 自定义背景层 - backgroundUri.value?.let { uri -> - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(-1f) - .alpha(bgAlpha) - ) { - // 背景图片 - bgImagePainter?.let { painter -> - Box( - modifier = Modifier - .fillMaxSize() - .paint( - painter = painter, - contentScale = ContentScale.Crop - ) - .graphicsLayer { - alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f - } - ) - } - - // 亮度调节层 - Box( - modifier = Modifier - .fillMaxSize() - .background( - if (darkTheme) Color.Black.copy(alpha = 0.6f) - else Color.White.copy(alpha = 0.1f) - ) - ) - - // 边缘渐变遮罩 - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.radialGradient( - colors = listOf( - Color.Transparent, - if (darkTheme) Color.Black.copy(alpha = 0.5f) - else Color.Black.copy(alpha = 0.2f) - ), - radius = 1200f - ) - ) - ) - } - } - - // 内容层 - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(1f) - ) { - content() - } - } - } -} - -/** - * 创建动态深色颜色方案 - */ -@RequiresApi(Build.VERSION_CODES.S) -@Composable -private fun createDynamicDarkColorScheme(context: Context) = - dynamicDarkColorScheme(context).copy( - background = Color.Transparent, - surface = Color.Transparent, - onBackground = Color.White, - onSurface = Color.White - ) - -/** - * 创建动态浅色颜色方案 - */ -@RequiresApi(Build.VERSION_CODES.S) -@Composable -private fun createDynamicLightColorScheme(context: Context) = - dynamicLightColorScheme(context).copy( - background = Color.Transparent, - surface = Color.Transparent - ) - -/** - * 创建深色颜色方案 - */ -@Composable -private fun createDarkColorScheme() = darkColorScheme( - primary = ThemeConfig.currentTheme.Primary.copy(alpha = 0.8f), - onPrimary = Color.White, - primaryContainer = ThemeConfig.currentTheme.PrimaryContainer.copy(alpha = 0.15f), - onPrimaryContainer = Color.White, - secondary = ThemeConfig.currentTheme.Secondary.copy(alpha = 0.8f), - onSecondary = Color.White, - secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer.copy(alpha = 0.15f), - onSecondaryContainer = Color.White, - tertiary = ThemeConfig.currentTheme.Tertiary.copy(alpha = 0.8f), - onTertiary = Color.White, - tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer.copy(alpha = 0.15f), - onTertiaryContainer = Color.White, - background = Color.Transparent, - surface = Color.Transparent, - onBackground = Color.White, - onSurface = Color.White, - surfaceVariant = Color(0xFF2F2F2F), - onSurfaceVariant = Color.White.copy(alpha = 0.7f), - outline = Color.White.copy(alpha = 0.12f), - outlineVariant = Color.White.copy(alpha = 0.12f), - error = ThemeConfig.currentTheme.Error, - onError = ThemeConfig.currentTheme.OnError, - errorContainer = ThemeConfig.currentTheme.ErrorContainer.copy(alpha = 0.15f), - onErrorContainer = Color.White -) - -/** - * 创建浅色颜色方案 - */ -@Composable -private fun createLightColorScheme() = lightColorScheme( - primary = ThemeConfig.currentTheme.Primary, - onPrimary = ThemeConfig.currentTheme.OnPrimary, - primaryContainer = ThemeConfig.currentTheme.PrimaryContainer, - onPrimaryContainer = ThemeConfig.currentTheme.OnPrimaryContainer, - secondary = ThemeConfig.currentTheme.Secondary, - onSecondary = ThemeConfig.currentTheme.OnSecondary, - secondaryContainer = ThemeConfig.currentTheme.SecondaryContainer, - onSecondaryContainer = ThemeConfig.currentTheme.OnSecondaryContainer, - tertiary = ThemeConfig.currentTheme.Tertiary, - onTertiary = ThemeConfig.currentTheme.OnTertiary, - tertiaryContainer = ThemeConfig.currentTheme.TertiaryContainer, - onTertiaryContainer = ThemeConfig.currentTheme.OnTertiaryContainer, - background = Color.Transparent, - surface = Color.Transparent, - onBackground = Color.Black.copy(alpha = 0.87f), - onSurface = Color.Black.copy(alpha = 0.87f), - surfaceVariant = Color(0xFFF5F5F5), - onSurfaceVariant = Color.Black.copy(alpha = 0.78f), - outline = Color.Black.copy(alpha = 0.12f), - outlineVariant = Color.Black.copy(alpha = 0.12f), - error = ThemeConfig.currentTheme.Error, - onError = ThemeConfig.currentTheme.OnError, - errorContainer = ThemeConfig.currentTheme.ErrorContainer, - onErrorContainer = ThemeConfig.currentTheme.OnErrorContainer -) - -/** - * 复制图片到应用内部存储并提升持久性 - */ -private fun Context.copyImageToInternalStorage(uri: Uri): Uri? { - return try { - val contentResolver: ContentResolver = contentResolver - val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null - - val fileName = "custom_background.jpg" - val file = File(filesDir, fileName) - - val backupFile = File(filesDir, "${fileName}.backup") - val outputStream = FileOutputStream(backupFile) - val buffer = ByteArray(4 * 1024) - var read: Int - - while (inputStream.read(buffer).also { read = it } != -1) { - outputStream.write(buffer, 0, read) - } - - outputStream.flush() - outputStream.close() - inputStream.close() - - if (file.exists()) { - file.delete() - } - backupFile.renameTo(file) - - Uri.fromFile(file) - } catch (e: Exception) { - Log.e("ImageCopy", "复制图片失败: ${e.message}") - null - } -} - -/** - * 保存并应用自定义背景 - */ -fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) { - val finalUri = if (transformation != null) { - saveTransformedBackground(uri, transformation) - } else { - copyImageToInternalStorage(uri) - } - - // 保存到配置文件 - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString("custom_background", finalUri?.toString()) - // 设置阻止刷新标志为false,允许新设置的背景加载一次 - putBoolean("prevent_background_refresh", false) - } - - ThemeConfig.customBackgroundUri = finalUri - ThemeConfig.backgroundImageLoaded = false - ThemeConfig.preventBackgroundRefresh = false - CardConfig.cardElevation = 0.dp - CardConfig.isCustomBackgroundEnabled = true -} - -/** - * 保存自定义背景 - */ -fun Context.saveCustomBackground(uri: Uri?) { - val newUri = uri?.let { copyImageToInternalStorage(it) } - - // 保存到配置文件 - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString("custom_background", newUri?.toString()) - if (uri == null) { - // 如果清除背景,也重置阻止刷新标志 - putBoolean("prevent_background_refresh", false) - } else { - // 设置阻止刷新标志为false,允许新设置的背景加载一次 - putBoolean("prevent_background_refresh", false) - } - } - - ThemeConfig.customBackgroundUri = newUri - ThemeConfig.backgroundImageLoaded = false - ThemeConfig.preventBackgroundRefresh = false - - if (uri != null) { - CardConfig.cardElevation = 0.dp - CardConfig.isCustomBackgroundEnabled = true - } -} - -/** - * 加载自定义背景 - */ -fun Context.loadCustomBackground() { - val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getString("custom_background", null) - - val newUri = uriString?.toUri() - val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getBoolean("prevent_background_refresh", false) - - ThemeConfig.preventBackgroundRefresh = preventRefresh - - if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) { - Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh") - ThemeConfig.customBackgroundUri = newUri - ThemeConfig.backgroundImageLoaded = false - } -} - -/** - * 保存主题模式 - */ -fun Context.saveThemeMode(forceDark: Boolean?) { - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString( - "theme_mode", when (forceDark) { - true -> "dark" - false -> "light" - null -> "system" - } - ) - } - ThemeConfig.forceDarkMode = forceDark - ThemeConfig.needsResetOnThemeChange = forceDark == null -} - -/** - * 加载主题模式 - */ -fun Context.loadThemeMode() { - val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getString("theme_mode", "system") - - ThemeConfig.forceDarkMode = when(mode) { - "dark" -> true - "light" -> false - else -> null - } - ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null -} - -/** - * 保存主题颜色 - */ -fun Context.saveThemeColors(themeName: String) { - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString("theme_colors", themeName) - } - - ThemeConfig.currentTheme = ThemeColors.fromName(themeName) -} - -/** - * 加载主题颜色 - */ -fun Context.loadThemeColors() { - val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getString("theme_colors", "default") - - ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default") -} - -/** - * 保存动态颜色状态 - */ -fun Context.saveDynamicColorState(enabled: Boolean) { - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putBoolean("use_dynamic_color", enabled) - } - ThemeConfig.useDynamicColor = enabled -} - -/** - * 加载动态颜色状态 - */ -fun Context.loadDynamicColorState() { - val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getBoolean("use_dynamic_color", true) - - ThemeConfig.useDynamicColor = enabled -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt deleted file mode 100644 index beefa2e2..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Type.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.sukisu.ultra.ui.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp - -val Typography = Typography( - // 大标题 - displayLarge = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 57.sp, - lineHeight = 64.sp, - letterSpacing = (-0.25).sp - ), - displayMedium = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 45.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp - ), - displaySmall = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp - ), - - // 标题 - headlineLarge = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp - ), - headlineMedium = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp - ), - headlineSmall = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.sp - ), - - // 标题栏 - titleLarge = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - titleMedium = TextStyle( - fontWeight = FontWeight.SemiBold, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.15.sp - ), - titleSmall = TextStyle( - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - - // 主体文字 - bodyLarge = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ), - bodyMedium = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp - ), - bodySmall = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp - ), - - // 标签 - labelLarge = TextStyle( - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp - ), - labelMedium = TextStyle( - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ), - labelSmall = TextStyle( - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) -) \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt deleted file mode 100644 index 498d49d4..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.sukisu.ultra.ui.util - -import android.content.ContentResolver -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.Canvas -import android.graphics.Matrix -import android.net.Uri -import android.util.Log -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream -import androidx.core.graphics.createBitmap - -data class BackgroundTransformation( - val scale: Float = 1f, - val offsetX: Float = 0f, - val offsetY: Float = 0f -) - -fun Context.getImageBitmap(uri: Uri): Bitmap? { - return try { - val contentResolver: ContentResolver = contentResolver - val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null - val bitmap = BitmapFactory.decodeStream(inputStream) - inputStream.close() - bitmap - } catch (e: Exception) { - Log.e("BackgroundUtils", "Failed to get image bitmap: ${e.message}") - null - } -} - -fun Context.applyTransformationToBitmap(bitmap: Bitmap, transformation: BackgroundTransformation): Bitmap { - val width = bitmap.width - val height = bitmap.height - - // 创建与屏幕比例相同的目标位图 - val displayMetrics = resources.displayMetrics - val screenWidth = displayMetrics.widthPixels - val screenHeight = displayMetrics.heightPixels - val screenRatio = screenHeight.toFloat() / screenWidth.toFloat() - - // 计算目标宽高 - val targetWidth: Int - val targetHeight: Int - if (width.toFloat() / height.toFloat() > screenRatio) { - targetHeight = height - targetWidth = (height / screenRatio).toInt() - } else { - targetWidth = width - targetHeight = (width * screenRatio).toInt() - } - - // 创建与目标相同大小的位图 - val scaledBitmap = createBitmap(targetWidth, targetHeight) - val canvas = Canvas(scaledBitmap) - - val matrix = Matrix() - - // 确保缩放值有效 - val safeScale = maxOf(0.1f, transformation.scale) - matrix.postScale(safeScale, safeScale) - - // 计算偏移量,确保不会出现负最大值的问题 - val widthDiff = (bitmap.width * safeScale - targetWidth) - val heightDiff = (bitmap.height * safeScale - targetHeight) - - // 安全计算偏移量边界 - val maxOffsetX = maxOf(0f, widthDiff / 2) - val maxOffsetY = maxOf(0f, heightDiff / 2) - - // 限制偏移范围 - val safeOffsetX = if (maxOffsetX > 0) - transformation.offsetX.coerceIn(-maxOffsetX, maxOffsetX) else 0f - val safeOffsetY = if (maxOffsetY > 0) - transformation.offsetY.coerceIn(-maxOffsetY, maxOffsetY) else 0f - - // 应用偏移量到矩阵 - val translationX = -widthDiff / 2 + safeOffsetX - val translationY = -heightDiff / 2 + safeOffsetY - - matrix.postTranslate(translationX, translationY) - - // 将原始位图绘制到新位图上 - canvas.drawBitmap(bitmap, matrix, null) - - return scaledBitmap -} - -fun Context.saveTransformedBackground(uri: Uri, transformation: BackgroundTransformation): Uri? { - try { - val bitmap = getImageBitmap(uri) ?: return null - val transformedBitmap = applyTransformationToBitmap(bitmap, transformation) - - val fileName = "custom_background_transformed.jpg" - val file = File(filesDir, fileName) - val outputStream = FileOutputStream(file) - - transformedBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream) - outputStream.flush() - outputStream.close() - - return Uri.fromFile(file) - } catch (e: Exception) { - Log.e("BackgroundUtils", "Failed to save transformed image: ${e.message}", e) - return null - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/CompositionProvider.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/CompositionProvider.kt deleted file mode 100644 index 1ba64d73..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/CompositionProvider.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sukisu.ultra.ui.util - -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.compositionLocalOf - -val LocalSnackbarHost = compositionLocalOf { - error("CompositionLocal LocalSnackbarController not present") -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/Downloader.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/Downloader.kt deleted file mode 100644 index f17cfe31..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/Downloader.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.sukisu.ultra.ui.util - -import android.annotation.SuppressLint -import android.app.DownloadManager -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.net.Uri -import android.os.Environment -import android.util.Log -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.core.content.ContextCompat -import com.sukisu.ultra.ui.util.module.LatestVersionInfo -import androidx.core.net.toUri - -/** - * @author weishu - * @date 2023/6/22. - */ -@SuppressLint("Range") -fun download( - context: Context, - url: String, - fileName: String, - description: String, - onDownloaded: (Uri) -> Unit = {}, - onDownloading: () -> Unit = {} -) { - 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) - downloadManager.query(query).use { cursor -> - while (cursor.moveToNext()) { - val uri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_URI)) - val localUri = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)) - val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) - val columnTitle = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_TITLE)) - if (url == uri || fileName == columnTitle) { - if (status == DownloadManager.STATUS_RUNNING || status == DownloadManager.STATUS_PENDING) { - onDownloading() - return - } else if (status == DownloadManager.STATUS_SUCCESSFUL) { - onDownloaded(localUri.toUri()) - return - } - } - } - } - - val request = DownloadManager.Request(url.toUri()) - .setDestinationInExternalPublicDir( - Environment.DIRECTORY_DOWNLOADS, - fileName - ) - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - .setMimeType("application/zip") - .setTitle(fileName) - .setDescription(description) - - downloadManager.enqueue(request) -} - -fun checkNewVersion(): LatestVersionInfo { - val url = "https://api.github.com/repos/ShirkNeko/SukiSU-Ultra/releases/latest" - val defaultValue = LatestVersionInfo() - return runCatching { - okhttp3.OkHttpClient().newCall(okhttp3.Request.Builder().url(url).build()).execute() - .use { response -> - if (!response.isSuccessful) { - Log.d("CheckUpdate", "Network request failed: ${response.message}") - return defaultValue - } - val body = response.body?.string() - if (body == null) { - Log.d("CheckUpdate", "Response body is null") - return defaultValue - } - Log.d("CheckUpdate", "Response body: $body") - val json = org.json.JSONObject(body) - - // 直接从 tag_name 提取版本号(如 v1.1) - val tagName = json.optString("tag_name", "") - val versionName = tagName.removePrefix("v") // 移除前缀 "v" - - // 从 body 字段获取更新日志(保留换行符) - val changelog = json.optString("body") - .replace("\\r\\n", "\n") // 转换换行符 - - val assets = json.getJSONArray("assets") - for (i in 0 until assets.length()) { - val asset = assets.getJSONObject(i) - val name = asset.getString("name") - if (!name.endsWith(".apk")) continue - - // 修改正则表达式,只匹配 SukiSU 和版本号 - val regex = Regex("SukiSU.*_(\\d+)-release") - val matchResult = regex.find(name) - if (matchResult == null) { - Log.d("CheckUpdate", "No match found in $name, skipping") - continue - } - val versionCode = matchResult.groupValues[1].toInt() - - val downloadUrl = asset.getString("browser_download_url") - return LatestVersionInfo( - versionCode, - downloadUrl, - changelog, - versionName - ) - } - Log.d("CheckUpdate", "No valid apk asset found, returning default value") - defaultValue - } - }.getOrDefault(defaultValue) -} - - -@Composable -fun DownloadListener(context: Context, onDownloaded: (Uri) -> Unit) { - DisposableEffect(context) { - val receiver = object : BroadcastReceiver() { - @SuppressLint("Range") - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action == DownloadManager.ACTION_DOWNLOAD_COMPLETE) { - val id = intent.getLongExtra( - DownloadManager.EXTRA_DOWNLOAD_ID, -1 - ) - val query = DownloadManager.Query().setFilterById(id) - val downloadManager = - context?.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - val cursor = downloadManager.query(query) - if (cursor.moveToFirst()) { - val status = cursor.getInt( - cursor.getColumnIndex(DownloadManager.COLUMN_STATUS) - ) - if (status == DownloadManager.STATUS_SUCCESSFUL) { - val uri = cursor.getString( - cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI) - ) - onDownloaded(uri.toUri()) - } - } - } - } - } - ContextCompat.registerReceiver( - context, - receiver, - IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), - ContextCompat.RECEIVER_EXPORTED - ) - onDispose { - context.unregisterReceiver(receiver) - } - } -} - diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java deleted file mode 100644 index b7104115..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HanziToPinyin.java +++ /dev/null @@ -1,576 +0,0 @@ -package com.sukisu.ultra.ui.util; -/* - * Copyright (C) 2009 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.text.TextUtils; -import android.util.Log; - -import java.text.Collator; -import java.util.ArrayList; -import java.util.Locale; - -/** - * An object to convert Chinese character to its corresponding pinyin string. For characters with - * multiple possible pinyin string, only one is selected according to collator. Polyphone is not - * supported in this implementation. This class is implemented to achieve the best runtime - * performance and minimum runtime resources with tolerable sacrifice of accuracy. This - * implementation highly depends on zh_CN ICU collation data and must be always synchronized with - * ICU. - *

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

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

- * Each pinyin is corresponding to unihans of same - * offset in the unihans array. - */ - public static final byte[][] PINYINS = { - {65, 0, 0, 0, 0, 0}, {65, 73, 0, 0, 0, 0}, - {65, 78, 0, 0, 0, 0}, {65, 78, 71, 0, 0, 0}, - {65, 79, 0, 0, 0, 0}, {66, 65, 0, 0, 0, 0}, - {66, 65, 73, 0, 0, 0}, {66, 65, 78, 0, 0, 0}, - {66, 65, 78, 71, 0, 0}, {66, 65, 79, 0, 0, 0}, - {66, 69, 73, 0, 0, 0}, {66, 69, 78, 0, 0, 0}, - {66, 69, 78, 71, 0, 0}, {66, 73, 0, 0, 0, 0}, - {66, 73, 65, 78, 0, 0}, {66, 73, 65, 79, 0, 0}, - {66, 73, 69, 0, 0, 0}, {66, 73, 78, 0, 0, 0}, - {66, 73, 78, 71, 0, 0}, {66, 79, 0, 0, 0, 0}, - {66, 85, 0, 0, 0, 0}, {67, 65, 0, 0, 0, 0}, - {67, 65, 73, 0, 0, 0}, {67, 65, 78, 0, 0, 0}, - {67, 65, 78, 71, 0, 0}, {67, 65, 79, 0, 0, 0}, - {67, 69, 0, 0, 0, 0}, {67, 69, 78, 0, 0, 0}, - {67, 69, 78, 71, 0, 0}, {90, 69, 78, 71, 0, 0}, - {67, 69, 78, 71, 0, 0}, {67, 72, 65, 0, 0, 0}, - {67, 72, 65, 73, 0, 0}, {67, 72, 65, 78, 0, 0}, - {67, 72, 65, 78, 71, 0}, {67, 72, 65, 79, 0, 0}, - {67, 72, 69, 0, 0, 0}, {67, 72, 69, 78, 0, 0}, - {83, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 0, 0}, - {67, 72, 69, 78, 71, 0}, {67, 72, 73, 0, 0, 0}, - {67, 72, 79, 78, 71, 0}, {67, 72, 79, 85, 0, 0}, - {67, 72, 85, 0, 0, 0}, {67, 72, 85, 65, 0, 0}, - {67, 72, 85, 65, 73, 0}, {67, 72, 85, 65, 78, 0}, - {67, 72, 85, 65, 78, 71}, {67, 72, 85, 73, 0, 0}, - {67, 72, 85, 78, 0, 0}, {67, 72, 85, 79, 0, 0}, - {67, 73, 0, 0, 0, 0}, {67, 79, 78, 71, 0, 0}, - {67, 79, 85, 0, 0, 0}, {67, 85, 0, 0, 0, 0}, - {67, 85, 65, 78, 0, 0}, {67, 85, 73, 0, 0, 0}, - {67, 85, 78, 0, 0, 0}, {67, 85, 79, 0, 0, 0}, - {68, 65, 0, 0, 0, 0}, {68, 65, 73, 0, 0, 0}, - {68, 65, 78, 0, 0, 0}, {68, 65, 78, 71, 0, 0}, - {68, 65, 79, 0, 0, 0}, {68, 69, 0, 0, 0, 0}, - {68, 69, 78, 0, 0, 0}, {68, 69, 78, 71, 0, 0}, - {68, 73, 0, 0, 0, 0}, {68, 73, 65, 0, 0, 0}, - {68, 73, 65, 78, 0, 0}, {68, 73, 65, 79, 0, 0}, - {68, 73, 69, 0, 0, 0}, {68, 73, 78, 71, 0, 0}, - {68, 73, 85, 0, 0, 0}, {68, 79, 78, 71, 0, 0}, - {68, 79, 85, 0, 0, 0}, {68, 85, 0, 0, 0, 0}, - {68, 85, 65, 78, 0, 0}, {68, 85, 73, 0, 0, 0}, - {68, 85, 78, 0, 0, 0}, {68, 85, 79, 0, 0, 0}, - {69, 0, 0, 0, 0, 0}, {69, 73, 0, 0, 0, 0}, - {69, 78, 0, 0, 0, 0}, {69, 78, 71, 0, 0, 0}, - {69, 82, 0, 0, 0, 0}, {70, 65, 0, 0, 0, 0}, - {70, 65, 78, 0, 0, 0}, {70, 65, 78, 71, 0, 0}, - {70, 69, 73, 0, 0, 0}, {70, 69, 78, 0, 0, 0}, - {70, 69, 78, 71, 0, 0}, {70, 73, 65, 79, 0, 0}, - {70, 79, 0, 0, 0, 0}, {70, 79, 85, 0, 0, 0}, - {70, 85, 0, 0, 0, 0}, {71, 65, 0, 0, 0, 0}, - {71, 65, 73, 0, 0, 0}, {71, 65, 78, 0, 0, 0}, - {71, 65, 78, 71, 0, 0}, {71, 65, 79, 0, 0, 0}, - {71, 69, 0, 0, 0, 0}, {71, 69, 73, 0, 0, 0}, - {71, 69, 78, 0, 0, 0}, {71, 69, 78, 71, 0, 0}, - {71, 79, 78, 71, 0, 0}, {71, 79, 85, 0, 0, 0}, - {71, 85, 0, 0, 0, 0}, {71, 85, 65, 0, 0, 0}, - {71, 85, 65, 73, 0, 0}, {71, 85, 65, 78, 0, 0}, - {71, 85, 65, 78, 71, 0}, {71, 85, 73, 0, 0, 0}, - {71, 85, 78, 0, 0, 0}, {71, 85, 79, 0, 0, 0}, - {72, 65, 0, 0, 0, 0}, {72, 65, 73, 0, 0, 0}, - {72, 65, 78, 0, 0, 0}, {72, 65, 78, 71, 0, 0}, - {72, 65, 79, 0, 0, 0}, {72, 69, 0, 0, 0, 0}, - {72, 69, 73, 0, 0, 0}, {72, 69, 78, 0, 0, 0}, - {72, 69, 78, 71, 0, 0}, {72, 77, 0, 0, 0, 0}, - {72, 79, 78, 71, 0, 0}, {72, 79, 85, 0, 0, 0}, - {72, 85, 0, 0, 0, 0}, {72, 85, 65, 0, 0, 0}, - {72, 85, 65, 73, 0, 0}, {72, 85, 65, 78, 0, 0}, - {72, 85, 65, 78, 71, 0}, {72, 85, 73, 0, 0, 0}, - {72, 85, 78, 0, 0, 0}, {72, 85, 79, 0, 0, 0}, - {74, 73, 0, 0, 0, 0}, {74, 73, 65, 0, 0, 0}, - {74, 73, 65, 78, 0, 0}, {74, 73, 65, 78, 71, 0}, - {74, 73, 65, 79, 0, 0}, {74, 73, 69, 0, 0, 0}, - {74, 73, 78, 0, 0, 0}, {74, 73, 78, 71, 0, 0}, - {74, 73, 79, 78, 71, 0}, {74, 73, 85, 0, 0, 0}, - {74, 85, 0, 0, 0, 0}, {74, 85, 65, 78, 0, 0}, - {74, 85, 69, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, - {75, 65, 0, 0, 0, 0}, {75, 65, 73, 0, 0, 0}, - {75, 65, 78, 0, 0, 0}, {75, 65, 78, 71, 0, 0}, - {75, 65, 79, 0, 0, 0}, {75, 69, 0, 0, 0, 0}, - {75, 69, 78, 0, 0, 0}, {75, 69, 78, 71, 0, 0}, - {75, 79, 78, 71, 0, 0}, {75, 79, 85, 0, 0, 0}, - {75, 85, 0, 0, 0, 0}, {75, 85, 65, 0, 0, 0}, - {75, 85, 65, 73, 0, 0}, {75, 85, 65, 78, 0, 0}, - {75, 85, 65, 78, 71, 0}, {75, 85, 73, 0, 0, 0}, - {75, 85, 78, 0, 0, 0}, {75, 85, 79, 0, 0, 0}, - {76, 65, 0, 0, 0, 0}, {76, 65, 73, 0, 0, 0}, - {76, 65, 78, 0, 0, 0}, {76, 65, 78, 71, 0, 0}, - {76, 65, 79, 0, 0, 0}, {76, 69, 0, 0, 0, 0}, - {76, 69, 73, 0, 0, 0}, {76, 69, 78, 71, 0, 0}, - {76, 73, 0, 0, 0, 0}, {76, 73, 65, 0, 0, 0}, - {76, 73, 65, 78, 0, 0}, {76, 73, 65, 78, 71, 0}, - {76, 73, 65, 79, 0, 0}, {76, 73, 69, 0, 0, 0}, - {76, 73, 78, 0, 0, 0}, {76, 73, 78, 71, 0, 0}, - {76, 73, 85, 0, 0, 0}, {76, 79, 0, 0, 0, 0}, - {76, 79, 78, 71, 0, 0}, {76, 79, 85, 0, 0, 0}, - {76, 85, 0, 0, 0, 0}, {76, 85, 65, 78, 0, 0}, - {76, 85, 69, 0, 0, 0}, {76, 85, 78, 0, 0, 0}, - {76, 85, 79, 0, 0, 0}, {77, 0, 0, 0, 0, 0}, - {77, 65, 0, 0, 0, 0}, {77, 65, 73, 0, 0, 0}, - {77, 65, 78, 0, 0, 0}, {77, 65, 78, 71, 0, 0}, - {77, 65, 79, 0, 0, 0}, {77, 69, 0, 0, 0, 0}, - {77, 69, 73, 0, 0, 0}, {77, 69, 78, 0, 0, 0}, - {77, 69, 78, 71, 0, 0}, {77, 73, 0, 0, 0, 0}, - {77, 73, 65, 78, 0, 0}, {77, 73, 65, 79, 0, 0}, - {77, 73, 69, 0, 0, 0}, {77, 73, 78, 0, 0, 0}, - {77, 73, 78, 71, 0, 0}, {77, 73, 85, 0, 0, 0}, - {77, 79, 0, 0, 0, 0}, {77, 79, 85, 0, 0, 0}, - {77, 85, 0, 0, 0, 0}, {78, 0, 0, 0, 0, 0}, - {78, 65, 0, 0, 0, 0}, {78, 65, 73, 0, 0, 0}, - {78, 65, 78, 0, 0, 0}, {78, 65, 78, 71, 0, 0}, - {78, 65, 79, 0, 0, 0}, {78, 69, 0, 0, 0, 0}, - {78, 69, 73, 0, 0, 0}, {78, 69, 78, 0, 0, 0}, - {78, 69, 78, 71, 0, 0}, {78, 73, 0, 0, 0, 0}, - {78, 73, 65, 78, 0, 0}, {78, 73, 65, 78, 71, 0}, - {78, 73, 65, 79, 0, 0}, {78, 73, 69, 0, 0, 0}, - {78, 73, 78, 0, 0, 0}, {78, 73, 78, 71, 0, 0}, - {78, 73, 85, 0, 0, 0}, {78, 79, 78, 71, 0, 0}, - {78, 79, 85, 0, 0, 0}, {78, 85, 0, 0, 0, 0}, - {78, 85, 65, 78, 0, 0}, {78, 85, 69, 0, 0, 0}, - {78, 85, 78, 0, 0, 0}, {78, 85, 79, 0, 0, 0}, - {79, 0, 0, 0, 0, 0}, {79, 85, 0, 0, 0, 0}, - {80, 65, 0, 0, 0, 0}, {80, 65, 73, 0, 0, 0}, - {80, 65, 78, 0, 0, 0}, {80, 65, 78, 71, 0, 0}, - {80, 65, 79, 0, 0, 0}, {80, 69, 73, 0, 0, 0}, - {80, 69, 78, 0, 0, 0}, {80, 69, 78, 71, 0, 0}, - {80, 73, 0, 0, 0, 0}, {80, 73, 65, 78, 0, 0}, - {80, 73, 65, 79, 0, 0}, {80, 73, 69, 0, 0, 0}, - {80, 73, 78, 0, 0, 0}, {80, 73, 78, 71, 0, 0}, - {80, 79, 0, 0, 0, 0}, {80, 79, 85, 0, 0, 0}, - {80, 85, 0, 0, 0, 0}, {81, 73, 0, 0, 0, 0}, - {81, 73, 65, 0, 0, 0}, {81, 73, 65, 78, 0, 0}, - {81, 73, 65, 78, 71, 0}, {81, 73, 65, 79, 0, 0}, - {81, 73, 69, 0, 0, 0}, {81, 73, 78, 0, 0, 0}, - {81, 73, 78, 71, 0, 0}, {81, 73, 79, 78, 71, 0}, - {81, 73, 85, 0, 0, 0}, {81, 85, 0, 0, 0, 0}, - {81, 85, 65, 78, 0, 0}, {81, 85, 69, 0, 0, 0}, - {81, 85, 78, 0, 0, 0}, {82, 65, 78, 0, 0, 0}, - {82, 65, 78, 71, 0, 0}, {82, 65, 79, 0, 0, 0}, - {82, 69, 0, 0, 0, 0}, {82, 69, 78, 0, 0, 0}, - {82, 69, 78, 71, 0, 0}, {82, 73, 0, 0, 0, 0}, - {82, 79, 78, 71, 0, 0}, {82, 79, 85, 0, 0, 0}, - {82, 85, 0, 0, 0, 0}, {82, 85, 65, 0, 0, 0}, - {82, 85, 65, 78, 0, 0}, {82, 85, 73, 0, 0, 0}, - {82, 85, 78, 0, 0, 0}, {82, 85, 79, 0, 0, 0}, - {83, 65, 0, 0, 0, 0}, {83, 65, 73, 0, 0, 0}, - {83, 65, 78, 0, 0, 0}, {83, 65, 78, 71, 0, 0}, - {83, 65, 79, 0, 0, 0}, {83, 69, 0, 0, 0, 0}, - {83, 69, 78, 0, 0, 0}, {83, 69, 78, 71, 0, 0}, - {83, 72, 65, 0, 0, 0}, {83, 72, 65, 73, 0, 0}, - {83, 72, 65, 78, 0, 0}, {83, 72, 65, 78, 71, 0}, - {83, 72, 65, 79, 0, 0}, {83, 72, 69, 0, 0, 0}, - {83, 72, 69, 78, 0, 0}, {88, 73, 78, 0, 0, 0}, - {83, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 71, 0}, - {83, 72, 73, 0, 0, 0}, {83, 72, 79, 85, 0, 0}, - {83, 72, 85, 0, 0, 0}, {83, 72, 85, 65, 0, 0}, - {83, 72, 85, 65, 73, 0}, {83, 72, 85, 65, 78, 0}, - {83, 72, 85, 65, 78, 71}, {83, 72, 85, 73, 0, 0}, - {83, 72, 85, 78, 0, 0}, {83, 72, 85, 79, 0, 0}, - {83, 73, 0, 0, 0, 0}, {83, 79, 78, 71, 0, 0}, - {83, 79, 85, 0, 0, 0}, {83, 85, 0, 0, 0, 0}, - {83, 85, 65, 78, 0, 0}, {83, 85, 73, 0, 0, 0}, - {83, 85, 78, 0, 0, 0}, {83, 85, 79, 0, 0, 0}, - {84, 65, 0, 0, 0, 0}, {84, 65, 73, 0, 0, 0}, - {84, 65, 78, 0, 0, 0}, {84, 65, 78, 71, 0, 0}, - {84, 65, 79, 0, 0, 0}, {84, 69, 0, 0, 0, 0}, - {84, 69, 78, 71, 0, 0}, {84, 73, 0, 0, 0, 0}, - {84, 73, 65, 78, 0, 0}, {84, 73, 65, 79, 0, 0}, - {84, 73, 69, 0, 0, 0}, {84, 73, 78, 71, 0, 0}, - {84, 79, 78, 71, 0, 0}, {84, 79, 85, 0, 0, 0}, - {84, 85, 0, 0, 0, 0}, {84, 85, 65, 78, 0, 0}, - {84, 85, 73, 0, 0, 0}, {84, 85, 78, 0, 0, 0}, - {84, 85, 79, 0, 0, 0}, {87, 65, 0, 0, 0, 0}, - {87, 65, 73, 0, 0, 0}, {87, 65, 78, 0, 0, 0}, - {87, 65, 78, 71, 0, 0}, {87, 69, 73, 0, 0, 0}, - {87, 69, 78, 0, 0, 0}, {87, 69, 78, 71, 0, 0}, - {87, 79, 0, 0, 0, 0}, {87, 85, 0, 0, 0, 0}, - {88, 73, 0, 0, 0, 0}, {88, 73, 65, 0, 0, 0}, - {88, 73, 65, 78, 0, 0}, {88, 73, 65, 78, 71, 0}, - {88, 73, 65, 79, 0, 0}, {88, 73, 69, 0, 0, 0}, - {88, 73, 78, 0, 0, 0}, {88, 73, 78, 71, 0, 0}, - {88, 73, 79, 78, 71, 0}, {88, 73, 85, 0, 0, 0}, - {88, 85, 0, 0, 0, 0}, {88, 85, 65, 78, 0, 0}, - {88, 85, 69, 0, 0, 0}, {88, 85, 78, 0, 0, 0}, - {89, 65, 0, 0, 0, 0}, {89, 65, 78, 0, 0, 0}, - {89, 65, 78, 71, 0, 0}, {89, 65, 79, 0, 0, 0}, - {89, 69, 0, 0, 0, 0}, {89, 73, 0, 0, 0, 0}, - {89, 73, 78, 0, 0, 0}, {89, 73, 78, 71, 0, 0}, - {89, 79, 0, 0, 0, 0}, {89, 79, 78, 71, 0, 0}, - {89, 79, 85, 0, 0, 0}, {89, 85, 0, 0, 0, 0}, - {89, 85, 65, 78, 0, 0}, {89, 85, 69, 0, 0, 0}, - {89, 85, 78, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, - {89, 85, 78, 0, 0, 0}, {90, 65, 0, 0, 0, 0}, - {90, 65, 73, 0, 0, 0}, {90, 65, 78, 0, 0, 0}, - {90, 65, 78, 71, 0, 0}, {90, 65, 79, 0, 0, 0}, - {90, 69, 0, 0, 0, 0}, {90, 69, 73, 0, 0, 0}, - {90, 69, 78, 0, 0, 0}, {90, 69, 78, 71, 0, 0}, - {90, 72, 65, 0, 0, 0}, {90, 72, 65, 73, 0, 0}, - {90, 72, 65, 78, 0, 0}, {90, 72, 65, 78, 71, 0}, - {67, 72, 65, 78, 71, 0}, {90, 72, 65, 78, 71, 0}, - {90, 72, 65, 79, 0, 0}, {90, 72, 69, 0, 0, 0}, - {90, 72, 69, 78, 0, 0}, {90, 72, 69, 78, 71, 0}, - {90, 72, 73, 0, 0, 0}, {83, 72, 73, 0, 0, 0}, - {90, 72, 73, 0, 0, 0}, {90, 72, 79, 78, 71, 0}, - {90, 72, 79, 85, 0, 0}, {90, 72, 85, 0, 0, 0}, - {90, 72, 85, 65, 0, 0}, {90, 72, 85, 65, 73, 0}, - {90, 72, 85, 65, 78, 0}, {90, 72, 85, 65, 78, 71}, - {90, 72, 85, 73, 0, 0}, {90, 72, 85, 78, 0, 0}, - {90, 72, 85, 79, 0, 0}, {90, 73, 0, 0, 0, 0}, - {90, 79, 78, 71, 0, 0}, {90, 79, 85, 0, 0, 0}, - {90, 85, 0, 0, 0, 0}, {90, 85, 65, 78, 0, 0}, - {90, 85, 73, 0, 0, 0}, {90, 85, 78, 0, 0, 0}, - {90, 85, 79, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, - {83, 72, 65, 78, 0, 0}, {0, 0, 0, 0, 0, 0},}; - - /** - * First and last Chinese character with known Pinyin according to zh collation - */ - private static final String FIRST_PINYIN_UNIHAN = "\u963F"; - private static final String LAST_PINYIN_UNIHAN = "\u9FFF"; - - private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA); - - private static HanziToPinyin sInstance; - private final boolean mHasChinaCollator; - - public static class Token { - /** - * Separator between target string for each source char - */ - public static final String SEPARATOR = " "; - - public static final int LATIN = 1; - public static final int PINYIN = 2; - public static final int UNKNOWN = 3; - - public Token() { - } - - public Token(int type, String source, String target) { - this.type = type; - this.source = source; - this.target = target; - } - - /** - * Type of this token, ASCII, PINYIN or UNKNOWN. - */ - public int type; - /** - * Original string before translation. - */ - public String source; - /** - * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is - * original string in source. - */ - public String target; - } - - protected HanziToPinyin(boolean hasChinaCollator) { - mHasChinaCollator = hasChinaCollator; - } - - public static HanziToPinyin getInstance() { - synchronized (HanziToPinyin.class) { - if (sInstance != null) { - return sInstance; - } - // Check if zh_CN collation data is available - final Locale[] locale = Collator.getAvailableLocales(); - for (Locale value : locale) { - if (value.equals(Locale.CHINA) || value.getLanguage().contains("zh")) { - // Do self validation just once. - if (DEBUG) { - Log.d(TAG, "Self validation. Result: " + doSelfValidation()); - } - sInstance = new HanziToPinyin(true); - return sInstance; - } - } - if (sInstance == null){//这个判断是用于处理国产ROM的兼容性问题 - if (Locale.CHINA.equals(Locale.getDefault())){ - sInstance = new HanziToPinyin(true); - return sInstance; - } - } - Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled"); - sInstance = new HanziToPinyin(false); - return sInstance; - } - } - - /** - * Validate if our internal table has some wrong value. - * - * @return true when the table looks correct. - */ - private static boolean doSelfValidation() { - char lastChar = UNIHANS[0]; - String lastString = Character.toString(lastChar); - for (char c : UNIHANS) { - if (lastChar == c) { - continue; - } - final String curString = Character.toString(c); - int cmp = COLLATOR.compare(lastString, curString); - if (cmp >= 0) { - Log.e(TAG, "Internal error in Unihan table. " + "The last string \"" + lastString - + "\" is greater than current string \"" + curString + "\"."); - return false; - } - lastString = curString; - } - return true; - } - - private Token getToken(char character) { - Token token = new Token(); - final String letter = Character.toString(character); - token.source = letter; - int offset = -1; - int cmp; - if (character < 256) { - token.type = Token.LATIN; - token.target = letter; - return token; - } else { - cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN); - if (cmp < 0) { - token.type = Token.UNKNOWN; - token.target = letter; - return token; - } else if (cmp == 0) { - token.type = Token.PINYIN; - offset = 0; - } else { - cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN); - if (cmp > 0) { - token.type = Token.UNKNOWN; - token.target = letter; - return token; - } else if (cmp == 0) { - token.type = Token.PINYIN; - offset = UNIHANS.length - 1; - } - } - } - - token.type = Token.PINYIN; - if (offset < 0) { - int begin = 0; - int end = UNIHANS.length - 1; - while (begin <= end) { - offset = (begin + end) / 2; - final String unihan = Character.toString(UNIHANS[offset]); - cmp = COLLATOR.compare(letter, unihan); - if (cmp == 0) { - break; - } else if (cmp > 0) { - begin = offset + 1; - } else { - end = offset - 1; - } - } - } - if (cmp < 0) { - offset--; - } - StringBuilder pinyin = new StringBuilder(); - for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) { - pinyin.append((char) PINYINS[offset][j]); - } - token.target = pinyin.toString(); - if (TextUtils.isEmpty(token.target)) { - token.type = Token.UNKNOWN; - token.target = token.source; - } - return token; - } - - /** - * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without - * space will be put into a Token, One Hanzi character which has pinyin will be treated as a - * Token. If these is no China collator, the empty token array is returned. - */ - public ArrayList get(final String input) { - ArrayList tokens = new ArrayList<>(); - if (!mHasChinaCollator || TextUtils.isEmpty(input)) { - // return empty tokens. - return tokens; - } - final int inputLength = input.length(); - final StringBuilder sb = new StringBuilder(); - int tokenType = Token.LATIN; - // Go through the input, create a new token when - // a. Token type changed - // b. Get the Pinyin of current charater. - // c. current character is space. - for (int i = 0; i < inputLength; i++) { - final char character = input.charAt(i); - if (character == ' ') { - if (sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - } else if (character < 256) { - if (tokenType != Token.LATIN && sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - tokenType = Token.LATIN; - sb.append(character); - } else { - Token t = getToken(character); - if (t.type == Token.PINYIN) { - if (sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - tokens.add(t); - tokenType = Token.PINYIN; - } else { - if (tokenType != t.type && sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - tokenType = t.type; - sb.append(character); - } - } - } - if (sb.length() > 0) { - addToken(sb, tokens, tokenType); - } - return tokens; - } - - private void addToken( - final StringBuilder sb, final ArrayList tokens, final int tokenType) { - String str = sb.toString(); - tokens.add(new Token(tokenType, str, str)); - sb.setLength(0); - } - - public String toPinyinString(String string) { - if (string == null) { - return null; - } - StringBuilder sb = new StringBuilder(); - ArrayList tokens = get(string); - for (Token token : tokens) { - sb.append(token.target); - } - return sb.toString().toLowerCase(); - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HyperlinkText.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/HyperlinkText.kt deleted file mode 100644 index 299975ea..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/HyperlinkText.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.sukisu.ultra.ui.util - -import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextDecoration -import java.util.regex.Pattern - -@Composable -fun LinkifyText( - text: String, - modifier: Modifier = Modifier -) { - val uriHandler = LocalUriHandler.current - val layoutResult = remember { - mutableStateOf(null) - } - val linksList = extractUrls(text) - val annotatedString = buildAnnotatedString { - append(text) - linksList.forEach { - addStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ), - start = it.start, - end = it.end - ) - addStringAnnotation( - tag = "URL", - annotation = it.url, - start = it.start, - end = it.end - ) - } - } - Text( - text = annotatedString, - modifier = modifier.pointerInput(Unit) { - detectTapGestures { offsetPosition -> - layoutResult.value?.let { - val position = it.getOffsetForPosition(offsetPosition) - annotatedString.getStringAnnotations(position, position).firstOrNull() - ?.let { result -> - if (result.tag == "URL") { - uriHandler.openUri(result.item) - } - } - } - } - }, - onTextLayout = { layoutResult.value = it } - ) -} - -private val urlPattern: Pattern = Pattern.compile( - "(?:^|[\\W])((ht|f)tp(s?):\\/\\/|www\\.)" - + "(([\\w\\-]+\\.){1,}?([\\w\\-.~]+\\/?)*" - + "[\\p{Alnum}.,%_=?&#\\-+()\\[\\]\\*$~@!:/{};']*)", - Pattern.CASE_INSENSITIVE or Pattern.MULTILINE or Pattern.DOTALL -) - -private data class LinkInfo( - val url: String, - val start: Int, - val end: Int -) - -private fun extractUrls(text: String): List = buildList { - val matcher = urlPattern.matcher(text) - while (matcher.find()) { - val matchStart = matcher.start(1) - val matchEnd = matcher.end() - val url = text.substring(matchStart, matchEnd).replaceFirst("http://", "https://") - add(LinkInfo(url, matchStart, matchEnd)) - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt deleted file mode 100644 index bec86ed8..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt +++ /dev/null @@ -1,550 +0,0 @@ -package com.sukisu.ultra.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 com.sukisu.ultra.BuildConfig -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ksuApp -import org.json.JSONArray -import java.io.File - - -/** - * @author weishu - * @date 2023/1/1. - */ -private const val TAG = "KsuCli" - -private fun getKsuDaemonPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozako.so" -} - -object KsuCli { - val SHELL: Shell = createRootShell() - val GLOBAL_MNT_SHELL: Shell = createRootShell(true) -} - -fun getRootShell(globalMnt: Boolean = false): Shell { - return if (globalMnt) KsuCli.GLOBAL_MNT_SHELL else { - KsuCli.SHELL - } -} - -inline fun withNewRootShell( - globalMnt: Boolean = false, - block: Shell.() -> T -): T { - return createRootShell(globalMnt).use(block) -} - -fun Uri.getFileName(context: Context): String? { - var fileName: String? = null - val contentResolver: ContentResolver = context.contentResolver - val cursor: Cursor? = contentResolver.query(this, null, null, null, null) - cursor?.use { - if (it.moveToFirst()) { - fileName = it.getString(it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) - } - } - return fileName -} - -fun createRootShell(globalMnt: Boolean = false): Shell { - Shell.enableVerboseLogging = BuildConfig.DEBUG - val builder = Shell.Builder.create() - return try { - if (globalMnt) { - builder.build(getKsuDaemonPath(), "debug", "su", "-g") - } else { - builder.build(getKsuDaemonPath(), "debug", "su") - } - } catch (e: Throwable) { - Log.w(TAG, "ksu failed: ", e) - try { - if (globalMnt) { - builder.build("su") - } else { - builder.build("su", "-mm") - } - } catch (e: Throwable) { - Log.e(TAG, "su failed: ", e) - builder.build("sh") - } - } -} - -fun execKsud(args: String, newShell: Boolean = false): Boolean { - return if (newShell) { - withNewRootShell { - ShellUtils.fastCmdResult(this, "${getKsuDaemonPath()} $args") - } - } else { - ShellUtils.fastCmdResult(getRootShell(), "${getKsuDaemonPath()} $args") - } -} - -fun install() { - val start = SystemClock.elapsedRealtime() - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so").absolutePath - val result = execKsud("install --magiskboot $magiskboot", true) - Log.w(TAG, "install result: $result, cost: ${SystemClock.elapsedRealtime() - start}ms") -} - -fun listModules(): String { - val shell = getRootShell() - - val out = - shell.newJob().add("${getKsuDaemonPath()} module list").to(ArrayList(), null).exec().out - return out.joinToString("\n").ifBlank { "[]" } -} - -fun getModuleCount(): Int { - val result = listModules() - runCatching { - val array = JSONArray(result) - return array.length() - }.getOrElse { return 0 } -} - -fun getSuperuserCount(): Int { - return Natives.allowList.size -} - -fun toggleModule(id: String, enable: Boolean): Boolean { - val cmd = if (enable) { - "module enable $id" - } else { - "module disable $id" - } - val result = execKsud(cmd, true) - Log.i(TAG, "$cmd result: $result") - return result -} - -fun uninstallModule(id: String): Boolean { - val cmd = "module uninstall $id" - val result = execKsud(cmd, true) - Log.i(TAG, "uninstall module $id result: $result") - return result -} - -fun restoreModule(id: String): Boolean { - val cmd = "module restore $id" - val result = execKsud(cmd, true) - Log.i(TAG, "restore module $id result: $result") - return result -} - -private fun flashWithIO( - cmd: String, - onStdout: (String) -> Unit, - onStderr: (String) -> Unit -): Shell.Result { - - val stdoutCallback: CallbackList = object : CallbackList() { - override fun onAddElement(s: String?) { - onStdout(s ?: "") - } - } - - val stderrCallback: CallbackList = object : CallbackList() { - override fun onAddElement(s: String?) { - onStderr(s ?: "") - } - } - - return withNewRootShell { - newJob().add(cmd).to(stdoutCallback, stderrCallback).exec() - } -} - -fun flashModule( - uri: Uri, - 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") - file.outputStream().use { output -> - this?.copyTo(output) - } - val cmd = "module install ${file.absolutePath}" - val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr) - Log.i("KernelSU", "install module $uri result: $result") - - file.delete() - - onFinish(result.isSuccess, result.code) - return result.isSuccess - } -} - -fun runModuleAction( - moduleId: String, onStdout: (String) -> Unit, onStderr: (String) -> Unit -): Boolean { - val shell = createRootShell(true) - - val stdoutCallback: CallbackList = object : CallbackList() { - override fun onAddElement(s: String?) { - onStdout(s ?: "") - } - } - - val stderrCallback: CallbackList = object : CallbackList() { - override fun onAddElement(s: String?) { - onStderr(s ?: "") - } - } - - val result = shell.newJob().add("${getKsuDaemonPath()} module action $moduleId") - .to(stdoutCallback, stderrCallback).exec() - Log.i("KernelSU", "Module runAction result: $result") - - return result.isSuccess -} - -fun restoreBoot( - onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit -): Boolean { - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.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, "libzakoboot.so") - val result = flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr) - onFinish(result.isSuccess, result.code) - return result.isSuccess -} - -@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 bootFile = bootUri?.let { uri -> - with(resolver.openInputStream(uri)) { - val bootFile = File(ksuApp.cacheDir, "boot.img") - bootFile.outputStream().use { output -> - this?.copyTo(output) - } - - bootFile - } - } - - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") - var cmd = "boot-patch --magiskboot ${magiskboot.absolutePath}" - - cmd += if (bootFile == null) { - // no boot.img, use -f to force install - " -f" - } else { - " -b ${bootFile.absolutePath}" - } - - 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 = "") { - val shell = getRootShell() - if (reason == "recovery") { - // KEYCODE_POWER = 26, hide incorrect "Factory data reset" message - ShellUtils.fastCmd(shell, "/system/bin/input keyevent 26") - } - ShellUtils.fastCmd(shell, "/system/bin/svc power reboot $reason || /system/bin/reboot $reason") -} - -fun rootAvailable(): Boolean { - val shell = getRootShell() - return shell.isRoot -} - -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 hasMagisk(): Boolean { - val shell = getRootShell(true) - val result = shell.newJob().add("which magisk").exec() - Log.i(TAG, "has magisk: ${result.isSuccess}") - return result.isSuccess -} - -fun isSepolicyValid(rules: String?): Boolean { - if (rules == null) { - return true - } - val shell = getRootShell() - val result = - shell.newJob().add("${getKsuDaemonPath()} sepolicy check '$rules'").to(ArrayList(), null) - .exec() - return result.isSuccess -} - -fun getSepolicy(pkg: String): String { - val shell = getRootShell() - val result = - shell.newJob().add("${getKsuDaemonPath()} profile get-sepolicy $pkg").to(ArrayList(), null) - .exec() - Log.i(TAG, "code: ${result.code}, out: ${result.out}, err: ${result.err}") - return result.out.joinToString("\n") -} - -fun setSepolicy(pkg: String, rules: String): Boolean { - val shell = getRootShell() - val result = shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'") - .to(ArrayList(), null).exec() - Log.i(TAG, "set sepolicy result: ${result.code}") - return result.isSuccess -} - -fun listAppProfileTemplates(): List { - val shell = getRootShell() - return shell.newJob().add("${getKsuDaemonPath()} profile list-templates").to(ArrayList(), null) - .exec().out -} - -fun getAppProfileTemplate(id: String): String { - val shell = getRootShell() - return shell.newJob().add("${getKsuDaemonPath()} profile get-template '${id}'") - .to(ArrayList(), null).exec().out.joinToString("\n") -} - -fun setAppProfileTemplate(id: String, template: String): Boolean { - val shell = getRootShell() - val escapedTemplate = template.replace("\"", "\\\"") - val cmd = """${getKsuDaemonPath()} profile set-template "$id" "$escapedTemplate'"""" - return shell.newJob().add(cmd) - .to(ArrayList(), null).exec().isSuccess -} - -fun deleteAppProfileTemplate(id: String): Boolean { - val shell = getRootShell() - return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'") - .to(ArrayList(), null).exec().isSuccess -} - -fun forceStopApp(packageName: String) { - val shell = getRootShell() - val result = shell.newJob().add("am force-stop $packageName").exec() - Log.i(TAG, "force stop $packageName result: $result") -} - -fun launchApp(packageName: String) { - - val shell = getRootShell() - 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) -} - -fun getSuSFSDaemonPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozakozako.so" -} - -fun getSuSFS(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} support") - return result -} - -fun getSuSFSVersion(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} version") - return result -} - -fun getSuSFSVariant(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} variant") - return result -} -fun getSuSFSFeatures(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} features") - return result -} - -fun susfsSUS_SU_0(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su 0") - return result -} - -fun susfsSUS_SU_2(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su 2") - return result -} - -fun susfsSUS_SU_Mode(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su mode") - return result -} - -fun getKpmmgrPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so" -} - - -fun loadKpmModule(path: String, args: String? = null): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}" - return ShellUtils.fastCmd(shell, cmd) -} - -fun unloadKpmModule(name: String): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} unload $name" - return ShellUtils.fastCmd(shell, cmd) -} - -fun getKpmModuleCount(): Int { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} num" - val result = ShellUtils.fastCmd(shell, cmd) - return result.trim().toIntOrNull() ?: 0 -} - -fun runCmd(shell : Shell, cmd : String) : String { - return shell.newJob() - .add(cmd) - .to(mutableListOf(), null) - .exec().out - .joinToString("\n") -} - -fun listKpmModules(): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} list" - return try { - runCmd(shell, cmd).trim() - } catch (e: Exception) { - Log.e(TAG, "Failed to list KPM modules", e) - "" - } -} - -fun getKpmModuleInfo(name: String): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} info $name" - return try { - runCmd(shell, cmd).trim() - } catch (e: Exception) { - Log.e(TAG, "Failed to get KPM module info: $name", e) - "" - } -} - -fun controlKpmModule(name: String, args: String? = null): Int { - val shell = getRootShell() - val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}"""" - val result = runCmd(shell, cmd) - return result.trim().toIntOrNull() ?: -1 -} - -fun getKpmVersion(): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} version" - val result = ShellUtils.fastCmd(shell, cmd) - return result.trim() -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/LogEvent.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/LogEvent.kt deleted file mode 100644 index 758cc2b8..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/LogEvent.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.sukisu.ultra.ui.util - -import android.content.Context -import android.os.Build -import android.system.Os -import com.topjohnwu.superuser.ShellUtils -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ui.screen.getManagerVersion -import java.io.File -import java.io.FileWriter -import java.io.PrintWriter -import java.time.LocalDateTime -import java.time.format.DateTimeFormatter - -fun getBugreportFile(context: Context): File { - - val bugreportDir = File(context.cacheDir, "bugreport") - bugreportDir.mkdirs() - - val dmesgFile = File(bugreportDir, "dmesg.txt") - val logcatFile = File(bugreportDir, "logcat.txt") - val tombstonesFile = File(bugreportDir, "tombstones.tar.gz") - val dropboxFile = File(bugreportDir, "dropbox.tar.gz") - val pstoreFile = File(bugreportDir, "pstore.tar.gz") - // Xiaomi/Readmi devices have diag in /data/vendor/diag - val diagFile = File(bugreportDir, "diag.tar.gz") - val oplusFile = File(bugreportDir, "oplus.tar.gz") - val bootlogFile = File(bugreportDir, "bootlog.tar.gz") - val mountsFile = File(bugreportDir, "mounts.txt") - val fileSystemsFile = File(bugreportDir, "filesystems.txt") - val adbFileTree = File(bugreportDir, "adb_tree.txt") - val adbFileDetails = File(bugreportDir, "adb_details.txt") - val ksuFileSize = File(bugreportDir, "ksu_size.txt") - val appListFile = File(bugreportDir, "packages.txt") - val propFile = File(bugreportDir, "props.txt") - val allowListFile = File(bugreportDir, "allowlist.bin") - val procModules = File(bugreportDir, "proc_modules.txt") - val bootConfig = File(bugreportDir, "boot_config.txt") - val kernelConfig = File(bugreportDir, "defconfig.gz") - - val shell = getRootShell(true) - - 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 . --exclude=./minidump.gz").exec() - shell.newJob().add("tar -czf ${oplusFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .").exec() - shell.newJob().add("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .").exec() - - shell.newJob().add("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec() - shell.newJob().add("cat /proc/filesystems > ${fileSystemsFile.absolutePath}").exec() - shell.newJob().add("busybox tree /data/adb > ${adbFileTree.absolutePath}").exec() - shell.newJob().add("ls -alRZ /data/adb > ${adbFileDetails.absolutePath}").exec() - shell.newJob().add("du -sh /data/adb/ksu/* > ${ksuFileSize.absolutePath}").exec() - shell.newJob().add("cp /data/system/packages.list ${appListFile.absolutePath}").exec() - shell.newJob().add("getprop > ${propFile.absolutePath}").exec() - shell.newJob().add("cp /data/adb/ksu/.allowlist ${allowListFile.absolutePath}").exec() - shell.newJob().add("cp /proc/modules ${procModules.absolutePath}").exec() - shell.newJob().add("cp /proc/bootconfig ${bootConfig.absolutePath}").exec() - shell.newJob().add("cp /proc/config.gz ${kernelConfig.absolutePath}").exec() - - val selinux = ShellUtils.fastCmd(shell, "getenforce") - - // basic information - val buildInfo = File(bugreportDir, "basic.txt") - PrintWriter(FileWriter(buildInfo)).use { pw -> - pw.println("Kernel: ${System.getProperty("os.version")}") - pw.println("BRAND: " + Build.BRAND) - pw.println("MODEL: " + Build.MODEL) - pw.println("PRODUCT: " + Build.PRODUCT) - pw.println("MANUFACTURER: " + Build.MANUFACTURER) - pw.println("SDK: " + Build.VERSION.SDK_INT) - pw.println("PREVIEW_SDK: " + Build.VERSION.PREVIEW_SDK_INT) - pw.println("FINGERPRINT: " + Build.FINGERPRINT) - pw.println("DEVICE: " + Build.DEVICE) - pw.println("Manager: " + getManagerVersion(context)) - pw.println("SELinux: $selinux") - - val uname = Os.uname() - pw.println("KernelRelease: ${uname.release}") - pw.println("KernelVersion: ${uname.version}") - pw.println("Machine: ${uname.machine}") - pw.println("Nodename: ${uname.nodename}") - pw.println("Sysname: ${uname.sysname}") - - val ksuKernel = Natives.version - pw.println("KernelSU: $ksuKernel") - val safeMode = Natives.isSafeMode - pw.println("SafeMode: $safeMode") - val lkmMode = Natives.isLkmMode - pw.println("LKM: $lkmMode") - } - - // modules - val modulesFile = File(bugreportDir, "modules.json") - modulesFile.writeText(listModules()) - - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") - val current = LocalDateTime.now().format(formatter) - - val targetFile = File(context.cacheDir, "KernelSU_bugreport_${current}.tar.gz") - - shell.newJob().add("tar czf ${targetFile.absolutePath} -C ${bugreportDir.absolutePath} .").exec() - shell.newJob().add("rm -rf ${bugreportDir.absolutePath}").exec() - shell.newJob().add("chmod 0644 ${targetFile.absolutePath}").exec() - - return targetFile -} - diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt deleted file mode 100644 index 22a9af2a..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt +++ /dev/null @@ -1,330 +0,0 @@ -package com.sukisu.ultra.ui.util - -import android.app.AlertDialog -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.util.Log -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.material3.SnackbarDuration -import androidx.compose.material3.SnackbarHostState -import androidx.compose.material3.SnackbarResult -import androidx.compose.runtime.Composable -import androidx.compose.runtime.rememberCoroutineScope -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import com.sukisu.ultra.R -import java.io.BufferedReader -import java.io.IOException -import java.io.InputStreamReader -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -object ModuleModify { - suspend fun showRestoreConfirmation(context: Context): Boolean { - val result = CompletableDeferred() - withContext(Dispatchers.Main) { - AlertDialog.Builder(context) - .setTitle(context.getString(R.string.restore_confirm_title)) - .setMessage(context.getString(R.string.restore_confirm_message)) - .setPositiveButton(context.getString(R.string.confirm)) { _, _ -> result.complete(true) } - .setNegativeButton(context.getString(R.string.cancel)) { _, _ -> result.complete(false) } - .setOnCancelListener { result.complete(false) } - .show() - } - return result.await() - } - - suspend fun backupModules(context: Context, snackBarHost: SnackbarHostState, uri: Uri) { - withContext(Dispatchers.IO) { - try { - val busyboxPath = "/data/adb/ksu/bin/busybox" - val moduleDir = "/data/adb/modules" - - // 直接将tar输出重定向到用户选择的文件 - val command = """ - cd "$moduleDir" && - $busyboxPath tar -cz ./* > /proc/self/fd/1 - """.trimIndent() - - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", command)) - - // 直接将tar输出写入到用户选择的文件 - context.contentResolver.openOutputStream(uri)?.use { output -> - process.inputStream.copyTo(output) - } - - val error = BufferedReader(InputStreamReader(process.errorStream)).readText() - if (process.exitValue() != 0) { - throw IOException(context.getString(R.string.command_execution_failed, error)) - } - - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.backup_success), - duration = SnackbarDuration.Long - ) - } - - } catch (e: Exception) { - Log.e("Backup", context.getString(R.string.backup_failed, ""), e) - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.backup_failed, e.message), - duration = SnackbarDuration.Long - ) - } - } - } - } - - suspend fun restoreModules(context: Context, snackBarHost: SnackbarHostState, uri: Uri) { - val userConfirmed = showRestoreConfirmation(context) - if (!userConfirmed) return - - withContext(Dispatchers.IO) { - try { - val busyboxPath = "/data/adb/ksu/bin/busybox" - val moduleDir = "/data/adb/modules" - - // 直接从用户选择的文件读取并解压 - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "$busyboxPath tar -xz -C $moduleDir")) - - context.contentResolver.openInputStream(uri)?.use { input -> - input.copyTo(process.outputStream) - } - process.outputStream.close() - - process.waitFor() - - val error = BufferedReader(InputStreamReader(process.errorStream)).readText() - if (process.exitValue() != 0) { - throw IOException(context.getString(R.string.command_execution_failed, error)) - } - - withContext(Dispatchers.Main) { - val snackbarResult = snackBarHost.showSnackbar( - message = context.getString(R.string.restore_success), - actionLabel = context.getString(R.string.restart_now), - duration = SnackbarDuration.Long - ) - if (snackbarResult == SnackbarResult.ActionPerformed) { - reboot() - } - } - - } catch (e: Exception) { - Log.e("Restore", context.getString(R.string.restore_failed, ""), e) - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - message = context.getString( - R.string.restore_failed, - e.message ?: context.getString(R.string.unknown_error) - ), - duration = SnackbarDuration.Long - ) - } - } - } - } - - suspend fun showAllowlistRestoreConfirmation(context: Context): Boolean { - val result = CompletableDeferred() - withContext(Dispatchers.Main) { - AlertDialog.Builder(context) - .setTitle(context.getString(R.string.allowlist_restore_confirm_title)) - .setMessage(context.getString(R.string.allowlist_restore_confirm_message)) - .setPositiveButton(context.getString(R.string.confirm)) { _, _ -> result.complete(true) } - .setNegativeButton(context.getString(R.string.cancel)) { _, _ -> result.complete(false) } - .setOnCancelListener { result.complete(false) } - .show() - } - return result.await() - } - - suspend fun backupAllowlist(context: Context, snackBarHost: SnackbarHostState, uri: Uri) { - withContext(Dispatchers.IO) { - try { - val allowlistPath = "/data/adb/ksu/.allowlist" - - // 直接复制文件到用户选择的位置 - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "cat $allowlistPath")) - - context.contentResolver.openOutputStream(uri)?.use { output -> - process.inputStream.copyTo(output) - } - - val error = BufferedReader(InputStreamReader(process.errorStream)).readText() - if (process.exitValue() != 0) { - throw IOException(context.getString(R.string.command_execution_failed, error)) - } - - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.allowlist_backup_success), - duration = SnackbarDuration.Long - ) - } - - } catch (e: Exception) { - Log.e("AllowlistBackup", context.getString(R.string.allowlist_backup_failed, ""), e) - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.allowlist_backup_failed, e.message), - duration = SnackbarDuration.Long - ) - } - } - } - } - - suspend fun restoreAllowlist(context: Context, snackBarHost: SnackbarHostState, uri: Uri) { - val userConfirmed = showAllowlistRestoreConfirmation(context) - if (!userConfirmed) return - - withContext(Dispatchers.IO) { - try { - val allowlistPath = "/data/adb/ksu/.allowlist" - - // 直接从用户选择的文件读取并写入到目标位置 - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "cat > $allowlistPath")) - - context.contentResolver.openInputStream(uri)?.use { input -> - input.copyTo(process.outputStream) - } - process.outputStream.close() - - process.waitFor() - - val error = BufferedReader(InputStreamReader(process.errorStream)).readText() - if (process.exitValue() != 0) { - throw IOException(context.getString(R.string.command_execution_failed, error)) - } - - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.allowlist_restore_success), - duration = SnackbarDuration.Long - ) - } - - } catch (e: Exception) { - Log.e("AllowlistRestore", context.getString(R.string.allowlist_restore_failed, ""), e) - withContext(Dispatchers.Main) { - snackBarHost.showSnackbar( - context.getString(R.string.allowlist_restore_failed, e.message), - duration = SnackbarDuration.Long - ) - } - } - } - } - - @Composable - fun rememberModuleBackupLauncher( - context: Context, - snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() - ) = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { - result.data?.data?.let { uri -> - scope.launch { - backupModules(context, snackBarHost, uri) - } - } - } - } - - @Composable - fun rememberModuleRestoreLauncher( - context: Context, - snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() - ) = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { - result.data?.data?.let { uri -> - scope.launch { - restoreModules(context, snackBarHost, uri) - } - } - } - } - - @Composable - fun rememberAllowlistBackupLauncher( - context: Context, - snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() - ) = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { - result.data?.data?.let { uri -> - scope.launch { - backupAllowlist(context, snackBarHost, uri) - } - } - } - } - - @Composable - fun rememberAllowlistRestoreLauncher( - context: Context, - snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() - ) = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { - result.data?.data?.let { uri -> - scope.launch { - restoreAllowlist(context, snackBarHost, uri) - } - } - } - } - - fun createBackupIntent(): Intent { - return Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/zip" - val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - putExtra(Intent.EXTRA_TITLE, "modules_backup_$timestamp.zip") - } - } - - fun createRestoreIntent(): Intent { - return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/zip" - } - } - - fun createAllowlistBackupIntent(): Intent { - return Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/octet-stream" - val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - putExtra(Intent.EXTRA_TITLE, "ksu_allowlist_backup_$timestamp.dat") - } - } - - fun createAllowlistRestoreIntent(): Intent { - return Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - addCategory(Intent.CATEGORY_OPENABLE) - type = "application/octet-stream" - } - } - - private fun reboot() { - Runtime.getRuntime().exec(arrayOf("su", "-c", "reboot")) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SELinuxChecker.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/SELinuxChecker.kt deleted file mode 100644 index cb5e1eef..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SELinuxChecker.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.sukisu.ultra.ui.util - -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import com.topjohnwu.superuser.Shell -import com.sukisu.ultra.R - -@Composable -fun getSELinuxStatus(): String { - val shell = Shell.Builder.create().build("sh") - val list = ArrayList() - - val result = shell.use { - it.newJob().add("getenforce").to(list, list).exec() - } - - val output = list.joinToString("\n").trim() - - return if (result.isSuccess) { - when (output) { - "Enforcing" -> stringResource(R.string.selinux_status_enforcing) - "Permissive" -> stringResource(R.string.selinux_status_permissive) - "Disabled" -> stringResource(R.string.selinux_status_disabled) - else -> stringResource(R.string.selinux_status_unknown) - } - } else { - if (output.contains("Permission denied")) { - stringResource(R.string.selinux_status_enforcing) - } else { - stringResource(R.string.selinux_status_unknown) - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/LatestVersionInfo.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/LatestVersionInfo.kt deleted file mode 100644 index 6c134a50..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/LatestVersionInfo.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.sukisu.ultra.ui.util.module - -data class LatestVersionInfo( - val versionCode : Int = 0, - val downloadUrl : String = "", - val changelog : String = "", - val versionName: String = "" -) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/KpmViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/KpmViewModel.kt deleted file mode 100644 index 579edd4b..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/KpmViewModel.kt +++ /dev/null @@ -1,156 +0,0 @@ -package com.sukisu.ultra.ui.viewmodel - -import android.util.Log -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import com.sukisu.ultra.ui.util.* - -class KpmViewModel : ViewModel() { - var moduleList by mutableStateOf(emptyList()) - private set - - var search by mutableStateOf("") - internal set - - var isRefreshing by mutableStateOf(false) - private set - - var currentModuleDetail by mutableStateOf("") - private set - - fun fetchModuleList() { - viewModelScope.launch { - isRefreshing = true - try { - val moduleCount = getKpmModuleCount() - Log.d("KsuCli", "Module count: $moduleCount") - - moduleList = getAllKpmModuleInfo() - - // 获取 KPM 版本信息 - val kpmVersion = getKpmVersion() - Log.d("KsuCli", "KPM Version: $kpmVersion") - } catch (e: Exception) { - Log.e("KsuCli", "获取模块列表失败", e) - } finally { - isRefreshing = false - } - } - } - - private fun getAllKpmModuleInfo(): List { - val result = mutableListOf() - try { - val str = listKpmModules() - val moduleNames = str - .split("\n") - .filter { it.isNotBlank() } - - for (name in moduleNames) { - try { - val moduleInfo = parseModuleInfo(name) - moduleInfo?.let { result.add(it) } - } catch (e: Exception) { - Log.e("KsuCli", "Error processing module $name", e) - } - } - } catch (e: Exception) { - Log.e("KsuCli", "Failed to get module list", e) - } - return result - } - - private fun parseModuleInfo(name: String): ModuleInfo? { - val info = getKpmModuleInfo(name) - if (info.isBlank()) return null - - val properties = info.lineSequence() - .filter { line -> - val trimmed = line.trim() - trimmed.isNotEmpty() && !trimmed.startsWith("#") - } - .mapNotNull { line -> - line.split("=", limit = 2).let { parts -> - when (parts.size) { - 2 -> parts[0].trim() to parts[1].trim() - 1 -> parts[0].trim() to "" - else -> null - } - } - } - .toMap() - - return ModuleInfo( - id = name, - name = properties["name"] ?: name, - version = properties["version"] ?: "", - author = properties["author"] ?: "", - description = properties["description"] ?: "", - args = properties["args"] ?: "", - enabled = true, - hasAction = true - ) - } - - fun loadModuleDetail(moduleId: String) { - viewModelScope.launch { - try { - currentModuleDetail = withContext(Dispatchers.IO) { - getKpmModuleInfo(moduleId) - } - Log.d("KsuCli", "Module detail loaded: $currentModuleDetail") - } catch (e: Exception) { - Log.e("KsuCli", "Failed to load module detail", e) - currentModuleDetail = "Error: ${e.message}" - } - } - } - - var showInputDialog by mutableStateOf(false) - private set - - var selectedModuleId by mutableStateOf(null) - private set - - var inputArgs by mutableStateOf("") - private set - - fun showInputDialog(moduleId: String) { - selectedModuleId = moduleId - showInputDialog = true - } - - fun hideInputDialog() { - showInputDialog = false - selectedModuleId = null - inputArgs = "" - } - - fun updateInputArgs(args: String) { - inputArgs = args - } - - fun executeControl(): Int { - val moduleId = selectedModuleId ?: return -1 - val result = controlKpmModule(moduleId, inputArgs) - hideInputDialog() - return result - } - - data class ModuleInfo( - val id: String, - val name: String, - val version: String, - val author: String, - val description: String, - val args: String, - val enabled: Boolean, - val hasAction: Boolean - ) -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt deleted file mode 100644 index 60676a93..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt +++ /dev/null @@ -1,162 +0,0 @@ -package com.sukisu.ultra.ui.viewmodel - -import android.os.SystemClock -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 androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import com.sukisu.ultra.ui.util.HanziToPinyin -import com.sukisu.ultra.ui.util.listModules -import org.json.JSONArray -import org.json.JSONObject -import java.text.Collator -import java.util.Locale - -class ModuleViewModel : ViewModel() { - - companion object { - private const val TAG = "ModuleViewModel" - private var modules by mutableStateOf>(emptyList()) - } - - class ModuleInfo( - val id: String, - val name: String, - val author: String, - val version: String, - val versionCode: Int, - val description: String, - val enabled: Boolean, - val update: Boolean, - val remove: Boolean, - val updateJson: String, - val hasWebUi: Boolean, - val hasActionScript: Boolean, - val dirId: String, // real module id (dir name) - ) - - var isRefreshing by mutableStateOf(false) - private set - var search by mutableStateOf("") - - var sortEnabledFirst by mutableStateOf(false) - var sortActionFirst by mutableStateOf(false) - val moduleList by derivedStateOf { - 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 - } - } - - var isNeedRefresh by mutableStateOf(false) - private set - - fun markNeedRefresh() { - isNeedRefresh = true - } - - fun fetchModuleList() { - viewModelScope.launch(Dispatchers.IO) { - isRefreshing = true - - val oldModuleList = modules - - val start = SystemClock.elapsedRealtime() - - kotlin.runCatching { - val result = listModules() - - Log.i(TAG, "result: $result") - - val array = JSONArray(result) - modules = (0 until array.length()) - .asSequence() - .map { array.getJSONObject(it) } - .map { obj -> - ModuleInfo( - obj.getString("id"), - obj.optString("name"), - obj.optString("author", "Unknown"), - obj.optString("version", "Unknown"), - obj.optInt("versionCode", 0), - obj.optString("description"), - obj.getBoolean("enabled"), - obj.getBoolean("update"), - obj.getBoolean("remove"), - obj.optString("updateJson"), - obj.optBoolean("web"), - obj.optBoolean("action"), - obj.getString("dir_id"), - ) - }.toList() - isNeedRefresh = false - }.onFailure { e -> - Log.e(TAG, "fetchModuleList: ", e) - isRefreshing = false - } - - // when both old and new is kotlin.collections.EmptyList - // moduleList update will don't trigger - if (oldModuleList === modules) { - isRefreshing = false - } - - Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}, modules: $modules") - } - } - - fun checkUpdate(m: ModuleInfo): Triple { - val empty = Triple("", "", "") - if (m.updateJson.isEmpty() || m.remove || m.update || !m.enabled) { - return empty - } - // download updateJson - val result = kotlin.runCatching { - val url = m.updateJson - Log.i(TAG, "checkUpdate url: $url") - val response = okhttp3.OkHttpClient() - .newCall( - okhttp3.Request.Builder() - .url(url) - .build() - ).execute() - Log.d(TAG, "checkUpdate code: ${response.code}") - if (response.isSuccessful) { - response.body?.string() ?: "" - } else { - "" - } - }.getOrDefault("") - Log.i(TAG, "checkUpdate result: $result") - - if (result.isEmpty()) { - return empty - } - - val updateJson = kotlin.runCatching { - JSONObject(result) - }.getOrNull() ?: return empty - - val version = updateJson.optString("version", "") - val versionCode = updateJson.optInt("versionCode", 0) - val zipUrl = updateJson.optString("zipUrl", "") - val changelog = updateJson.optString("changelog", "") - if (versionCode <= m.versionCode || zipUrl.isEmpty()) { - return empty - } - - return Triple(zipUrl, version, changelog) - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt deleted file mode 100644 index e7635c98..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.sukisu.ultra.ui.viewmodel - -import android.content.ComponentName -import android.content.Intent -import android.content.ServiceConnection -import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo -import android.os.IBinder -import android.os.Parcelable -import android.os.SystemClock -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 com.topjohnwu.superuser.Shell -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import kotlinx.parcelize.Parcelize -import com.sukisu.zako.IKsuInterface -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ksuApp -import com.sukisu.ultra.ui.KsuService -import com.sukisu.ultra.ui.util.HanziToPinyin -import com.sukisu.ultra.ui.util.KsuCli -import java.text.Collator -import java.util.* -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -class SuperUserViewModel : ViewModel() { - companion object { - private const val TAG = "SuperUserViewModel" - private var apps by mutableStateOf>(emptyList()) - } - - @Parcelize - data class AppInfo( - val label: String, - val packageInfo: PackageInfo, - val profile: Natives.Profile?, - ) : Parcelable { - val packageName: String - get() = packageInfo.packageName - val uid: Int - get() = packageInfo.applicationInfo!!.uid - - val allowSu: Boolean - get() = profile != null && profile.allowSu - val hasCustomProfile: Boolean - get() { - if (profile == null) { - return false - } - return if (profile.allowSu) { - !profile.rootUseDefault - } else { - !profile.nonRootUseDefault - } - } - } - - var search by mutableStateOf("") - var showSystemApps by mutableStateOf(false) - var isRefreshing by mutableStateOf(false) - private set - - // 批量操作相关状态 - var showBatchActions by mutableStateOf(false) - internal set - var selectedApps by mutableStateOf>(emptySet()) - internal set - - private val sortedList by derivedStateOf { - val comparator = compareBy { - when { - it.allowSu -> 0 - it.hasCustomProfile -> 1 - else -> 2 - } - }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) - apps.sortedWith(comparator).also { - isRefreshing = false - } - } - - val appList by derivedStateOf { - sortedList.filter { - it.label.contains(search, true) || it.packageName.contains( - search, - true - ) || HanziToPinyin.getInstance() - .toPinyinString(it.label).contains(search, true) - }.filter { - it.uid == 2000 || showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 - } - } - - // 切换批量操作模式 - fun toggleBatchMode() { - showBatchActions = !showBatchActions - if (!showBatchActions) { - clearSelection() - } - } - - // 切换应用选择状态 - fun toggleAppSelection(packageName: String) { - selectedApps = if (selectedApps.contains(packageName)) { - selectedApps - packageName - } else { - selectedApps + packageName - } - } - - // 清除所有选择 - fun clearSelection() { - selectedApps = emptySet() - } - - // 批量更新权限 - suspend fun updateBatchPermissions(allowSu: Boolean) { - selectedApps.forEach { packageName -> - val app = apps.find { it.packageName == packageName } - app?.let { - val profile = Natives.getAppProfile(packageName, it.uid) - val updatedProfile = profile.copy(allowSu = allowSu) - if (Natives.setAppProfile(updatedProfile)) { - apps = apps.map { app -> - if (app.packageName == packageName) { - app.copy(profile = updatedProfile) - } else { - app - } - } - } - } - } - clearSelection() - showBatchActions = false // 批量操作完成后退出批量模式 - fetchAppList() // 刷新列表以显示最新状态 - } - - private suspend fun connectKsuService( - onDisconnect: () -> Unit = {} - ): Pair = suspendCoroutine { continuation -> - val connection = object : ServiceConnection { - override fun onServiceDisconnected(name: ComponentName?) { - onDisconnect() - } - - override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { - continuation.resume(binder as IBinder to this) - } - } - - val intent = Intent(ksuApp, KsuService::class.java) - - val task = KsuService.bindOrTask( - intent, - Shell.EXECUTOR, - connection, - ) - val shell = KsuCli.SHELL - task?.let { it1 -> shell.execTask(it1) } - } - - private fun stopKsuService() { - val intent = Intent(ksuApp, KsuService::class.java) - KsuService.stop(intent) - } - - suspend fun fetchAppList() { - isRefreshing = true - - val result = connectKsuService { - Log.w(TAG, "KsuService disconnected") - } - - withContext(Dispatchers.IO) { - val pm = ksuApp.packageManager - val start = SystemClock.elapsedRealtime() - - val binder = result.first - val allPackages = IKsuInterface.Stub.asInterface(binder).getPackages(0) - - withContext(Dispatchers.Main) { - stopKsuService() - } - - val packages = allPackages.list - - apps = packages.map { - val appInfo = it.applicationInfo - val uid = appInfo!!.uid - val profile = Natives.getAppProfile(it.packageName, uid) - AppInfo( - label = appInfo.loadLabel(pm).toString(), - packageInfo = it, - profile = profile, - ) - }.filter { it.packageName != ksuApp.packageName } - Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}") - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/TemplateViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/TemplateViewModel.kt deleted file mode 100644 index 38f7577d..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/TemplateViewModel.kt +++ /dev/null @@ -1,328 +0,0 @@ -package com.sukisu.ultra.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 com.sukisu.ultra.Natives -import com.sukisu.ultra.profile.Capabilities -import com.sukisu.ultra.profile.Groups -import com.sukisu.ultra.ui.util.getAppProfileTemplate -import com.sukisu.ultra.ui.util.listAppProfileTemplates -import com.sukisu.ultra.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/com/sukisu/ultra/ui/webui/MimeUtil.java b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java deleted file mode 100644 index 5a801039..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/MimeUtil.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.sukisu.ultra.ui.webui; - -import java.net.URLConnection; - -class MimeUtil { - - public static String getMimeFromFileName(String fileName) { - if (fileName == null) { - return null; - } - - // Copying the logic and mapping that Chromium follows. - // First we check against the OS (this is a limited list by default) - // but app developers can extend this. - // We then check against a list of hardcoded mime types above if the - // OS didn't provide a result. - String mimeType = URLConnection.guessContentTypeFromName(fileName); - - if (mimeType != null) { - return mimeType; - } - - return guessHardcodedMime(fileName); - } - - // We should keep this map in sync with the lists under - // //net/base/mime_util.cc in Chromium. - // A bunch of the mime types don't really apply to Android land - // like word docs so feel free to filter out where necessary. - private static String guessHardcodedMime(String fileName) { - int finalFullStop = fileName.lastIndexOf('.'); - if (finalFullStop == -1) { - return null; - } - - final String extension = fileName.substring(finalFullStop + 1).toLowerCase(); - - return switch (extension) { - case "webm" -> "video/webm"; - case "mpeg", "mpg" -> "video/mpeg"; - case "mp3" -> "audio/mpeg"; - case "wasm" -> "application/wasm"; - case "xhtml", "xht", "xhtm" -> "application/xhtml+xml"; - case "flac" -> "audio/flac"; - case "ogg", "oga", "opus" -> "audio/ogg"; - case "wav" -> "audio/wav"; - case "m4a" -> "audio/x-m4a"; - case "gif" -> "image/gif"; - case "jpeg", "jpg", "jfif", "pjpeg", "pjp" -> "image/jpeg"; - case "png" -> "image/png"; - case "apng" -> "image/apng"; - case "svg", "svgz" -> "image/svg+xml"; - case "webp" -> "image/webp"; - case "mht", "mhtml" -> "multipart/related"; - case "css" -> "text/css"; - case "html", "htm", "shtml", "shtm", "ehtml" -> "text/html"; - case "js", "mjs" -> "application/javascript"; - case "xml" -> "text/xml"; - case "mp4", "m4v" -> "video/mp4"; - case "ogv", "ogm" -> "video/ogg"; - case "ico" -> "image/x-icon"; - case "woff" -> "application/font-woff"; - case "gz", "tgz" -> "application/gzip"; - case "json" -> "application/json"; - case "pdf" -> "application/pdf"; - case "zip" -> "application/zip"; - case "bmp" -> "image/bmp"; - case "tiff", "tif" -> "image/tiff"; - default -> null; - }; - } -} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java deleted file mode 100644 index 7d76f6df..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/SuFilePathHandler.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.sukisu.ultra.ui.webui; - -import android.content.Context; -import android.util.Log; -import android.webkit.WebResourceResponse; - -import androidx.annotation.NonNull; -import androidx.annotation.WorkerThread; -import androidx.webkit.WebViewAssetLoader; - -import com.topjohnwu.superuser.Shell; -import com.topjohnwu.superuser.io.SuFile; -import com.topjohnwu.superuser.io.SuFileInputStream; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.GZIPInputStream; - -/** - * Handler class to open files from file system by root access - * For more information about android storage please refer to - * Android Developers - * Docs: Data and file storage overview. - *

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

- * A typical usage would be like: - *

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

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

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

- * The application should typically use a dedicated subdirectory for the files it intends to - * expose and keep them separate from other files. - * - * @param 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/com/sukisu/ultra/ui/webui/WebUIActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt deleted file mode 100644 index 62bde78d..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.sukisu.ultra.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 com.sukisu.ultra.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/com/sukisu/ultra/ui/webui/WebViewInterface.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt deleted file mode 100644 index a3d02a6e..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt +++ /dev/null @@ -1,223 +0,0 @@ -package com.sukisu.ultra.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 com.sukisu.ultra.ui.util.createRootShell -import com.sukisu.ultra.ui.util.listModules -import com.sukisu.ultra.ui.util.withNewRootShell -import org.json.JSONArray -import org.json.JSONObject -import com.sukisu.ultra.ui.util.controlKpmModule -import com.sukisu.ultra.ui.util.listKpmModules -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() - } - - // =================== KPM支持 ============================= - - @JavascriptInterface - fun listAllKpm() : String { - return listKpmModules() - } - - @JavascriptInterface - fun controlKpm(name: String, args: String) : Int { - return controlKpmModule(name, args) - } -} - -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/java/com/sukisu/ultra/utils/AssetsUtil.kt b/manager/app/src/main/java/com/sukisu/ultra/utils/AssetsUtil.kt deleted file mode 100644 index 91ad7c79..00000000 --- a/manager/app/src/main/java/com/sukisu/ultra/utils/AssetsUtil.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.sukisu.ultra.utils - -import android.content.Context -import java.io.File -import java.io.FileOutputStream -import java.io.IOException - -object AssetsUtil { - @Throws(IOException::class) - fun exportFiles(context: Context, src: String, out: String) { - val fileNames = context.assets.list(src) - if (fileNames?.isNotEmpty() == true) { - val file = File(out) - file.mkdirs() - fileNames.forEach { fileName -> - exportFiles(context, "$src/$fileName", "$out/$fileName") - } - } else { - context.assets.open(src).use { inputStream -> - FileOutputStream(File(out)).use { outputStream -> - inputStream.copyTo(outputStream) - } - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/io/sukisu/ultra/UltraShellHelper.java b/manager/app/src/main/java/io/sukisu/ultra/UltraShellHelper.java deleted file mode 100644 index 6d0a7bd7..00000000 --- a/manager/app/src/main/java/io/sukisu/ultra/UltraShellHelper.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.sukisu.ultra; - -import java.util.ArrayList; - -import com.sukisu.ultra.ui.util.KsuCli; - -public class UltraShellHelper { - public static String runCmd(String cmds) { - StringBuilder sb = new StringBuilder(); - for(String str : KsuCli.INSTANCE.getGLOBAL_MNT_SHELL() - .newJob() - .add(cmds) - .to(new ArrayList<>(), null) - .exec() - .getOut()) { - sb.append(str).append("\n"); - } - return sb.toString(); - } - - public static boolean isPathExists(String path) { - return runCmd("file " + path).contains("No such file or directory"); - } - - public static void CopyFileTo(String path, String target) { - runCmd("cp -f " + path + " " + target); - } -} diff --git a/manager/app/src/main/java/io/sukisu/ultra/UltraToolInstall.java b/manager/app/src/main/java/io/sukisu/ultra/UltraToolInstall.java deleted file mode 100644 index 3532915e..00000000 --- a/manager/app/src/main/java/io/sukisu/ultra/UltraToolInstall.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.sukisu.ultra; - -import static com.sukisu.ultra.ui.util.KsuCliKt.getKpmmgrPath; -import static com.sukisu.ultra.ui.util.KsuCliKt.getSuSFSDaemonPath; - -public class UltraToolInstall { - private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr"; - private static final String OUTSIDE_SUSFSD_PATH = "/data/adb/ksu/bin/susfsd"; - public static void tryToInstall() { - String kpmmgrPath = getKpmmgrPath(); - if (UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) { - UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH); - UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH); - } - String SuSFSDaemonPath = getSuSFSDaemonPath(); - if (UltraShellHelper.isPathExists(OUTSIDE_SUSFSD_PATH)) { - UltraShellHelper.CopyFileTo(SuSFSDaemonPath, OUTSIDE_SUSFSD_PATH); - UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_SUSFSD_PATH); - } - } -} diff --git a/manager/app/src/main/jniLibs/.gitignore b/manager/app/src/main/jniLibs/.gitignore deleted file mode 100644 index 05ffdc05..00000000 --- a/manager/app/src/main/jniLibs/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -libzakozako.so -libzakozakozako.so -libkpmmgr.so -libzako.so -libandroidx.graphics.path.so \ No newline at end of file diff --git a/manager/app/src/main/jniLibs/arm64-v8a/libzakoboot.so b/manager/app/src/main/jniLibs/arm64-v8a/libzakoboot.so deleted file mode 100644 index 451ca81c..00000000 Binary files a/manager/app/src/main/jniLibs/arm64-v8a/libzakoboot.so and /dev/null differ diff --git a/manager/app/src/main/res/drawable/ic_launcher_foreground.xml b/manager/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index c9ad0ad4..00000000 --- a/manager/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/manager/app/src/main/res/drawable/ic_launcher_monochrome.xml b/manager/app/src/main/res/drawable/ic_launcher_monochrome.xml deleted file mode 100644 index e4dc522e..00000000 --- a/manager/app/src/main/res/drawable/ic_launcher_monochrome.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/manager/app/src/main/res/mipmap-anydpi/ic_launcher.xml b/manager/app/src/main/res/mipmap-anydpi/ic_launcher.xml deleted file mode 100644 index f30783b2..00000000 --- a/manager/app/src/main/res/mipmap-anydpi/ic_launcher.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/manager/app/src/main/res/mipmap-hdpi/ic_launcher.png b/manager/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index 485b815d..00000000 Binary files a/manager/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/manager/app/src/main/res/mipmap-ldpi/ic_launcher.png b/manager/app/src/main/res/mipmap-ldpi/ic_launcher.png deleted file mode 100644 index 5c18cc79..00000000 Binary files a/manager/app/src/main/res/mipmap-ldpi/ic_launcher.png and /dev/null differ diff --git a/manager/app/src/main/res/mipmap-mdpi/ic_launcher.png b/manager/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 9df35253..00000000 Binary files a/manager/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/manager/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/manager/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index f8dc01c5..00000000 Binary files a/manager/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/manager/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/manager/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index ba22bf82..00000000 Binary files a/manager/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/manager/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/manager/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index cddff920..00000000 Binary files a/manager/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/manager/app/src/main/res/resources.properties b/manager/app/src/main/res/resources.properties deleted file mode 100644 index d5a3ddc9..00000000 --- a/manager/app/src/main/res/resources.properties +++ /dev/null @@ -1 +0,0 @@ -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 deleted file mode 100644 index a9f57193..00000000 --- a/manager/app/src/main/res/values-ar/strings.xml +++ /dev/null @@ -1,133 +0,0 @@ - - - الرئيسية - غير مثبت - إضغط للتثبيت - يعمل - الإصدار: %d - مستخدمين الجذر: %d - الإضافات: %d - غير مدعوم - KernelSU يدعم GKI kernels فقط - إصدار النواة - إصدار المدير - البصمة - وضع SELinux - معطل - مفروض - متساهل - مجهول - مستخدم خارق - لا يمكن تشغيل %s الوحدة - فشل تعطيل الإضافة : %s - لا توجد إضافات مثبتة - الإضافات - إلغاء التثبيت - تثبيت الوحدة - تثبيت - إعادة تشغيل - الإعدادات - إعادة تشغيل سريعة - إعادة تشغيل إلى وضع Recovery - إعادة تشغيل إلى وضع Bootloader - إعادة تشغيل إلى وضع Download - إعادة تشغيل إلى وضع EDL - من نحن - هل أنت متأكد أنك تريد إلغاء تثبيت الإضافة %s ? - تم إلغاء تثبيتها %s - فشل إلغاء تثبيت %s - الإصدار - المطور - إنعاش - إظهار تطبيقات النظام - إخفاء تطبيقات النظام - إرسال السجلات - الوضع الآمن - إعادة التشغيل لتطبيق التغييرات - الوحدات غير متاحة بسبب تعارضها مع Magisk! - تعلم KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html - تعرف على كيفية تثبيت KernelSU واستخدام الإضافات - إدعمنا - KernelSU سيظل دائماً مجانياً ومفتوح المصدر. مع ذلك، يمكنك أن تظهر لنا أنك تهتم بالتبرع. - القدرات - تحديث - تحميل الإضافة: %s - ابدأ التنزيل: %s - الإصدار الجديد: %s متاح ، انقر للتحديث. - تشغيل - الإفتراضي - نموذج - موروث - عالمي - فردي - مجموعات - مُخصّص - تركيب مساحة الاسم - الغاء تحميل الإضافات - فشل تحديث ملف تعريف التطبيق لـ %s - سياق SELinux - ايقاف إجباري - الغاء تحميل الإضافات بشكل افتراضي - القيمة الافتراضية العامة لـ\"إلغاء تحميل الإضافات\" في ملفات تعريف التطبيقات. إذا تم تمكينه، إزالة جميع تعديلات الإضافات على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف. - سيسمح تمكين هذا الخيار لـKernelSU باستعادة أي ملفات معدلة بواسطة الإضافات لهذا التطبيق. - المجال - القواعد - إعادة تشغيل التطبيق - فشل تحديث قواعد SELinux لـ %s - اسم الملف الشخصي - سجل التغييرات - تم الاستيراد بنجاح - تصدير إلى الحافظة - لا يمكن العثور على القالب المحلي للتصدير! - معرف القالب موجود بالفعل! - استيراد من الحافظة - فشل في جلب سجل التغيير: %s - الاسم - معرف القالب غير صالح - مزامنة القوالب عبر الإنترنت - إنشاء قالب - للقراءة فقط - استيراد / تصدير - فشل في حفظ القالب - تحرير القالب - المعرف - قالب ملف تعريف التطبيق - الوصف - حفظ - إدارة القالب المحلي وعبر الإنترنت لملف تعريف التطبيق - حذف - الحافظة فارغة! - عرض القالب - فشل في منح صلاحية الجذر! - فتح - التحقق تلقائيًا من وجود تحديثات عند فتح التطبيق - التحقق من التحديث - تمكين تصحيح أخطاء WebView - يمكن استخدامه لتصحيح أخطاء WebUI، يرجى تمكينه فقط عند الحاجة. - التالي - اختيار ملف - تثبيت مباشر (موصى به) - التثبيت على فتحة غير نشطة (بعد OTA) - سيتم **إجبار** جهازك على التمهيد إلى الفتحة غير النشطة الحالية بعد إعادة التشغيل! -\nاستخدم هذا الخيار فقط بعد انتهاء التحديث. -\nأستمرار؟ - اختر KMI - يوصى باستخدام صورة القسم %1$s - إلغاء التثبيت - إلغاء التثبيت مؤقتًا - إلغاء التثبيت بشكل دائم - استعادة الصورة الاصلية - ‬إلغاء تثبيت KernelSU .(الجذر وجميع الوحدات) بشكل كامل ودائم. - تركيب - نجح التركيب - فشل التركيب - LKM المحددة: %s - استعادة صورة المصنع المخزنة (في حالة وجود نسخة احتياطية)، والتي تُستخدم عادة قبل OTA؛ إذا كنت بحاجة إلى إلغاء تثبيت KernelSU، فيرجى استخدام \"إلغاء التثبيت الدائم\". - قم بإلغاء تثبيت KernelSU مؤقتًا، واستعد إلى حالته الأصلية بعد إعادة التشغيل التالية. - حفظ السجلات - إجراء - السجلات محفوظة - فرز (الممكن أولاً) - فرز (الإجراء أولاً) - diff --git a/manager/app/src/main/res/values-az/strings.xml b/manager/app/src/main/res/values-az/strings.xml deleted file mode 100644 index 00a92ddc..00000000 --- a/manager/app/src/main/res/values-az/strings.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - Ana səhifə - Super istifadəçilər: %d - Nüvə - Yüklənmədi - Yükləmək üçün toxunun - İşləyir - Versiya: %d - Modullar: %d - Hal-hazırda KernelSU yalnız GKI nüvələrini dəstəkləyir - Dəstəklənmir - Yüklə - Yüklə - Naməlum - Barmaq izi - Menecer versiyası - Qeyri-aktiv - SELinux vəziyyəti - Sərbəst - Məcburi - Super istifadəçi - Sil - Modulu aktiv etmək mümkün olmadı: %s - Modulu deaktiv etmək mümkün olmadı: %s - Heç bir modul quraşdırılmayıb - Modul - Yenidən başlat - Parametrlər - Bərpa rejimində yenidən başlat - Yüngül vəziyyətdə yenodən başlat - Bootloader rejimində yenidən başlat - Yükləmə rejimində yenidən başlat - Versiya - Sahib - Modulu silmək istədiyinizdən əminsiniz %s\? - Sistem proqramlarını göstər - Haqqında - EDL rejimində yenidən başlat - Silmək mümkün olmadı: %s - %s silindi - Sistem proqramlarını gizlət - Log-u göndər - Yenilə - Təhlükəsiz rejimi - Qüvvəyə minməsi üçün yenidən başlat - Modular deaktiv edilir,çünki o Magisk-in modulları ilə toqquşur! - KernelSU-yu öyrən - https://kernelsu.org/guide/what-is-kernelsu.html - Bizi dəstəkləyin - KernelSU-yu necə quraşdırılacağını və modulların necə istifadə ediləcəyini öyrən - Şablon - Defolt - Özəl - KernelSU pulsuz və açıq mənbəlidir,həmişə belə olacaqdır. Bununla belə, ianə etməklə bizə qayğı göstərdiyinizi göstərə bilərsiniz. - Profil adı - Bacarıqlar - Modulları umount et - Miras qalmış - Qlobal - Bölmənin ad sahəsi - Fərdi - Qruplar - Defolt olaraq modulları umount et - SELinux konteksi - %s görə tətbiq profillərini güncəlləmək mümkün olmadı - Tətbiq Profillərində \"Umount modulları\" üçün qlobal standart dəyər. Aktivləşdirilərsə, o, Profil dəsti olmayan proqramlar üçün sistemdəki bütün modul dəyişikliklərini siləcək. - Domen - Qaydalar - Güncəllə - Endirməni başlat: %s - Yeni versiya: %s əlçatandır, endirmək üçün toxunun - Modul yüklənir: %s - Bu seçimi aktivləşdirmək KernelSU-ya bu proqram üçün modullar tərəfindən hər hansı dəyişdirilmiş faylları bərpa etməyə imkan verəcək. - - Məcburi dayandır - Yenidən başlat - %s görə SELinux qaydalarını güncəlləmək mümkün olmadı - Girişləri Saxla - diff --git a/manager/app/src/main/res/values-bn-rBD/strings.xml b/manager/app/src/main/res/values-bn-rBD/strings.xml deleted file mode 100644 index 9d8ddff7..00000000 --- a/manager/app/src/main/res/values-bn-rBD/strings.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - কর্নেল এস ইউ কেবল মাত্র জিকআই কর্নেল সাপোর্ট করে - এসইলিনাক্স স্টেটাস - আননোন - মোডিউল ইনেবল করা যায়নি: %s - ইন্সটল করটে চাপুন - কাজ করছে - মোডিউল: %d - অমূলক - কর্নেল - ম্যানেজার ভারসন - ফিঙ্গারপ্রিন্ট - ডিসেবল - এনফোর্সিং - সুপার ইউজার - মোডিউল - আনইন্সটল - ইন্সটল - ইন্সটল - রিবুট - সেটিংস - সফট রিবুট - গ্লোবাল - গ্রুপস - এসইলিনাক্স কন্টেক্সট - %s এর জন্য অ্যাপ প্রফাইল আপডেট করা যায়নি - বাইডিফল্ট মোডিউল আনমাউন্ট - হোম - ইন্সটল হয়নী - পারমিসিভ - মোডিউল ডিসেবল করা যায়নি: %s - কোনো মোডিউল ইন্সটল করা নেই - সংস্করণ: %d - সুপার ইউজার: %d - নেইম স্পেস মাউন্ট - ইনহেরিটেড - ইন্ডিভিজুয়াল - ক্যাপাবিলিটিস - আনমাউন্ট মোডিউলস - রিকভারিতে বুট - বুটলোডারে বুট - ডাউনলোড মডে বুট - ইমারজেন্সি ডাউনলোড মডে বুট - অ্যাবাউট - %s মোডিউল আনইনস্টলের বেপারে নিশ্চিৎ\? - %s আনইনস্টলড - %s আনইনস্টল করা যায়নি - ভার্সন - অথার - লগ সংরক্ষণ করুন - diff --git a/manager/app/src/main/res/values-bn/strings.xml b/manager/app/src/main/res/values-bn/strings.xml deleted file mode 100644 index 0fa6426c..00000000 --- a/manager/app/src/main/res/values-bn/strings.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - হোম - ইনস্টল করা হয়নি - ইনস্টল করার জন্য ক্লিক করুন - ওয়ার্কিং - ওয়ার্কিং সংস্করণ: %d - সুপার ইউজার: %d - মডিউল: %d - অসমর্থিত - KernelSU শুধুমাত্র GKI কার্নেল সমর্থন করে - কার্নেল - ম্যানেজার সংস্করণ - ফিঙ্গারপ্রিন্ট - SELinux স্টেটাস - ডিজেবল - কার্যকর - অনুমতিমূলক - অজানা - সুপার ইউজার - মডিউল সক্ষম করতে ব্যর্থ হয়েছে: %s - মডিউল নিষ্ক্রিয় করতে ব্যর্থ হয়েছে: %s - কোন মডিউল ইনস্টল করা নেই - মডিউল - আনইন্সটল - মডিউল ইনস্টল - ইনস্টল - রিবুট - সেটিংস - সফট রিবুট - রিবুট রিকোভারি - রিবুট বুটলোডার - রিবুট ডাউনলোড - রিবুট ইডিএল - এবাউট - মডিউল আনইনস্টল নিশ্চিত করুন %s? - %s আনইনস্টল সফল - আনইন্সটল ব্যর্থ: %s - ভার্সন - লেখক - রিফ্রেশ - শো সিস্টেম অ্যাপস - হাইড সিস্টেম অ্যাপস - সেন্ড লগ - সেইফ মোড - রিবুট এপ্লাই - মডিউলগুলি অক্ষম কারণ তারা ম্যাজিস্কের সাথে বিরোধিতা করে! - লার্ন কার্নেলএসইউ - https://kernelsu.org/guide/what-is-kernelsu.html - কিভাবে কার্নেলএসইউ ইনস্টল করতে হয় এবং মডিউল ব্যবহার করতে হয় তা শিখুন - সাপোর্ট টাইটেল - কার্নেলএসইউ বিনামূল্যে এবং ওপেন সোর্স, এবং সবসময় থাকবে। আপনি সবসময় একটি অনুদান দিয়ে আপনার কৃতজ্ঞতা প্রদর্শন করতে পারেন. - প্রফাইলের নাম - নেমস্পেস মাউন্ট - গ্রুপস - যোগ্যতা - এসই লিনাক্স কনটেক্সট - ডিফল্ট - টেমপ্লেট - কাস্টম - গ্লোবাল - আলাদাভাবে - আনমাউন্ট মোডিউল - লগ সংরক্ষণ করুন - diff --git a/manager/app/src/main/res/values-bs/strings.xml b/manager/app/src/main/res/values-bs/strings.xml deleted file mode 100644 index cebe99d6..00000000 --- a/manager/app/src/main/res/values-bs/strings.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - Imenski prostor nosača - Naslijeđen - Globalan - Pojedinačan - Grupe - Sposobnosti - SELinux kontekst - Umount module - Ažuriranje Profila Aplikacije za %s nije uspjelo - Umount module po zadanom - Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil. - Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju. - Ažuriranje - Skidanje module: %s - Započnite sa skidanjem: %s - Nova verzija: %s je dostupna, kliknite da skinete - Pokrenite - Prisilno Zaustavite - Resetujte - U Provođenju - Početna - Nije instalirano - Kliknite da instalirate - Superkorisnici: %d - Module: %d - Nepodržano - KernelSU samo podržava GKI kernele sad - Verzija Upravitelja - Otisak prsta - SELinux stanje - Instalirajte - Instalirajte - Ponovo pokrenite - Podešavanja - Verzija - Autor - Osvježi - Prikažite sistemske aplikacije - Sakrijte sistemske aplikacije - Sigurnosni mod - Ponovo pokrenite da bi proradilo - Module su isključene jer je u sukobu sa Magisk-om! - https://kernelsu.org/guide/what-is-kernelsu.html - Naučite kako da instalirate KernelSU i da koristite module - Podržite Nas - Pošaljite Izvještaj - Naučite KernelSU - Domena - Pravila - Neuspješno ažuriranje SELinux pravila za: %s - Radi - Verzija: %d - Kernel - Permisivno - Deinstalirajte - Nepoznato - Nema instaliranih modula - Superkorisnik - Modula - Ponovo pokrenite u Pogonski Učitavatelj - Ponovo pokrenite u Oporavu - %s deinstalirana - Lagano Ponovo pokretanje - Neuspješno uključivanje module: %s - Ponovo pokrenite u Preuzimanje - Neuspješno isključivanje module: %s - Ponovo pokrenite u EDL - Neuspješna deinstalacija: %s - Isključeno - O - Jeste li sigurni da želite deinstalirati modulu %s\? - KernelSU je, i uvijek če biti, besplatan, i otvorenog izvora. Možete nam međutim pokazati da vas je briga s time da napravite donaciju. - Zadano - Šablon - Prilagođeno - Naziv profila - Sačuvaj Dnevnike - diff --git a/manager/app/src/main/res/values-da/strings.xml b/manager/app/src/main/res/values-da/strings.xml deleted file mode 100644 index 3ff7a8ce..00000000 --- a/manager/app/src/main/res/values-da/strings.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - Arbejder - Moduler: %d - Ikke understøttet - Kernel - KernelSU understøtter kun GKI kernels - Manager Version - SELinux-status - Deaktiveret - Tilladende - Superbruger - Håndhævende - Deaktivering af modul fejlede: %s - Intet modul installeret - Afinstaller - Installer - Installer - Genstart - Indstillinger - Blød Genstart - Genstart til Download - Genstart til EDL - Om - Er du sikker på, at du vil afinstallere modulet %s\? - %s afinstalleret - Afinstallation af: %s fejlede - Opdater - Send Log - Sikker tilstand - Genstart for at tage effekt - Lær KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html - Lær hvordan man installerer KernelSU og moduler - Standard - Skabelon - Monter navnerum - Arvet - Global - Grupper - Evner - SELinux-kontext - Afmonteret moduler - Afmontere moduler som standard - Aktivering af denne indstilling vil tillade KernelSU at gendanne hvilken som helst modificeret filer af modulet for denne applikation. - Opdatering - Downloader modulet: %s - Ny version: %s er tilgængelig, kilk for at downloade - Start - Tving Stop - Opdatering af SELinux-regler for: %s fejlede - Start download: %s - Klik for at installere - Version: %d - Hjem - Ikke installeret - Superbrugere: %d - Fingeraftryk - Ukendt - Aktivering af modul fejlede: %s - Genstart til Recovery - Modul - Forfatter - Genstart til Bootloader - Version - Gem system-apps - Vis system-apps - Moduler er deaktiveret, fordi der er konflikt med Magiskes! - Støt Os - KernelSU er, og vil altid være gratis og open source. Du kan stadig vise os din støtte ved at donere. - Brugerdefineret - Profilnavn - Individuel - Opdatering af App Profil for %s fejlede - Den globale standard værdi for \"Afmonter moduler\" i App Profiler. Hvis aktiveret vil den fjerne alle modulers modifikationer til system applikationerne der ikke har en sat Profil. - Domæne - Regler - Genstart - Gem Logfiler - diff --git a/manager/app/src/main/res/values-de/strings.xml b/manager/app/src/main/res/values-de/strings.xml deleted file mode 100644 index 4f5cf9f5..00000000 --- a/manager/app/src/main/res/values-de/strings.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - Startseite - Nicht installiert - Permissiv - Funktioniert - Version: %d - Superuser - Tippe zum Installieren - Superuser: %d - Unbekannt - Erzwingen - In den Bootloader-Modus neustarten - In den Download-Modus neustarten - In den EDL-Modus neustarten - Autor - Über KernelSU - Module sind aufgrund eines Konfliktes mit Magisk nicht verfügbar! - https://kernelsu.org/guide/what-is-kernelsu.html - Erfahre, wie KernelSU installiert wird und wie Module verwendet werden - Unterstütze uns - KernelSU ist und wird immer frei und quelloffen sein. Du kannst uns jedoch deine Unterstützung zeigen, indem du eine Spende tätigst. - SELinux-Kontext - Module standardmäßig aushängen - Globaler Standardwert für \"Module aushängen\" im App-Profil. Falls er aktiviert ist, werden alle Moduländerungen im System für alle Apps entfernt, für die kein Profil festgelegt ist. - Standard - Vorlage - Benutzerdefiniert - App-Profilaktualisierung für %s fehlgeschlagen - Geerbt - Global - Individuell - Domäne - Aktualisieren - Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen. - Regeln - Starte Download: %s - Aktualisieren der SELinux-Regeln schlug fehl für: %s - Starten - Neue Version %s verfügbar, tippen zum Aktualisieren. - Stopp erzwingen - Neustarten - Module: %d - Manager-Version - SELinux Status - Deaktiviert - Modulaktivierung fehlgeschlagen: %s - Moduldeaktivierung fehlgeschlagen: %s - Keine Modul installiert - Modul - Deinstallieren - Installieren - Neustarten - Einstellungen - In den Recovery-Modus neustarten - %s deinstalliert - Version - Aktualisieren - System-Apps anzeigen - System-Apps ausblenden - Protokoll senden - KernelSU verstehen - Sicherer Modus - Neustarten, damit Änderungen wirksam werden - Profilname - Namespace einhängen - Gruppen - Fähigkeiten - Module aushängen - Lädt Modul %s herunter - Nicht unterstützt - KernelSU unterstützt derzeit nur GKI-Kernel - Kernel - Fingerabdruck - Installieren - Soft-Reboot - Möchtest du wirklich Modul %s deinstallieren? - Deinstallation fehlgeschlagen: %s - Ä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. - Protokolle Speichern - Permanent deinstallieren - Standard-Abbild wiederherstellen - KernelSU temporär deinstallieren, originalen Status nach dem nächsten Neustart wiederherstellen. - Das Standard Werksabbild wiederherstellen (falls ein Backup existiert), normalerweise vor einem OTA zu verwenden; falls Sie KernelSU deinstallieren müssen, nutzen Sie bitte \"Permanent deinstallieren\". - Schreibt - Schreiben erfolgreich - Schreiben fehlgeschlagen - Wähle LKM: %s - Aktion - Protokolle gespeichert - diff --git a/manager/app/src/main/res/values-es/strings.xml b/manager/app/src/main/res/values-es/strings.xml deleted file mode 100644 index 2eb4dedc..00000000 --- a/manager/app/src/main/res/values-es/strings.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - Inicio - No instalado - Haz clic para instalar - Funcionando - Versión: %d - Superusuarios: %d - Módulos: %d - Sin soporte - KernelSU solo admite kernels GKI por ahora - Versión del kernel - Versión del gestor - Huella del dispositivo - Estado de SELinux - - Desactivado - Estricto - Permisivo - Desconocido - Superusuario - Error al activar el módulo: %s - Error al desactivar el módulo: %s - Ningún módulo instalado - Módulo - Desinstalar - Instalar - Instalar - Reiniciar - Ajustes - Reinicio suave - Reiniciar en modo de recuperación - Reiniciar en modo de arranque - Reiniciar en modo Download - Reiniciar en modo EDL - Acerca de - ¿Está seguro de que desea desinstalar el módulo %s? - %s desinstalado - Fallo al desinstalar: %s - Versión - Autor - Refrescar - Mostrar aplicaciones del sistema - Ocultar aplicaciones del sistema - Enviar registros - Modo seguro - Reinicia para aplicar cambios - ¡Los módulos no están disponibles debido a un conflicto con Magisk! - Aprende KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html - Aprende a instalar KernelSU y a utilizar módulos - Apóyanos - KernelSU es, y siempre será, gratuito y de código abierto. Sin embargo, puedes demostrarnos que te importamos haciendo una donación. - Predeterminado - Plantilla - Personalizado - Nombre de perfil - Montaje del espacio de nombres - Heredado - Global - Individual - Grupos - Capacidades - Contexto SELinux - Desmontar módulos - Error al actualizar el perfil de la aplicación para %s - Desmontar módulos por defecto - El valor global predeterminado para \"Umount modules\" en App Profile. Si está activado, eliminará todas las modificaciones de módulos del sistema para las apps que no tengan un perfil establecido. - Activar esta opción permitirá a KernelSU restaurar cualquier archivo modificado por los módulos para esta aplicación. - Dominio - Reglas - Actualizar - Descargando módulo: %s - Iniciar descarga: %s - La nueva versión %s está disponible, haga clic para actualizar. - Iniciar - Forzar detención - Reiniciar - Error al actualizar las reglas SELinux para: %s - 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. - Comprobar actualización - Comprobación automática de actualizaciones al abrir la aplicación - Puede ser usado para depurar WebUI, por favor habilítalo sólo cuando sea necesario. - Restaurar la imagen de fábrica stock (Si existe una copia de seguridad), por lo general se utiliza antes de OTA; si necesita desinstalar KernelSU, por favor, utilice \"Desinstalar permanentemente\". - diff --git a/manager/app/src/main/res/values-et/strings.xml b/manager/app/src/main/res/values-et/strings.xml deleted file mode 100644 index 10d24f19..00000000 --- a/manager/app/src/main/res/values-et/strings.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - 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 - 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 - Profiili nimi - Kohandatud - Päritud - Globaalne - Individuaalne - Võimekused - Sobimatu malli ID - SELinux kontekst - 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 - diff --git a/manager/app/src/main/res/values-fa/strings.xml b/manager/app/src/main/res/values-fa/strings.xml deleted file mode 100644 index 4fa06c8f..00000000 --- a/manager/app/src/main/res/values-fa/strings.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - خانه - نصب نشده است - برای نصب ضربه بزنید - به درستی کار می‌کند - نسخه: %d - برنامه های با دسترسی روت: %d - ماژول‌ها: %d - پشتیبانی نشده - کرنل اس یو فقط هسته های gki را پشتیبانی میکند - هسته - نسخه برنامه - اثرانگشت - وضعیت SELinux - غیرفعال - قانونمند - آزاد - ناشناخته - دسترسی روت - فعال کردن ماژول ناموفق بود: %s - غیرفعال کردن ماژول ناموفق بود: %s - هیچ ماژولی نصب نشده است - ماژول - لغو نصب - نصب - نصب - راه اندازی دوباره - تنظیمات - راه اندازی نرم - راه اندازی به ریکاوری - راه اندازی به بوتلودر - راه اندازی به حالت دانلود - راه اندازی به EDL - درباره - آیا مطمئنید که میخواهید ماژول %s را پاک کنید؟ - %s پاک شد - پاک کردن ناموفق بود: %s - نسخه - سازنده - تازه‌سازی - نمایش برنامه های سیستمی - مخفی کردن برنامه های سیستمی - ارسال وقایع - حالت امن - راه‌اندازی مجدد برای تاثیرگذاری - مازول به دلیل تعارض با مجیسک غیرفعال شده اند\'s! - یادگیری کرنل اس یو - https://kernelsu.org/guide/what-is-kernelsu.html - یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید - از ما حمایت کنید - KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است. - پیش‌فرض - قالب - شخصی سازی شده - اسم پروفایل - Mount namespace - اثر گرفته - گلوبال - تکی - جداکردن ماژول ها - ذخیره گزارش‌ها - diff --git a/manager/app/src/main/res/values-fil/strings.xml b/manager/app/src/main/res/values-fil/strings.xml deleted file mode 100644 index 5c13391d..00000000 --- a/manager/app/src/main/res/values-fil/strings.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - Katayuan ng SELinux - Hindi pinagana - Enforcing - Permissive - Hindi naka-install - Home - Pindutin para mag-install - Gumagana - Bersyon: %d - Hindi matukoy - Mga Modyul: %d - Hindi Suportado - Sinusuportahan lang ng KernelSU ang mga kernel ng GKI ngayon - Nabigong paganahin ang modyul: %s - Nabigong i-disable ang modyul: %s - Walang naka-install na modyul - Modyul - I-install - I-install - I-reboot - I-soft Reboot - I-reboot sa Download - I-reboot sa EDL - Tungkol - Sigurado ka bang gusto mong i-uninstall ang modyul %s\? - Na-uninstall ang %s - Nabigong i-uninstall: %s - May-akda - I-refresh - Ipakita ang mga application ng system - Magpadala ng Log - I-reboot para umepekto - Hindi pinagana ang mga modyul dahil salungat ito sa Magisk! - Alamin ang KernelSU - Matutunan kung paano mag-install ng KernelSU at gumamit ng mga modyul - Suportahan Kami - Ang KernelSU ay, at palaging magiging, libre, at open source. Gayunpaman, maaari mong ipakita sa amin na nagmamalasakit ka sa pamamagitan ng pagbibigay ng donasyon. - I-mount ang namespace - Indibidwal - Mga Grupo - Mga Kakayanan - Konteksto ng SELinux - I-unmount ang mga modyul - Nabigong i-update ang App Profile para sa %s - Ang pagpapagana sa opsyong ito ay magbibigay-daan sa KernelSU na ibalik ang anumang binagong file ng mga modyul para sa aplikasyon na ito. - Mga Tuntunin - Nagda-download ng modyul: %s - Simulan ang pag-download: %s - Bagong bersyon: Available ang %s, i-click upang i-download - Ilunsad - Pilit na I-hinto - I-restart - Nabigong i-update ang mga panuntunan ng SELinux para sa: %s - Bersyon ng Manager - Mga setting - I-reboot sa Recovery - I-reboot sa Bootloader - Bersyon - I-uninstall - Itago ang mga application ng system - 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 deleted file mode 100644 index 886ce326..00000000 --- a/manager/app/src/main/res/values-fr/strings.xml +++ /dev/null @@ -1,133 +0,0 @@ - - - Non installé - Fonctionnel - Version : %d - Super-utilisateurs : %d - Modules : %d - KernelSU ne prend désormais en charge que les noyaux GKI - Noyau - Empreinte digitale - Mode SELinux - Désactivé - Permissive - Inconnu - Super-utilisateur - Aucun module installé - Accueil - Appuyez ici pour installer - Non pris en charge - Échec de la désinstallation : %s - Version - Version du gestionnaire - Enforcing - Échec de l\'activation du module : %s - Modules - Désinstaller - Installer - Échec de la désactivation du module : %s - Redémarrer - Installer - Paramètres - Redémarrer en mode bootloader - Redémarrage progressif - Redémarrer en mode de récupération - Redémarrer en mode EDL - À propos - %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 - Rafraîchir - Afficher les applications système - Masquer les applications système - Mode sans échec - Envoyer les journaux - Redémarrez pour appliquer les modifications - Les modules sont indisponibles en raison d\'un conflit avec Magisk ! - https://kernelsu.org/guide/what-is-kernelsu.html - Soutenez-nous - Découvrez comment installer KernelSU et utiliser les modules - KernelSU est, et restera toujours, gratuit et open source. Vous pouvez cependant nous témoigner de votre soutien en nous faisant un don. - Modèle - Par défaut - Personnalisé - Nom du profil - Espace de noms de montage - Hérité - Individuel - Contexte SELinux - Global - Groupes - Capacités - Démonter les modules - Échec de la modification du profil d\'application de %s - 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 l\'option \"Démonter les modules\" dans les profils d\'application. Lorsque l\'option est activée, les modifications apportées au système par les modules sont supprimées pour les applications qui n\'ont pas de profil défini. - Domaine - Règles - Mettre à jour - Téléchargement du module : %s - Lancer - La nouvelle version %s est disponible, appuyez 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 SELinux pour : %s - 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. Activez uniquement cette option 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 - Désinstaller - Désinstaller temporairement - Désinstaller définitivement - Restaurer l\'image d\'origine - Restaurer l\'image d\'origine d\'usine (s\'il en existe une sauvegarde). Utilisé généralement avant une mise à jour OTA ; si vous devez désinstaller KernelSU, utilisez plutôt l\'option \"Désinstaller définitivement\". - Flash en cours - Flash réussi - Échec du flash - LKM sélectionné : %s - Désinstallation complète et permanente de KernelSU (root et tous les modules). - Désinstaller KernelSU temporairement et rétablir l\'état original au redémarrage suivant. - Enregistrer les journaux - Trier par action - Trier par activé - Action - Journaux enregistrés - diff --git a/manager/app/src/main/res/values-gl/strings.xml b/manager/app/src/main/res/values-gl/strings.xml deleted file mode 100644 index 89956f23..00000000 --- a/manager/app/src/main/res/values-gl/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - 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 deleted file mode 100644 index 7684a9ac..00000000 --- a/manager/app/src/main/res/values-hi/strings.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - प्रभाव में होने के लिए रीबूट करें - जानें कि KernelSU कैसे स्थापित करें और मॉड्यूल का उपयोग कैसे करें - अज्ञात - सिस्टम एप्प दिखाए - %s अनइंस्टॉल सफल हुआ - मॉड्यूल्स अनमाउंट करें - लॉग भेजे - डिसेबल्ड (बंद) - हमें प्रोत्साहन दें - Inherited - मॉड्यूल बंद कर दिए गए हैं क्योंकि यह मैजिक के साथ टकरा रहे है! - क्या बदलाव हुए है - पर्मिसिव - डाउनलोड में रिबूट करें - डिफ़ॉल्ट रूप से मॉड्यूल अनमाउन्ट करें - इस विकल्प को चालू करने से KernelSU को इस एप्लिकेशन के लिए मॉड्यूल द्वारा किसी भी मोडिफाइड फ़ाइल को रिस्टोर करें। - Individual - %s मॉड्यूल चालू करने में विफल - जबर्दस्ती बंद करें - EDL मोड में रिबूट करें - फिर से चालू करें - क्षमताएं - सुपरयूजर : %d - %s की डाउनलोडिंग स्टार्ट करें - Global - ऐप प्रोफाइल में \"अनमाउंट मॉड्यूल\" के लिए ग्लोबल डिफ़ॉल्ट वैल्यू। यदि चालू किया गया है, तो यह एप्लीकेशंस के लिऐ सिस्टम के सभी मॉड्यूल मोडिफिकेशन को हटा देगा जिनकी प्रोफ़ाइल सेट नहीं है। - मॉड्यूल्स : %d - एनफोर्सिंग - SELinux context - फिंगरप्रिंट - डिफॉल्ट - लॉन्च करें - सेफ मोड - रिकवरी में रिबूट करें - सॉफ्ट रिबूट - प्रोफाइल का नाम - KernelSU मुफ़्त और ओपन सोर्स और हमेशा रहेगा। हालाँकि आप दान देकर हमें दिखा सकते हैं कि आप संरक्षण करते हैं। - अनइंस्टॉल करें - Namspace माउंट करें - इंस्टाल करें - इंस्टाल करने के लिए क्लिक करें - नियम - समूह - मॉड्यूल - निर्माता - हमारे बारे में - वर्जन: %d - रीबूट करें - KernelSU अभी केवल GKI कर्नल्स को सपोर्ट करता है - SELinux स्थिति - सिस्टम एप्प छिपाए - वर्जन - सपोर्ट नहीं करता है - डोमेन - होम - कस्टम - टेम्पलेट - रिफ्रेश - %s मॉड्यूल डाउनलोड हो रहा है - अपडेट - KernelSU सीखें - क्या आप सच में मॉड्यूल %s को अनइंस्टॉल करना चाहते हैं\? - %s अनइंस्टल करने में असफल - सुपरयूजर - सेटिंग - काम कर रहा है - %s मॉड्यूल बंद करने में विफल - कोई मॉड्यूल इंस्टाल नहीं हुआ - इंस्टाल करें - कर्नल - इंस्टाल नहीं हुआ - %s के लिए ऐप प्रोफ़ाइल अपडेट करने में विफल - https://kernelsu.org/guide/what-is-kernelsu.html - %s के लिए SELinux नियमों को अपटेड करने में विफल - बुटलोडर में रिबूट करें - मैनेजर वर्जन - नया वर्जन: %s उपलब्ध है,अपग्रेड के लिए क्लिक करें - लॉग सहेजें - diff --git a/manager/app/src/main/res/values-hr/strings.xml b/manager/app/src/main/res/values-hr/strings.xml deleted file mode 100644 index a95c1f50..00000000 --- a/manager/app/src/main/res/values-hr/strings.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - Prikažite sistemske aplikacije - Sakrijte sistemske aplikacije - Pošaljite Izvještaj - Sigurnosni mod - Ponovno pokrenite da bi proradilo - Neuspješno ažuriranje SELinux pravila za: %s - Početna - Nije instalirano - Verzija: %d - Kliknite da instalirate - Radi - Superkorisnici: %d - Module: %d - Nepodržano - KernelSU samo podržava GKI kernele sad - Kernel - Verzija Voditelja - Otisak prsta - Isključeno - U Provođenju - Permisivno - SELinux stanje - Nepoznato - Superkorisnik - Neuspješno uključivanje module: %s - Neuspješno isključivanje module: %s - Nema instaliranih modula - Modula - Deinstalirajte - Instalirajte - Instalirajte - Ponovno pokrenite - Postavke - Lagano Ponovno pokretanje - Ponovno pokrenite u Oporavu - Ponovno pokrenite u Pogonski Učitavalac - Ponovno pokrenite u Preuzimanje - Ponovo pokrenite u EDL - O - Jeste li sigurni da želite deinstalirati modulu %s\? - %s deinstalirana - Neuspješna deinstalacija: %s - Verzija - Autor - Osvježi - Module su isključene jer je u sukobu sa Magisk-om! - Naučite KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html - Naučite kako da instalirate KernelSU i da koristite module - Podržite Nas - KernelSU je, i uvijek če biti, besplatan, i otvorenog izvora. Možete nam međutim pokazati da vas je briga s time da napravite donaciju. - Zadano - Šablon - Prilagođeno - Naziv profila - Naslijeđen - Imenski prostor nosača - Ažuriranje Profila Aplikacije za %s nije uspjelo - Globalan - Pojedinačan - Umount module - Grupe - Sposobnosti - SELinux kontekst - Umount module po zadanom - Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil. - Domena - Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju. - Pravila - Ažuriranje - Preuzimanje module: %s - Započnite sa preuzimanjem: %s - Nova verzija: %s je dostupna, kliknite da preuzmete - Pokrenite - Prisilno Zaustavite - Resetujte - Spremi Zapise - diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml deleted file mode 100644 index 14250500..00000000 --- a/manager/app/src/main/res/values-hu/strings.xml +++ /dev/null @@ -1,129 +0,0 @@ - - - Működik - Verzió: %d - Modulok: %d - A KernelSU jelenleg csak GKI kerneleket támogat - Kernel - Alkalmazás verziója - Ujjlenyomat - Letiltva - Újraindítás letöltő módba - Újraindítás EDL-be - Névjegy - Biztos benne hogy eltávolítja a következő modult: %s? - Nem sikerült eltávolítani: %s - Készítő - Frissítés - Rendszeralkalmazások megjelenítése - Rendszeralkalmazások elrejtése - Biztonságos mód - A modulok nem érhetők el a Magiskkel való ütközés miatt! - Tudjon meg többet a KernelSU-ról - Ismerje meg a KernelSU telepítését és a modulok használatát - Támogasson minket - Alapértelmezett - Sablon - Egyedi - Profil neve - Névtér csatlakoztatása - Örökölt - https://kernelsu.org/guide/what-is-kernelsu.html - Különálló - Csoportok - Jogosultságok - SELinux kontextus - Modulok leválasztása alapértelmezetten - Ha engedélyezi ezt az opciót, a KernelSU visszaállíthatja az alkalmazás moduljai által módosított fájlokat. - Tartomány - Szabályok - Frissítés - Modul letöltése: %s - Letöltés indítása: %s - Indítás - Kényszerített leállítás - újraindítás - Kezdőlap - Nincs telepítve - Kattintson a telepítéshez - Engedélyezett alkalmazások: %d - Nem támogatott - SELinux állapot - Kényszerített - Engedélyezett - Ismeretlen - Superuser - Nem sikerült engedélyezni a következő modult: %s - Nem sikerült letiltani a következő modult: %s - Nincs telepített modul - Modulok - Eltávolítás - Telepítés - Telepítés - Újraindítás - Beállítások - Rendszerfelület újraindítása - Újraindítás recovery-módba - Újraindítás bootloader-módba - %s eltávolítva - Verzió - Naplók küldése - Indítsa újra a készüléket a változások érvényesítéséhez - A KernelSU ingyenes, nyílt forráskódú és mindig is az lesz. Ön azonban adományozással megmutathatja, hogy törődik a projekttel. - Globális - Modulok leválasztása - Nem sikerült frissíteni az App Profilt ehhez: %s - A \"Modulok leválasztása\" globális alapértelmezett értéke az App Profile-ban. Ha engedélyezve van, eltávolít minden modulmódosítást a rendszerből azon alkalmazások esetében, amelyeknek nincs profilja beállítva. - Elérhető az új, %s verzió, kattintson a frissítéshez. - Nem sikerült frissíteni az SELinux szabályokat a következőhöz: %s - 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? - Á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 - diff --git a/manager/app/src/main/res/values-in/strings.xml b/manager/app/src/main/res/values-in/strings.xml deleted file mode 100644 index c93bffab..00000000 --- a/manager/app/src/main/res/values-in/strings.xml +++ /dev/null @@ -1,133 +0,0 @@ - - - Beranda - Tidak terinstal - Klik untuk menginstal - Berfungsi - Versi: %d - SuperUser: %d - Modul: %d - Tidak didukung - KernelSU saat ini hanya mendukung kernel GKI - Kernel - Versi manager - Identitas - Status SELinux - Nonaktif - Enforcing - Permisif - Tidak diketahui - SuperUser - Gagal mengaktifkan modul: %s - Gagal menonaktifkan modul: %s - Tidak ada modul yang terpasang - Modul - Hapus - Instal - Instal - Reboot - Pengaturan - Soft Reboot - Reboot ke Recovery - Reboot ke Bootloader - Reboot ke Download - Reboot ke EDL - Tentang - Yakin menghapus modul %s? - %s berhasil dihapus - Gagal menghapus: %s - Versi - Oleh - Muat ulang - Tampilkan aplikasi sistem - Sembunyikan aplikasi sistem - Kirim Log - Mode aman - Reboot agar berfungsi - Konflik dengan Magisk, fungsi modul ditiadakan! - Pelajari KernelSU - https://kernelsu.org/id_ID/guide/what-is-kernelsu.html - Pelajari cara instal KernelSU dan menggunakan modul - Dukung Kami - KernelSU akan selalu menjadi aplikasi gratis dan terbuka. Anda dapat memberikan donasi sebagai bentuk dukungan. - Bawaan - Templat - Khusus - Nama profil - Mount Namespace - Diwariskan - Universal - Individual - Kelompok - Kemampuan - Konteks SELinux - 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 - Membarui - Mengunduh modul: %s - Mulai mengunduh: %s - Tersedia versi terbaru %s, Klik untuk membarui. - Jalankan - Paksa berhenti - Mulai ulang - Gagal membarui aturan SELinux pada: %s - 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 - 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) - diff --git a/manager/app/src/main/res/values-it/strings.xml b/manager/app/src/main/res/values-it/strings.xml deleted file mode 100644 index 84da3092..00000000 --- a/manager/app/src/main/res/values-it/strings.xml +++ /dev/null @@ -1,129 +0,0 @@ - - - Home - Non installato - Clicca per installare - In esecuzione - Versione: %d - Applicazioni con accesso root: %d - Moduli installati: %d - Non supportato - KernelSU ora supporta solo i kernel GKI - Kernel - Versione del manager - Impronta della build di Android - Stato di SELinux - Disabilitato - Enforcing - Permissive - Sconosciuto - Accesso root - Impossibile abilitare il modulo: %s - Impossibile disabilitare il modulo: %s - Nessun modulo installato - Modulo - Disinstalla - Installa - Installa - Riavvia - Impostazioni - Riavvio rapido - Riavvia in modalità Recovery - Riavvia in modalità Bootloader - Riavvia in modalità Download - Riavvia in modalità EDL - Informazioni - Sei sicuro di voler disinstallare il modulo %s? - %s disinstallato - Impossibile disinstallare: %s - Versione - Autore - 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 Magisk! - Scopri KernelSU - https://kernelsu.org/guide/what-is-kernelsu.html - Scopri come installare KernelSU e utilizzare i moduli - Supportaci - KernelSU è, e sempre sarà, gratuito e open source. Puoi comunque mostrarci il tuo apprezzamento facendo una donazione. - Nome profilo - Spazio dei nomi del mount - Globale - Gruppi - Ereditato - Individuale - Predefinito - Personalizzato - Modello - Scollega moduli - Contesto SELinux - Aggiornamento App Profile per %s fallito - Aggiorna - Apri - Capacità - Scollega moduli da default - Regole - Sto scaricando il modulo: %s - Inizia a scaricare:%s - Nuova versione: %s disponibile, tocca per aggiornare - Arresto forzato - Riavvia - Aggiornamento regole SELinux per %s fallito - Attivando questa opzione permetterai a KernelSU di ripristinare ogni file modificato dai moduli per questa app. - Dominio - Il valore predefinito per \"Scollega moduli\" in App Profile. Se attivato, rimuoverà tutte le modifiche al sistema da parte dei moduli per le applicazioni che non hanno un profilo impostato. - 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? - 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 - diff --git a/manager/app/src/main/res/values-iw/strings.xml b/manager/app/src/main/res/values-iw/strings.xml deleted file mode 100644 index fb97a716..00000000 --- a/manager/app/src/main/res/values-iw/strings.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - הפעל מחדש כדי להכניס לתוקף - למד כיצד להתקין את KernelSU ולהשתמש במודולים - לא ידוע - הצג אפליקציות מערכת - %s הוסר - הסרת טעינת מודולים - שלח לוג - מושבת - תמכו בנו - ירושה - מודולים מושבתים מכיוון שהם מתנגשים עם זה של Magisk! - יומן שינויים - התרים - הפעלה מחדש למצב הורדה - טעינת מודולים כברירת מחדל - הפעלת אפשרות זו תאפשר ל-KernelSU לשחזר קבצים שהשתנו על ידי המודולים עבור יישום זה. - אישי - הפעלת המודל נכשלה: %s - עצירה בכח - הפעלה מחדש למצב EDL - איתחול - יכולת - משתמשי על: %d - מפעיל מודל: %s - גלובלי - ערך ברירת המחדל הגלובלי עבור \"טעינת מודולים\" בפרופילי אפליקציה. אם מופעל, זה יסיר את כל שינויי המודול למערכת עבור יישומים שאין להם ערכת פרופיל. - מודלים:%d - אכיפה - הקשר SELinux - טביעת אצבע - ברירת מחדל - להשיק - מצב בטוח - הפעלה מחדש לריקברי - רך Reboot - שם פרופיל - KernelSU הוא, ותמיד יהיה, חינמי וקוד פתוח. עם זאת, תוכל להראות לנו שאכפת לך על ידי תרומה. - הסרה - טעינת מרחב שמות - התקנה - לחץ להתקנה - כללים - קבוצה - מודולים - יוצר - אודות - גרסה: %d - הפעלה מחדש - KernelSU תומך רק בליבת GKI כעת - סטטוס SELinux - הסתר אפליקציות מערכת - גרסה - אינו נתמך - תחום - בית - מותאם אישית - תבנית - רענון - מוריד מודל: %s - עדכון - למד אודות KernelSU - האם אתה בטוח שברצונך להסיר את התקנת המודל %s\? - הסרת התקנת %s נכשלה: - משתמש על - הגדרות - עובד - השבתת מודל %s נכשלה: - אין מודלים מותקנים - להתקין - Kernel - לא מותקן - נכשל עדכון פרופיל האפליקציה עבור %s - https://kernelsu.org/guide/what-is-kernelsu.html - נכשל עדכון כללי SELinux עבור: %s - הפעלה מחדש לבוטלאודר - גרסת מנהל - גרסה חדשה עבור: %s זמינה, לחץ כדי לשדרג - שמור יומנים - diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml deleted file mode 100644 index ef1197f0..00000000 --- a/manager/app/src/main/res/values-ja/strings.xml +++ /dev/null @@ -1,328 +0,0 @@ - - - ホーム - 未インストール - タップでインストール - 動作中 - バージョン: %d - スーパーユーザー: %d - モジュール: %d - 非対応 - カーネルの KernelSU ドライバが未検出です。カーネルが間違ってませんか? - カーネルのバージョン - SuSFS: %s - SuSFS のバージョン - SuS SU - マネージャーのバージョン - Fingerprint - SELinux のステータス - 無効 - Enforcing - Permissive - 不明 - スーパーユーザー - %s モジュールを ON にできませんでした - %s モジュールを OFF にできませんでした - モジュールがインストールされていません - モジュール - 並べ替え (アクション優先) - 並べ替え (最初に有効) - アンインストール - 復元 - インストール - インストール - 再起動 - 設定 - ソフトリブート - リカバリーで再起動 - ブートローダーで再起動 - ダウンロードモードで再起動 - EDL で再起動 - アプリについて - モジュール %s をアンインストールしますか? - %s はアンインストールされました - %s をアンインストールできませんでした - バージョン - 作者 - 更新 - システムアプリを表示 - システムアプリを非表示 - ログを送信する - セーフモード - 再起動すると有効化されます - モジュールが Magisk との競合により利用できません! - KernelSU について学ぶ - https://kernelsu.org/ja_JP/guide/what-is-kernelsu.html - KernelSU のインストール方法やモジュールの使い方を学習できます。 - 支援する - KernelSU は今後も無料でオープンソースです。ですが、寄付をして頂けると開発者への貢献になります。 - %2$s チャンネルにご参加ください。]]> - デフォルト - テンプレート - カスタム - プロファイル名 - 名前空間のマウント - 継承 - 共通 - 分離 - グループ - ケーパビリティ - SELinux コンテキスト - モジュールのアンマウント - %s のアプリのプロファイルの更新をできませでした - 現在の KernelSU のバージョン %d は低すぎるため、マネージャーは正常に動作しません。バージョン %d 以上に更新してください! - デフォルトでモジュールのマウントを解除する - アプリプロファイルの「モジュールのアンマウント」の共通となるデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。 - kprobe フックを無効化 - このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。 - ドメイン - ルール - 更新 - モジュールをダウンロード中: %s - ダウンロードを開始: %s - 新しいバージョン %s が利用可能です。タップしてダウンロード。 - 起動 - 強制停止 - 再起動 - SELinux ルールの更新に失敗しました %s - 変更履歴 - アプリプロファイルのテンプレート - アプリプロファイルのローカルおよびオンラインテンプレートを管理します - テンプレートの作成 - テンプレートの編集 - ID - 無効なテンプレート ID - 名前 - 説明 - 保存 - 消去 - テンプレートを表示 - 読み取り専用 - テンプレート ID はすでに存在します! - インポートとエクスポート - クリップボードからインポート - クリップボードからエクスポート - エクスポートするローカル テンプレートが見つかりません! - インポートが成功しました - オンラインテンプレートの同期 - テンプレートの保存に失敗しました - クリップボードが空です! - 変更ログの取得に失敗しました: %s - 更新を確認する - アプリを開いたときに更新を自動的に確認します - root の付与に失敗しました! - アクション - 開く - WebView デバッグを有効化する - WebUI のデバッグに使用できます。必要な場合でのみ有効化してください - 直接インストール (推奨) - パッチを適用する必要があるミラーを選択 - 非アクティブなスロットにインストール (OTA 後) - 再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。 -\nこのオプションは、OTA が完了した後にのみ使用してください。 -\n続行しますか? - 次へ - %1$s のパーティションイメージを推奨します - KMI を選択してください - アンインストール - 一時的にアンインストールする - 完全にアンインストールする - ストックイメージを復元 - KernelSU を一時的にアンインストールし、次回の再起動後に元の状態に戻します。 - KernelSU (root およびすべてのモジュール) を完全かつ恒久的にアンインストールします。 - バックアップが存在する場合、工場出荷時のイメージを復元できます (OTA の前に使用してください)。KernelSU をアンインストールする必要がある場合は、「完全にアンインストールする」を使用してください。 - フラッシュ - フラッシュが成功しました - フラッシュに失敗しました - 選択された LKM: %s - ログを保存 - 保存されたログ - 対応 - 非対応 - 不明 - SuS SU モード: - - %1$s のモジュールをインストールしますか? - 不明なモジュール - - モジュールの復元を確認 - この操作によりモジュールが上書きされます。続行しますか? - 確認 - キャンセル - - バックアップが完了しました (tar.gz) - バックアップに失敗: %1$s - モジュールをバックアップ - モジュールを復元 - - モジュールは正常に復元されました、再起動が必要です - 復元に失敗: %1$s - 今すぐ再起動 - 不明なエラー - - コマンドの実行に失敗しました: %1$s - - 許可リストのバックアップが成功しました - 許可リストのバックアップに失敗: %1$s - 許可リストの復元を確認 - この操作により許可リストが上書きされます。続行しますか? - 許可リストの復元が成功しました - 許可リストの復元に失敗: %1$s - 許可リストをバックアップ - 許可リストを復元 - カスタムアプリ背景 - 背景にする画像を選択してください - ナビゲーションバーの透過 - デフォルトに復元 - Android のバージョン - デバイスモデル - %s にスーパーユーザー権限を付与することはできません - su の互換性を無効化する - su コマンドを使用してアプリが root 権限を取得する動作を一時的に無効化します (既存の root プロセスは影響を受けません)。 - SukiSU Beta Manager を使用しています。 - 選択した %d 個のモジュールをインストールしてもよろしいですか? - %1$d 個のモジュールをインストールしてもよろしいですか?\n\n%2$s - その他の設定 - SELinux - 有効 - 無効 - シンプルモード - ON にすると不要なカードを非表示にします - カーネルのバージョンを非表示にする - カーネルのバージョンを非表示にします - その他の情報を非表示にする - ホームページ上のスーパーユーザー、モジュール、KPM モジュールの数に関する情報を非表示にします - SuSFS ステータスを非表示にする - ホームページ上の SuSFS ステータス情報を非表示にします - リンクカードのステータスを隠す - ホームページのリンクカード情報を隠す - テーマ - システムに従う - ライト - ダーク - 手動でフック - ダイナミックカラー - システムテーマのダイナミックカラーを使用します - テーマカラーを選択 - ブルー - グリーン - パープル - オレンジ - ピンク - グレー - アイボリー - ブラシの設定 - フラッシュするファイルを選択 - AnyKernel3 をインストール - AnyKernel3 カーネルファイルをフラッシュします - root 権限が必要です - ファイルのコピーに失敗しました - スクラブが完了しました - すぐに再起動しますか? - はい - いいえ - 再起動に失敗しました - Bulk ライセンス - 認証を一括でキャンセル - バックアップ - イエロー - カーネルモジュール - カーネルモジュール - カーネルモジュールは現在インストールされていません - バージョン - 作者 - アンインストール - アンインストールに失敗しました - アンインストールに失敗しました - インストール - KPM モジュールの読み込みに成功しました - KPM モジュールの読み込みに失敗しました - パラメータ - 実行 - KPM のバージョン - 閉じる - 以下のカーネルモジュール関数は KernelPatch によって開発され、SukiSU Ultra のカーネルモジュール関数を含むように変更されました - SukiSU Ultra の今後にご期待ください - 成功 - 失敗 - SukiSU Ultra は将来的に KSU から比較的に独立したブランチになりますが、公式の KernelSU や MKSU などの貢献は引き続き感謝しています! - 非対応 - 対応 - "KPM モジュールの数: %d " - 無効な KPM ファイル - カーネルはパッチされていません - カーネルは未設定です - カスタム設定 - KPM をインストール - 読み込む - 埋め込む - 選択してください: %1\$s モジュールのインストールモード \n\n読み込む: モジュールを一時的に読み込みます\n埋め込む: システムで恒久的にインストールします - モジュールファイルの存在を確認できませんでした - モジュールファイルが存在するか確認できません - アンインストールを確認 - アンインストール - キャンセル - テーマカラー - ファイルの種類が間違っています!.kpm ファイルを選択してください。 - アンインストール - 次の KPM がアンインストールされます: %s - KernelSU によって作成された kprobe フックを無効化して、代替となるインラインフックを使用します。これは、非 GKI カーネルのフック方式に似た物になります。 - 背景画像を調整 - 2 本の指で画像を拡大、1 本の指でドラッグで位置を調整します - イメージを読み込めません - 再プロビジョニング - - カーネルをフラッシュ - ログ: - フラッシュが完了しました - - 準備中… - ファイルを削除中… - ファイルをコピー中… - フラッシュツールを展開中… - フラッシュスクリプトをパッチ中… - カーネルをフラッシュ中… - フラッシュが完了しました - - フラッシュ先のスロットを選択 - フラッシュする boot のターゲットスロットを選択 - スロット A - スロット B - 選択したスロット: %1$s - オリジナルのスロットを取得 - 指定するスロットを設定 - デフォルトのスロットに復元 - 現在のスロット: %1$s - - コピーに失敗しました - 不明なエラー - フラッシュに失敗しました - - LKM の修復またはインストール - GKI/non-GKI のインストール - カーネルのバージョン: %1$s - パッチ適用ツールの使用: %1$s - 設定 - アプリの設定 - ツール - 現在 - - 削除 - root アプリの権限 - カスタマイズされたアプリ構成 - その他のアプリ - アプリがありません - SELinux 有効 - SELinux 無効 - SELinux ステータスの変更に失敗しました - 高度な設定 - ツールバーをカスタマイズ - 戻る - 最高の状態 - 設置 - SuSFS 有効 - SuSFS 無効 - 背景の設定が成功しました - カスタム背景を削除しました - root 権限が必要 - diff --git a/manager/app/src/main/res/values-kn/strings.xml b/manager/app/src/main/res/values-kn/strings.xml deleted file mode 100644 index d64d4e4d..00000000 --- a/manager/app/src/main/res/values-kn/strings.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - ಪರಿಣಾಮ ಬೀರಲು ರೀಬೂಟ್ ಮಾಡಿ - KernelSU ಅನ್ನು ಹೇಗೆ ಸ್ಥಾಪಿಸಬೇಕು ಮತ್ತು ಮಾಡ್ಯೂಲ್‌ಗಳನ್ನು ಬಳಸುವುದು ಹೇಗೆ ಎಂದು ತಿಳಿಯಿರಿ - ತಿಳಿಯದ - ಸಿಸ್ಟಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ತೋರಿಸಿ - %s ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ - Umount ಮಾಡ್ಯೂಲ್‌ಗಳು - ಲಾಗ್ ಕಳುಹಿಸಿ - ನಮ್ಮನ್ನು ಬೆಂಬಲಿಸಿ - ಪಿತ್ರಾರ್ಜಿತ - ಮಾಡ್ಯೂಲ್‌ಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ ಏಕೆಂದರೆ ಇದು ಮ್ಯಾಜಿಸ್ಕ್‌ನೊಂದಿಗೆ ಸಂಘರ್ಷವಾಗಿದೆ! - ಚೇಂಜ್ಲಾಗ್ - Permissive - ಡೀಫಾಲ್ಟ್ ಆಗಿ Umount ಮಾಡ್ಯೂಲ್ - ಈ ಆಯ್ಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸುವುದರಿಂದ ಈ ಅಪ್ಲಿಕೇಶನ್‌ಗಾಗಿ ಮಾಡ್ಯೂಲ್‌ಗಳ ಮೂಲಕ ಯಾವುದೇ ಮಾರ್ಪಡಿಸಿದ ಫೈಲ್‌ಗಳನ್ನು ಮರುಸ್ಥಾಪಿಸಲು KernelSU ಗೆ ಅನುಮತಿಸುತ್ತದೆ. - ವೈಯಕ್ತಿಕ - ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ: %s - ಫೋರ್ಸ್ ಸ್ಟಾಪ್ - EDL ಗೆ ರೀಬೂಟ್ - ಸಾಮರ್ಥ್ಯಗಳು - ಸೂಪರ್‌ಯೂಸರ್‌ಗಳು: %d - ಡೌನ್‌ಲೋಡ್ ಮಾಡುವುದನ್ನು ಪ್ರಾರಂಭಿಸಿ: %s - ಜಾಗತಿಕ - ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್‌ಗಳಲ್ಲಿ \"Umount ಮಾಡ್ಯೂಲ್\" ಗಾಗಿ ಜಾಗತಿಕ ಡೀಫಾಲ್ಟ್ ಮೌಲ್ಯ. ಸಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಪ್ರೊಫೈಲ್ ಸೆಟ್ ಅನ್ನು ಹೊಂದಿರದ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಿಗಾಗಿ ಸಿಸ್ಟಮ್‌ಗೆ ಎಲ್ಲಾ ಮಾಡ್ಯೂಲ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಇದು ತೆಗೆದುಹಾಕುತ್ತದೆ. - ಮಾಡ್ಯೂಲ್‌ಗಳು: %d - SELinux ಸಂದರ್ಭ - ಡೀಫಾಲ್ಟ್ - ಲಾಂಚ್ - ಸುರಕ್ಷಿತ ಮೋಡ್ - ಸಾಫ್ಟ್ ರೀಬೂಟ್ - ಪ್ರೊಫೈಲ್ ಹೆಸರು - KernelSU ಉಚಿತ ಮತ್ತು ಮುಕ್ತ ಮೂಲವಾಗಿದೆ ಮತ್ತು ಯಾವಾಗಲೂ ಇರುತ್ತದೆ. ಆದಾಗ್ಯೂ ನೀವು ದೇಣಿಗೆ ನೀಡುವ ಮೂಲಕ ನೀವು ಕಾಳಜಿ ವಹಿಸುತ್ತೀರಿ ಎಂದು ನಮಗೆ ತೋರಿಸಬಹುದು. - ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ - ಮೌಂಟ್ ನೇಮ್‌ಸ್ಪೇಸ್ - ನಿಯಮಗಳು - ಗುಂಪುಗಳು - ಮಾಡ್ಯೂಲ್ - ಲೇಖಕ - ಬಗ್ಗೆ - ವರ್ಷನ್: %d - ರೀಬೂಟ್ - KernelSU ಈಗ GKI ಕರ್ನಲ್‌ಗಳನ್ನು ಮಾತ್ರ ಬೆಂಬಲಿಸುತ್ತದೆ - SELinux ಸ್ಥಿತಿ - ಸಿಸ್ಟಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ಮರೆಮಾಡಿ - ವರ್ಷನ್ - ಬೆಂಬಲಿತವಾಗಿಲ್ಲ - ಡೊಮೇನ್ - ಮನೆ - ಕಸ್ಟಮ್ - ಟೆಂಪ್ಲೇಟ್ - ರಿಫ್ರೆಶ್ - ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ: %s - KernelSU ಕಲಿಯಿರಿ - %s ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಅಸ್ಥಾಪಿಸಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ\? - ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲು ವಿಫಲವಾಗಿದೆ: %s - ಸೂಪರ್ಯೂಸರ್ - ಕೆಲಸ ಮಾಡುತ್ತಿದೆ - ಮಾಡ್ಯೂಲ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ: %s - ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿಲ್ಲ - ಕರ್ನಲ್ - %s ಗಾಗಿ ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್ ಅನ್ನು ನವೀಕರಿಸಲು ವಿಫಲವಾಗಿದೆ - https://kernelsu.org/guide/what-is-kernelsu.html - ಮ್ಯಾನೇಜರ್ ವರ್ಷನ್ - ಹೊಸ ಆವೃತ್ತಿ: %s ಲಭ್ಯವಿದೆ, ಅಪ್‌ಗ್ರೇಡ್ ಮಾಡಲು ಕ್ಲಿಕ್ ಮಾಡಿ - ಲಾಗ್ಗಳನ್ನು ಉಳಿಸಿ - diff --git a/manager/app/src/main/res/values-ko/strings.xml b/manager/app/src/main/res/values-ko/strings.xml deleted file mode 100644 index b67c858e..00000000 --- a/manager/app/src/main/res/values-ko/strings.xml +++ /dev/null @@ -1,131 +0,0 @@ - - - - 설치되지 않음 - 이 곳을 눌러 설치하기 - 정상 작동 중 - 버전: %d - 루트 권한: %d개 - 설치된 모듈: %d개 - 지원되지 않음 - KernelSU는 현재 GKI 커널만 지원합니다 - 커널 - 매니저 버전 - 빌드 정보 - SELinux 상태 - 비활성화됨 - 적용 - 허용 - 알 수 없음 - 슈퍼유저 - 모듈 활성화 실패: %s - 모듈 비활성화 실패: %s - 설치된 모듈 없음 - 모듈 - 삭제 - 설치 - 설치 - 다시 시작 - 설정 - 빠른 다시 시작 - 복구 모드로 다시 시작 - 부트로더로 다시 시작 - 다운로드 모드로 다시 시작 - EDL 모드로 다시 시작 - 정보 - %s 모듈을 삭제할까요? - %s 모듈 삭제됨 - 모듈 삭제 실패: %s - 버전 - 제작자 - 새로고침 - 시스템 앱 보이기 - 시스템 앱 숨기기 - 로그 보내기 - 안전 모드 - 다시 시작하여 변경 사항 적용 - Magisk와 충돌로 모듈을 사용할 수 없습니다! - KernelSU 알아보기 - KernelSU 설치 방법과 모듈 사용 방법을 확인합니다 - 지원이 필요합니다 - KernelSU는 지금도, 앞으로도 항상 무료이며 오픈 소스로 유지됩니다. 기부를 통해 여러분의 관심을 보여주세요. - https://kernelsu.org/guide/what-is-kernelsu.html - 앱 프로필 메뉴의 \"모듈 마운트 해제\" 설정에 대한 전역 기본값을 설정합니다. 활성화 시, 개별 프로필이 설정되지 않은 앱은 시스템에 대한 모듈의 모든 수정사항이 적용되지 않습니다. - 다시 시작 - 규칙 - 새 버전: %s이 사용 가능합니다, 여기를 눌러 업그레이드하세요. - 다운로드 시작: %s - 강제 중지 - 기본값 - 사용자 지정 - 템플릿 - 프로필 이름 - 이름 공간 마운트 - 상속 - 전역 - 개별 - 사용자 그룹 - 모듈 사용 해제 - SELinux 컨텍스트 - 권한 - %s에 대한 앱 프로필 업데이트 실패 - 기본값으로 모듈 사용 해제 - 이 옵션이 활성화되면, KernelSU는 이 앱에 대한 모듈의 모든 수정사항을 복구합니다. - 업데이트 - 모듈 받는 중: %s - 도메인 - 실행 - 다음 앱에 대한 SELinux 규칙 업데이트 실패: %s - 로그 저장 - 업데이트 내역 - WebUI 디버깅에 사용 가능, 필요할 때만 활성화해주세요. - 플래시 중 - 선택된 LKM: %s - %1$s 파티션 이미지 권장됨 - KMI 선택 - 다음 - 완전히, 그리고 영구히 KernelSU (루트 및 모든 모듈)를 삭제합니다. - WebView 디버깅 활성화 - 동작 - 임시적 삭제 - 업데이트 내역 가져오기 실패: %s - 열기 - 재부팅 후 기기는 **강제로** 비활성 슬롯으로 부팅합니다!\nOTA를 진행한 후에만 이 옵션을 사용하세요.\n진행할까요? - 플래시 성공 - 플래시 실패 - 삭제 - 영구적 삭제 - 임시적으로 KernelSU를 삭제하고, 다음 재부팅에 원래대로 복구합니다. - 앱 프로필 템플레이트 - 앱 프로필의 로컬 및 온라인 템플레이트 관리 - ID - 올바르지 않은 템플레이트 id - 이름 - 설명 - 저장 - 삭제 - 읽기 전용 - 템플레이트 ID가 이미 존재합니다! - 불러오기/내보내기 - 클립보드에서 불러오기 - 클립보드로 내보내기 - 불러오기 성공 - 온라인 템플레이트 동기화 - 템플레이트 저장 실패 - 클립보드가 비었습니다! - 루트 부여 실패! - 템플레이트 생성 - 템플레이트 편집 - 템플레이트 보기 - 내보낼 로컬 템플레이트가 없습니다! - 파일 선택 - 직접 설치 (권장) - 비활성 슬롯에 설치 (OTA 이후) - 순정 이미지 복구 - 순정 이미지 복구 (백업이 존재한다면), OTA 전에 사용합니다; KernelSU를 삭제해야 한다면, \"영구적 삭제\"를 사용해 주세요. - 업데이트 확인 - 앱 실행시 자동으로 업데이트 확인 - 로그 저장됨 - 정렬 (활성화됨 우선) - 정렬 (동작이 있는 것 우선) - diff --git a/manager/app/src/main/res/values-lt/strings.xml b/manager/app/src/main/res/values-lt/strings.xml deleted file mode 100644 index 8a071fc4..00000000 --- a/manager/app/src/main/res/values-lt/strings.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - Pirštų atspaudas - Išjungta - Priverstinas - Nežinomas - Supernaudotojai - Nepavyko įjungti modulio: %s - Nepavyko išjungti modulio: %s - Leistinas - Nėra įdiegtų modulių - Moduliai - Perkrovimas neišjungus - Perkrauti į atkūrimo rėžimą - Perkrauti į įkrovos tvarkyklę - Perkrauti į atsisiuntimo rėžimą - Apie - Nepavyko išdiegti: %s - %s išdiegtas - Versija - Autorius - Rodyti sistemos programas - Slėpti sistemos programas - Siųsti žurnalą - Paleisti iš naujo - Atšviežinti - Saugus rėžimas - Paleiskite iš naujo, kad įsigaliotų - Moduliai yra išjungti, nes jie konfliktuoja su Magisk\'s! - https://kernelsu.org/guide/what-is-kernelsu.html - Sužinokite apie KernelSU - Sužinokite, kaip įdiegti KernelSU ir naudoti modulius - Numatytas - Šablonas - Pasirinktinis - Profilio pavadinimas - Prijungti vardų erdvę - Paveldėtas - Globalus - Individualus - Grupės - Galimybės - SELinux kontekstas - Atjungti modulius - Atjungti modulius pagal numatytuosius parametrus - Įjungus šią parinktį, KernelSU galės atkurti visus modulių modifikuotus failus šiai programai. - Domenas - Taisyklės - Atnaujinti - Atsisiunčiamas modulis: %s - Pradedamas atsisiuntimas: %s - Nauja versija: %s pasiekiama, spustelėkite norėdami atsinaujinti - Paleisti - Priversti sustoti - Perkrauti - Nepavyko atnaujinti SELinux taisyklių: %s - Namai - Neįdiegta - KernelSU dabar palaiko tik GKI branduolius - Spustelėkite norėdami įdiegti - Veikia - Supernaudotojai: %d - Versija: %d - Nepalaikoma - Moduliai: %d - Tvarkyklės versija - Branduolys - SELinux statusas - Išdiegti - Įdiegti - Įdiegti - Parametrai - Perkrauti į EDL - Ar tikrai norite išdiegti modulį %s\? - Paremkite mus - KernelSU yra ir visada bus nemokamas ir atvirojo kodo. Tačiau galite parodyti, kad jums rūpi, paaukodami mums. - Nepavyko atnaujinti programos profilio %s - Visuotinė numatytoji „Modulių atjungimo“ reikšmė programų profiliuose. Jei įjungta, ji pašalins visus sistemos modulio pakeitimus programoms, kurios neturi profilio. - Keitimų žurnalas - Saglabāt Žurnālus - diff --git a/manager/app/src/main/res/values-lv/strings.xml b/manager/app/src/main/res/values-lv/strings.xml deleted file mode 100644 index 55dec619..00000000 --- a/manager/app/src/main/res/values-lv/strings.xml +++ /dev/null @@ -1,129 +0,0 @@ - - - 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 - 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 - 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 - Iespējot WebView atkļūdošanu - Ieteicams %1$s nodalījuma attēls - Nākamais - Mantots - Izvēlieties failu - Instalēt neaktīvajā slotā (pēc OTA) - Pēc restartēšanas jūsu ierīce tiks **PIESPIESTI** palaista pašreizējā neaktīvajā slotā! -\nIzmantojiet šo opciju tikai pēc OTA pabeigšanas -\nTurpināt? - Tiešā instalēšana (Ieteicams) - Atinstalēt - Pagaidu atinstalēšana - Atjaunot oriģinālo attēlu - Īslaicīgi atinstalēt KernelSU, pēc nākamās restartēšanas atjaunot sākotnējo stāvokli. - KernelSU (saknes un visu moduļu) pilnīga atinstalēšana. - Atjaunojot rūpnīcas attēlu (ja ir dublējums), ko parasti izmanto pirms OTA; ja nepieciešams atinstalēt KernelSU, lūdzu, izmantojiet \"Neatgriezeniski atinstalēt\". - Izvēlētais lkm: %s - Neizdevās piešķirt sakni! - Atvērt - Pārbaudīt atjauninājumus - Automātiski pārbaudīt atjauninājumus atverot aplikāciju - Var izmantot WebUI atkļūdošanai, lūdzu, izmantot tikai tad, kad tas ir nepieciešams. - Izvēlieties KMI - Neatgriezeniski atinstalēt - Instalē - Instalēts veiksmīgi - Instalēšana neizdevās - Išsaugoti Žurnalus - diff --git a/manager/app/src/main/res/values-mr/strings.xml b/manager/app/src/main/res/values-mr/strings.xml deleted file mode 100644 index 64457bb8..00000000 --- a/manager/app/src/main/res/values-mr/strings.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - इंस्टॉल केले नाही - होम - इंस्टॉल साठी क्लिक करा - कार्यरत - आवृत्ती: %d - मॉड्यूल्स: %d - सुपरयूझर: %d - असमर्थित - KernelSU आता फक्त GKI कर्नलचे समर्थन करते - कर्नल - फिंगरप्रिंट - व्यवस्थापक आवृत्ती - SELinux स्थिती - अक्षम - एनफोर्सिंग - परमिसिव - अज्ञात - स्थापित करा - कोणतेही मॉड्यूल स्थापित केलेले नाही - रीबूट करा - सुपरयुझर - मॉड्यूल सक्षम करण्यात अयशस्वी: %s - विस्थापित करा - मॉड्यूल अक्षम करण्यात अयशस्वी: %s - मॉड्यूल - स्थापित करा - सेटिंग्ज - सॉफ्ट रीबूट - बद्दल - EDL वर रीबूट करा - तुमची खात्री आहे की तुम्ही मॉड्यूल %s विस्थापित करू इच्छिता\? - विस्थापित करण्यात अयशस्वी: %s - सिस्टम अॅप्स दाखवा - बूटलोडरवर रीबूट करा - %s विस्थापित - आवृत्ती - लेखक - रिफ्रेश करा - रिकवरी मध्ये रिबुट करा - डाउनलोड करण्यासाठी रीबूट करा - लॉग पाठवा - सुरक्षित मोड - सिस्टम अॅप्स लपवा - प्रभावी होण्यासाठी रीबूट करा - KernelSU शिका - https://kernelsu.org/guide/what-is-kernelsu.html - मॉड्यूल अक्षम केले आहेत कारण ते Magisk च्या विरोधाभास आहे! - KernelSU कसे स्थापित करायचे आणि मॉड्यूल कसे वापरायचे ते शिका - KernelSU विनामूल्य आणि मुक्त स्रोत आहे, आणि नेहमीच असेल. तथापि, देणगी देऊन तुम्ही आम्हाला दाखवू शकता की तुमची काळजी आहे. - आम्हाला पाठिंबा द्या - कस्टम - माउंट नेमस्पेस - डीफॉल्ट - साचा - वैयक्तिक - क्षमता - प्रोफाइल नाव - इनहेरीटेड - जागतिक - गट - SELinux संदर्भ - उमाउंट मॉड्यूल्स - %s साठी अॅप प्रोफाइल अपडेट करण्यात अयशस्वी - डीफॉल्टनुसार मॉड्यूल्स उमाउंट करा - अॅप प्रोफाइलमधील \"उमाउंट मॉड्यूल्स\" साठी जागतिक डीफॉल्ट मूल्य. सक्षम असल्यास, ते प्रोफाइल सेट नसलेल्या ॲप्लिकेशनचे सिस्टममधील सर्व मॉड्यूल बदल काढून टाकेल. - हा पर्याय सक्षम केल्याने KernelSU ला या ऍप्लिकेशनसाठी मॉड्यूल्सद्वारे कोणत्याही सुधारित फाइल्स पुनर्संचयित करण्यास अनुमती मिळेल. - यासाठी SELinux नियम अपडेट करण्यात अयशस्वी: %s - नियम - अपडेट करा - डोमेन - मॉड्यूल डाउनलोड करत आहे: %s - डाउनलोड करणे सुरू करा: %s - नवीन आवृत्ती: %s उपलब्ध आहे, डाउनलोड करण्यासाठी क्लिक करा - सक्तीने थांबा - लाँच करा - पुन्हा सुरू करा - लॉग जतन करा - diff --git a/manager/app/src/main/res/values-ms/strings.xml b/manager/app/src/main/res/values-ms/strings.xml deleted file mode 100644 index 67215ee7..00000000 --- a/manager/app/src/main/res/values-ms/strings.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - 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 - diff --git a/manager/app/src/main/res/values-night/themes.xml b/manager/app/src/main/res/values-night/themes.xml deleted file mode 100644 index d76ba8e6..00000000 --- a/manager/app/src/main/res/values-night/themes.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/manager/app/src/main/res/xml/backup_rules.xml b/manager/app/src/main/res/xml/backup_rules.xml deleted file mode 100644 index fa0f996d..00000000 --- a/manager/app/src/main/res/xml/backup_rules.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - \ No newline at end of file diff --git a/manager/app/src/main/res/xml/data_extraction_rules.xml b/manager/app/src/main/res/xml/data_extraction_rules.xml deleted file mode 100644 index 9ee9997b..00000000 --- a/manager/app/src/main/res/xml/data_extraction_rules.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/manager/app/src/main/res/xml/filepaths.xml b/manager/app/src/main/res/xml/filepaths.xml deleted file mode 100644 index f8a9a5c5..00000000 --- a/manager/app/src/main/res/xml/filepaths.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ 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 deleted file mode 100644 index 6dd26ccf..00000000 --- a/manager/app/src/main/res/xml/network_security_config.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - 127.0.0.1 - 0.0.0.0 - ::1 - - diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts deleted file mode 100644 index 0b3cd783..00000000 --- a/manager/build.gradle.kts +++ /dev/null @@ -1,92 +0,0 @@ -import com.android.build.api.dsl.ApplicationDefaultConfig -import com.android.build.api.dsl.CommonExtension -import com.android.build.gradle.api.AndroidBasePlugin -import java.io.ByteArrayOutputStream - -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) -} - -cmaker { - default { - arguments.addAll( - arrayOf( - "-DANDROID_STL=none", - ) - ) - abiFilters("arm64-v8a", "x86_64", "riscv64") - } - buildTypes { - if (it.name == "release") { - arguments += "-DDEBUG_SYMBOLS_PATH=${layout.buildDirectory.asFile.get().absolutePath}/symbols" - } - } -} - -val androidMinSdkVersion = 26 -val androidTargetSdkVersion = 36 -val androidCompileSdkVersion = 36 -val androidCompileNdkVersion = "28.0.13004108" -val androidSourceCompatibility = JavaVersion.VERSION_21 -val androidTargetCompatibility = JavaVersion.VERSION_21 -val managerVersionCode by extra(1 * 10000 + getGitCommitCount() + 606) -val managerVersionName by extra(getGitDescribe()) - -fun getGitCommitCount(): Int { - return providers.exec { - commandLine("git", "rev-list", "--count", "HEAD") - }.standardOutput.asText.get().trim().toInt() -} - -fun getGitDescribe(): String { - return providers.exec { - commandLine("git", "describe", "--tags", "--always", "--abbrev=0") - }.standardOutput.asText.get().trim() -} - - - -fun getVersionCode(): Int { - val commitCount = getGitCommitCount() - val major = 1 - return major * 10000 + commitCount + 606 -} - -fun getVersionName(): String { - return getGitDescribe() -} - -subprojects { - plugins.withType(AndroidBasePlugin::class.java) { - extensions.configure(CommonExtension::class.java) { - compileSdk = androidCompileSdkVersion - ndkVersion = androidCompileNdkVersion - - defaultConfig { - minSdk = androidMinSdkVersion - if (this is ApplicationDefaultConfig) { - targetSdk = androidTargetSdkVersion - versionCode = managerVersionCode - versionName = managerVersionName - } - ndk { - abiFilters += listOf("arm64-v8a", "x86_64", "riscv64") - } - } - - lint { - abortOnError = true - checkReleaseBuilds = false - } - - compileOptions { - sourceCompatibility = androidSourceCompatibility - targetCompatibility = androidTargetCompatibility - } - } - } -} \ No newline at end of file diff --git a/manager/gradle.properties b/manager/gradle.properties deleted file mode 100644 index 62d1c743..00000000 --- a/manager/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -android.experimental.enableNewResourceShrinker.preciseShrinking=true -android.enableAppCompileTimeRClass=true -android.useAndroidX=true -org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml deleted file mode 100644 index 66052f10..00000000 --- a/manager/gradle/libs.versions.toml +++ /dev/null @@ -1,84 +0,0 @@ -[versions] -agp = "8.10.0" -kotlin = "2.1.10" -ksp = "2.1.10-1.0.30" -compose-bom = "2025.04.01" -lifecycle = "2.8.7" -navigation = "2.8.9" -activity-compose = "1.10.1" -kotlinx-coroutines = "1.10.2" -coil-compose = "2.7.0" -compose-destination = "2.1.0" -sheets-compose-dialogs = "1.3.0" -markdown = "4.6.2" -webkit = "1.13.0" -appiconloader-coil = "1.5.0" -parcelablelist = "2.0.1" -libsu = "6.0.0" -apksign = "1.4" -cmaker = "1.2" -compose-material = "1.8.0" -compose-material3 = "1.3.2" -compose-ui = "1.8.0" -compose-foundation = "1.7.8" -documentfile = "1.0.1" - -[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.ref = "apksign" } -lsplugin-cmaker = { id = "org.lsposed.lsplugin.cmaker", version.ref = "cmaker" } - -[libraries] -androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } - -androidx-foundation = { module = "androidx.compose.foundation:foundation" } -androidx-material3 = { module = "androidx.compose.material3:material3" } -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" } -androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } -androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "compose-material" } -androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "compose-material3" } -androidx-compose-ui = { group = "androidx.compose.ui", name = "ui", version.ref = "compose-ui" } -androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview", version.ref = "compose-ui" } -androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } - -androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" } -androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } -androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } - -androidx-ui = { module = "androidx.compose.ui:ui" } -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.ref = "parcelablelist" } - -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.ref = "kotlinx-coroutines" } - -me-zhanghai-android-appiconloader-coil = { group = "me.zhanghai.android.appiconloader", name = "appiconloader-coil", version.ref = "appiconloader-coil" } - -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" } - -markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } - -lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "27.0.12077973" } -androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" } \ No newline at end of file diff --git a/manager/gradle/wrapper/gradle-wrapper.jar b/manager/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index a4b76b95..00000000 Binary files a/manager/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/manager/gradle/wrapper/gradle-wrapper.properties b/manager/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index e18bc253..00000000 --- a/manager/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/manager/gradlew b/manager/gradlew deleted file mode 100755 index f3b75f3b..00000000 --- a/manager/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# 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 -# -# https://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. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# 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/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# 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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -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=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -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, 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" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/manager/gradlew.bat b/manager/gradlew.bat deleted file mode 100644 index 9d21a218..00000000 --- a/manager/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@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 deleted file mode 100644 index 2230bf48..00000000 --- a/manager/settings.gradle.kts +++ /dev/null @@ -1,21 +0,0 @@ -@file:Suppress("UnstableApiUsage") - -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - -pluginManagement { - repositories { - google() - mavenCentral() - } -} - -dependencyResolutionManagement { - repositories { - google() - mavenCentral() - maven("https://jitpack.io") - } -} - -rootProject.name = "KernelSU" -include(":app") diff --git a/manager/sign.example.properties b/manager/sign.example.properties deleted file mode 100644 index bc70a60c..00000000 --- a/manager/sign.example.properties +++ /dev/null @@ -1,4 +0,0 @@ -KEYSTORE_FILE= -KEYSTORE_PASSWORD= -KEY_ALIAS= -KEY_PASSWORD= diff --git a/scripts/abi_gki_all.py b/scripts/abi_gki_all.py deleted file mode 100644 index d433634a..00000000 --- a/scripts/abi_gki_all.py +++ /dev/null @@ -1,10 +0,0 @@ -from xml.dom.minidom import parse -import xml.dom.minidom -import sys - - -DOMTree = xml.dom.minidom.parse(sys.argv[1]) -symbols = DOMTree.getElementsByTagName("elf-symbol") -print("[abi_symbol_list]") -for symbol in symbols: - print(" " + symbol.getAttribute("name")) diff --git a/scripts/add_device_handler.py b/scripts/add_device_handler.py deleted file mode 100644 index f18ad3a2..00000000 --- a/scripts/add_device_handler.py +++ /dev/null @@ -1,50 +0,0 @@ -import json -import sys -import os - - -def main(): - assert len(sys.argv) == 2 - file_name = sys.argv[1] - github = "https://github.com/" - issue_content = os.environ["ISSUE_CONTENT"] - lines = issue_content.split("\n\n") - assert len(lines) == 6 - url = lines[1] - print(url) - device = lines[3] - print(device) - code_of_conduct = lines[5] - print(code_of_conduct) - assert code_of_conduct.find("[X]") > 0 - tmp = url.removesuffix("/").replace(github, "").split("/") - print(tmp) - assert len(tmp) == 2 - maintainer = tmp[0] - print(maintainer) - maintainer_link = "%s%s" % (github, maintainer) - print(maintainer_link) - kernel_name = tmp[1] - print(kernel_name) - kernel_link = "%s%s/%s" % (github, maintainer, kernel_name) - print(kernel_link) - with open(file_name, "r") as f: - data = json.loads(f.read()) - data.append( - { - "maintainer": maintainer, - "maintainer_link": maintainer_link, - "kernel_name": kernel_name, - "kernel_link": kernel_link, - "devices": device, - } - ) - os.remove(file_name) - with open(file_name, "w") as f: - f.write(json.dumps(data, indent=4)) - os.system("echo success=true >> $GITHUB_OUTPUT") - os.system("echo device=%s >> $GITHUB_OUTPUT" % device) - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/scripts/allowlist.bt b/scripts/allowlist.bt deleted file mode 100644 index d95bb5ac..00000000 --- a/scripts/allowlist.bt +++ /dev/null @@ -1,89 +0,0 @@ -// 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/bin2c.py b/scripts/bin2c.py deleted file mode 100644 index 58513132..00000000 --- a/scripts/bin2c.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/python3 - -import argparse -import os -import re - -line_size = 80 - - -def bin2c(filename, varname='data'): - if not os.path.isfile(filename): - print('File "%s" is not found!' % filename) - return '' - if not re.match('[a-zA-Z_][a-zA-Z0-9_]*', varname): - print('Invalid variable name "%s"' % varname) - return - with open(filename, 'rb') as in_file: - data = in_file.read() - # limit the line length - byte_len = 6 # '0x00, ' - out = 'unsigned int %s_size = %d;\n' \ - 'const char %s[%d] = {\n' % (varname, len(data), varname, len(data)) - line = '' - for byte in data: - line += '0x%02x, ' % byte - if len(line) + 4 + byte_len >= line_size: - out += ' ' * 4 + line + '\n' - line = '' - # add the last line - if len(line) + 4 + byte_len < line_size: - out += ' ' * 4 + line + '\n' - # strip the last comma - out = out.rstrip(', \n') + '\n' - out += '};' - return out - - -def main(): - """ Main func """ - parser = argparse.ArgumentParser() - parser.add_argument( - 'filename', help='filename to convert to C array') - parser.add_argument( - 'varname', nargs='?', help='variable name', default='data') - args = parser.parse_args() - # print out the data - print(bin2c(args.filename, args.varname)) - - -if __name__ == '__main__': - main() diff --git a/scripts/ksubot.py b/scripts/ksubot.py deleted file mode 100644 index 3d254488..00000000 --- a/scripts/ksubot.py +++ /dev/null @@ -1,106 +0,0 @@ -import asyncio -import os -import sys -from telethon import TelegramClient - -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: - try: - CHAT_ID = int(CHAT_ID) - except: - pass - 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 not None and MESSAGE_THREAD_ID != "": - try: - MESSAGE_THREAD_ID = int(MESSAGE_THREAD_ID) - except: - print("[-] Invaild MESSAGE_THREAD_ID") - exit(1) - else: - MESSAGE_THREAD_ID = None - - -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/kpmmgr/.gitignore b/userspace/kpmmgr/.gitignore deleted file mode 100644 index 4b4ce0f2..00000000 --- a/userspace/kpmmgr/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/obj -/libs \ No newline at end of file diff --git a/userspace/kpmmgr/jni/Android.mk b/userspace/kpmmgr/jni/Android.mk deleted file mode 100644 index a278158f..00000000 --- a/userspace/kpmmgr/jni/Android.mk +++ /dev/null @@ -1,6 +0,0 @@ -LOCAL_PATH := $(call my-dir) - -include $(CLEAR_VARS) -LOCAL_MODULE := kpmmgr -LOCAL_SRC_FILES := kpmmgr.c -include $(BUILD_EXECUTABLE) diff --git a/userspace/kpmmgr/jni/Application.mk b/userspace/kpmmgr/jni/Application.mk deleted file mode 100644 index 61d31236..00000000 --- a/userspace/kpmmgr/jni/Application.mk +++ /dev/null @@ -1,3 +0,0 @@ -APP_ABI := arm64-v8a -APP_PLATFORM := android-24 -APP_STL := none diff --git a/userspace/kpmmgr/jni/kpmmgr.c b/userspace/kpmmgr/jni/kpmmgr.c deleted file mode 100644 index 7c4d9a7f..00000000 --- a/userspace/kpmmgr/jni/kpmmgr.c +++ /dev/null @@ -1,118 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#define KERNEL_SU_OPTION 0xDEADBEEF -#define KSU_OPTIONS 0xdeadbeef - -// KPM控制代码 -#define CMD_KPM_CONTROL 28 -#define CMD_KPM_CONTROL_MAX 7 - -// 控制代码 -// prctl(xxx, 28, "PATH", "ARGS") -// success return 0, error return -N -#define SUKISU_KPM_LOAD 28 - -// prctl(xxx, 29, "NAME") -// success return 0, error return -N -#define SUKISU_KPM_UNLOAD 29 - -// num = prctl(xxx, 30) -// error return -N -// success return +num or 0 -#define SUKISU_KPM_NUM 30 - -// prctl(xxx, 31, Buffer, BufferSize) -// success return +out, error return -N -#define SUKISU_KPM_LIST 31 - -// prctl(xxx, 32, "NAME", Buffer[256]) -// success return +out, error return -N -#define SUKISU_KPM_INFO 32 - -// prctl(xxx, 33, "NAME", "ARGS") -// success return KPM's result value -// error return -N -#define SUKISU_KPM_CONTROL 33 - -// prctl(xxx, 34, buffer, bufferSize) -// success return KPM's result value -// error return -N -#define SUKISU_KPM_VERSION 34 - -#define CONTROL_CODE(n) (n) - -void print_usage(const char *prog) { - printf("Usage: %s [args]\n", prog); - printf("Commands:\n"); - printf(" load Load a KPM module\n"); - printf(" unload Unload a KPM module\n"); - printf(" num Get number of loaded modules\n"); - printf(" list List loaded KPM modules\n"); - printf(" info Get info of a KPM module\n"); - printf(" control Send control command to a KPM module\n"); - printf(" version Print KPM Loader version\n"); -} - -int main(int argc, char *argv[]) { - if (argc < 2) { - print_usage(argv[0]); - return 1; - } - - int ret = -1; - int out = -1; // 存储返回值 - - if (strcmp(argv[1], "load") == 0 && argc >= 3) { - // 加载 KPM 模块 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_LOAD), argv[2], (argc > 3 ? argv[3] : NULL), &out); - if(out > 0) { - printf("Success"); - } - } else if (strcmp(argv[1], "unload") == 0 && argc >= 3) { - // 卸载 KPM 模块 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_UNLOAD), argv[2], NULL, &out); - } else if (strcmp(argv[1], "num") == 0) { - // 获取加载的 KPM 数量 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_NUM), NULL, NULL, &out); - printf("%d", out); - return 0; - } else if (strcmp(argv[1], "list") == 0) { - // 获取模块列表 - char buffer[1024] = {0}; - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_LIST), buffer, sizeof(buffer), &out); - if (out >= 0) { - printf("%s", buffer); - } - } else if (strcmp(argv[1], "info") == 0 && argc >= 3) { - // 获取指定模块信息 - char buffer[256] = {0}; - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_INFO), argv[2], buffer, &out); - if (out >= 0) { - printf("%s\n", buffer); - } - } else if (strcmp(argv[1], "control") == 0 && argc >= 4) { - // 控制 KPM 模块 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_CONTROL), argv[2], argv[3], &out); - } else if (strcmp(argv[1], "version") == 0) { - char buffer[1024] = {0}; - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_VERSION), buffer, sizeof(buffer), &out); - if (out >= 0) { - printf("%s", buffer); - } - } else { - print_usage(argv[0]); - return 1; - } - - if (out < 0) { - printf("Error: %s\n", strerror(-out)); - return -1; - } - - return 0; -} diff --git a/userspace/ksud/.gitignore b/userspace/ksud/.gitignore deleted file mode 100644 index 3c71873d..00000000 --- a/userspace/ksud/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -.cargo/ \ No newline at end of file diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock deleted file mode 100644 index 76b21a4c..00000000 --- a/userspace/ksud/Cargo.lock +++ /dev/null @@ -1,1615 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" - -[[package]] -name = "adler32" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" - -[[package]] -name = "ahash" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android-properties" -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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ecc8056bf6ab9892dcd53216c83d1597487d7dacac16c8df6b877d127df9937" - -[[package]] -name = "android_logger" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b07e8e73d720a1f2e4b6014766e6039fd2e96a4fa44e2a78d0e1fa2ff49826" -dependencies = [ - "android_log-sys", - "env_filter", - "log", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -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.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" -dependencies = [ - "anstyle", - "once_cell", - "windows-sys 0.59.0", -] - -[[package]] -name = "anyhow" -version = "1.0.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4" - -[[package]] -name = "arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" -dependencies = [ - "derive_arbitrary", -] - -[[package]] -name = "async-trait" -version = "0.1.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "backtrace" -version = "0.3.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets", -] - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9" - -[[package]] -name = "cc" -version = "1.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af" -dependencies = [ - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chrono" -version = "0.4.39" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" -dependencies = [ - "android-tzdata", - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-targets", -] - -[[package]] -name = "clap" -version = "4.5.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", -] - -[[package]] -name = "clap_derive" -version = "4.5.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" - -[[package]] -name = "colorchoice" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" - -[[package]] -name = "const_format" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -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.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-queue" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "dary_heap" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04d2cd9c18b9f454ed67da600630b021a8a80bf33f8c95896ab33aaf1c26b728" - -[[package]] -name = "deflate64" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" - -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "derive-new" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cdc8d50f426189eef89dac62fabfa0abb27d5cc008f25bf4156a0203325becc" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "either" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", -] - -[[package]] -name = "env_home" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" - -[[package]] -name = "env_logger" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" -dependencies = [ - "env_filter", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - -[[package]] -name = "errno" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - -[[package]] -name = "extattr" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b59f8a77817ff1b795adafc535941bdf664184f5f95e0b6d1d77dd6d12815dc" -dependencies = [ - "bitflags 1.3.2", - "errno 0.2.8", - "libc", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "flate2" -version = "1.0.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fs4" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be058769cf1633370c3d0dac6bb9b223b8f18900cf808abadf7843192e706238" -dependencies = [ - "rustix 0.38.44", - "windows-sys 0.59.0", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getopts" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "getrandom" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8" -dependencies = [ - "cfg-if", - "libc", - "wasi", - "windows-targets", -] - -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", -] - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "humansize" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" -dependencies = [ - "libm", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "include-flate" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df49c16750695486c1f34de05da5b7438096156466e7f76c38fcdf285cf0113e" -dependencies = [ - "include-flate-codegen", - "lazy_static", - "libflate", -] - -[[package]] -name = "include-flate-codegen" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c5b246c6261be723b85c61ecf87804e8ea4a35cb68be0ff282ed84b95ffe7d7" -dependencies = [ - "libflate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "indexmap" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" -dependencies = [ - "equivalent", - "hashbrown 0.15.2", -] - -[[package]] -name = "is_executable" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2" -dependencies = [ - "winapi", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" - -[[package]] -name = "itoa" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" - -[[package]] -name = "java-properties" -version = "2.0.0" -source = "git+https://github.com/Kernel-SU/java-properties.git?branch=master#42a4aa941b70ded2dd3be9e9f892471023e70229" -dependencies = [ - "encoding_rs", - "lazy_static", - "regex-lite", -] - -[[package]] -name = "js-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jwalk" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2735847566356cd2179a2a38264839308f7079fa96e6bd5a42d740460e003c56" -dependencies = [ - "crossbeam", - "rayon", -] - -[[package]] -name = "zakozako" -version = "0.1.0" -dependencies = [ - "android-properties", - "android_logger", - "anyhow", - "chrono", - "clap", - "const_format", - "derive-new", - "encoding_rs", - "env_logger", - "extattr", - "fs4", - "getopts", - "humansize", - "is_executable", - "java-properties", - "jwalk", - "libc", - "log", - "loopdev", - "nom", - "procfs", - "regex-lite", - "rust-embed", - "rustix 0.38.34", - "serde_json", - "sha1", - "sha256", - "tempfile", - "which", - "zip", - "zip-extensions", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.170" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" - -[[package]] -name = "libflate" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d9dfdc14ea4ef0900c1cddbc8dcd553fbaacd8a4a282cf4018ae9dd04fb21e" -dependencies = [ - "adler32", - "core2", - "crc32fast", - "dary_heap", - "libflate_lz77", -] - -[[package]] -name = "libflate_lz77" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e0d73b369f386f1c44abd9c570d5318f55ccde816ff4b562fa452e5182863d" -dependencies = [ - "core2", - "hashbrown 0.14.5", - "rle-decode-fast", -] - -[[package]] -name = "libm" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - -[[package]] -name = "log" -version = "0.4.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" - -[[package]] -name = "loopdev" -version = "0.5.0" -source = "git+https://github.com/Kernel-SU/loopdev#7a921f8d966477a645b1188732fac486c71a68ef" -dependencies = [ - "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.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "miniz_oxide" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" -dependencies = [ - "adler2", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "proc-macro2" -version = "1.0.93" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "procfs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" -dependencies = [ - "bitflags 2.8.0", - "chrono", - "flate2", - "hex", - "procfs-core", - "rustix 0.38.44", -] - -[[package]] -name = "procfs-core" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" -dependencies = [ - "bitflags 2.8.0", - "chrono", - "hex", -] - -[[package]] -name = "quote" -version = "1.0.38" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rayon" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "regex-lite" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" - -[[package]] -name = "rle-decode-fast" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" - -[[package]] -name = "rust-embed" -version = "8.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" -dependencies = [ - "include-flate", - "rust-embed-impl", - "rust-embed-utils", - "walkdir", -] - -[[package]] -name = "rust-embed-impl" -version = "8.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" -dependencies = [ - "proc-macro2", - "quote", - "rust-embed-utils", - "syn", - "walkdir", -] - -[[package]] -name = "rust-embed-utils" -version = "8.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" -dependencies = [ - "sha2", - "walkdir", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" - -[[package]] -name = "rustix" -version = "0.38.34" -source = "git+https://github.com/Kernel-SU/rustix.git?branch=main#4a53fbc7cb7a07cabe87125cc21dbc27db316259" -dependencies = [ - "bitflags 2.8.0", - "errno 0.3.10", - "itoa", - "libc", - "linux-raw-sys", - "once_cell", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.8.0", - "errno 0.3.10", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustversion" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" - -[[package]] -name = "ryu" -version = "1.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "serde" -version = "1.0.218" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.218" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6" -dependencies = [ - "itoa", - "memchr", - "ryu", - "serde", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha256" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" -dependencies = [ - "async-trait", - "bytes", - "hex", - "sha2", - "tokio", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "simd-adler32" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "syn" -version = "2.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" -dependencies = [ - "cfg-if", - "fastrand", - "getrandom", - "once_cell", - "rustix 0.38.44", - "windows-sys 0.59.0", -] - -[[package]] -name = "thiserror" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "time" -version = "0.3.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "tokio" -version = "1.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" -dependencies = [ - "backtrace", - "bytes", - "pin-project-lite", -] - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.13.3+wasi-0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2" -dependencies = [ - "wit-bindgen-rt", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "which" -version = "7.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283" -dependencies = [ - "either", - "env_home", - "rustix 0.38.44", - "winsafe", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" -dependencies = [ - "windows-sys 0.59.0", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -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.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -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", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -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 = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - -[[package]] -name = "wit-bindgen-rt" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" -dependencies = [ - "bitflags 2.8.0", -] - -[[package]] -name = "zerocopy" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.7.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zip" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae9c1ea7b3a5e1f4b922ff856a129881167511563dc219869afe3787fc0c1a45" -dependencies = [ - "arbitrary", - "crc32fast", - "crossbeam-utils", - "deflate64", - "displaydoc", - "flate2", - "indexmap", - "lzma-rs", - "memchr", - "thiserror", - "time", - "zopfli", -] - -[[package]] -name = "zip-extensions" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "386508a00aae1d8218b9252a41f59bba739ccee3f8e420bb90bcb1c30d960d4a" -dependencies = [ - "zip", -] - -[[package]] -name = "zopfli" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" -dependencies = [ - "bumpalo", - "crc32fast", - "lockfree-object-pool", - "log", - "once_cell", - "simd-adler32", -] diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml deleted file mode 100644 index 9c82f41c..00000000 --- a/userspace/ksud/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "zakozako" -version = "0.1.0" -edition = "2024" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -notify = "6.1" -anyhow = "1" -clap = { version = "4", features = ["derive"] } -const_format = "0.2" -zip = { version = "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_json = "1" -encoding_rs = "0.8" -humansize = "2" -libc = "0.2" -extattr = "1" -jwalk = "0.8" -is_executable = "1" -nom = "8" -derive-new = "0.7" -rust-embed = { version = "8", features = [ - "debug-embed", - "compression", # must clean build after updating binaries -] } -which = "7" -getopts = "0.2" -sha256 = "1" -sha1 = "0.10" -tempfile = "3" -chrono = "0.4" -regex-lite = "0.1" -fs4 = "0.13" - -[target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies] -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", features = ["bionic-deprecated"] } -procfs = "0.17" -loopdev = { git = "https://github.com/Kernel-SU/loopdev" } - -[target.'cfg(target_os = "android")'.dependencies] -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 deleted file mode 100644 index 1464b7ed..00000000 --- a/userspace/ksud/bin/.gitignore +++ /dev/null @@ -1 +0,0 @@ -**/*.ko \ No newline at end of file diff --git a/userspace/ksud/bin/aarch64/bootctl b/userspace/ksud/bin/aarch64/bootctl deleted file mode 100644 index cf5c6136..00000000 Binary files a/userspace/ksud/bin/aarch64/bootctl and /dev/null differ diff --git a/userspace/ksud/bin/aarch64/busybox b/userspace/ksud/bin/aarch64/busybox deleted file mode 100755 index 2fd8bcfa..00000000 Binary files a/userspace/ksud/bin/aarch64/busybox and /dev/null differ diff --git a/userspace/ksud/bin/aarch64/ksuinit b/userspace/ksud/bin/aarch64/ksuinit deleted file mode 100755 index 8d3ba057..00000000 Binary files a/userspace/ksud/bin/aarch64/ksuinit and /dev/null differ diff --git a/userspace/ksud/bin/aarch64/resetprop b/userspace/ksud/bin/aarch64/resetprop deleted file mode 100644 index 305032c9..00000000 Binary files a/userspace/ksud/bin/aarch64/resetprop and /dev/null differ diff --git a/userspace/ksud/bin/x86_64/busybox b/userspace/ksud/bin/x86_64/busybox deleted file mode 100755 index 3603e741..00000000 Binary files a/userspace/ksud/bin/x86_64/busybox and /dev/null differ diff --git a/userspace/ksud/bin/x86_64/ksuinit b/userspace/ksud/bin/x86_64/ksuinit deleted file mode 100755 index 9517baf9..00000000 Binary files a/userspace/ksud/bin/x86_64/ksuinit and /dev/null differ diff --git a/userspace/ksud/bin/x86_64/resetprop b/userspace/ksud/bin/x86_64/resetprop deleted file mode 100644 index 80030612..00000000 Binary files a/userspace/ksud/bin/x86_64/resetprop and /dev/null differ diff --git a/userspace/ksud/build.rs b/userspace/ksud/build.rs deleted file mode 100644 index 3dd54fbe..00000000 --- a/userspace/ksud/build.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::env; -use std::fs::File; -use std::io::Write; -use std::path::Path; -use std::process::Command; - -fn get_git_version() -> Result<(u32, String), std::io::Error> { - let output = Command::new("git") - .args(["rev-list", "--count", "HEAD"]) - .output()?; - - let output = output.stdout; - let version_code = String::from_utf8(output).expect("Failed to read git count stdout"); - let version_code: u32 = version_code - .trim() - .parse() - .map_err(|_| std::io::Error::new(std::io::ErrorKind::Other, "Failed to parse git count"))?; - let version_code = 10000 + 606 + version_code; // For historical reasons - - let version_name = String::from_utf8( - Command::new("git") - .args(["describe", "--tags", "--always"]) - .output()? - .stdout, - ) - .map_err(|_| { - std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to read git describe stdout", - ) - })?; - let version_name = version_name.trim_start_matches('v').to_string(); - Ok((version_code, version_name)) -} - -fn main() { - let (code, name) = match get_git_version() { - Ok((code, name)) => (code, name), - Err(_) => { - // show warning if git is not installed - println!("cargo:warning=Failed to get git version, using 0.0.0"); - (0, "0.0.0".to_string()) - } - }; - let out_dir = env::var("OUT_DIR").expect("Failed to get $OUT_DIR"); - let out_dir = Path::new(&out_dir); - File::create(Path::new(out_dir).join("VERSION_CODE")) - .expect("Failed to create VERSION_CODE") - .write_all(code.to_string().as_bytes()) - .expect("Failed to write VERSION_CODE"); - - File::create(Path::new(out_dir).join("VERSION_NAME")) - .expect("Failed to create VERSION_NAME") - .write_all(name.trim().as_bytes()) - .expect("Failed to write VERSION_NAME"); -} diff --git a/userspace/ksud/src/apk_sign.rs b/userspace/ksud/src/apk_sign.rs deleted file mode 100644 index 4b2c4a98..00000000 --- a/userspace/ksud/src/apk_sign.rs +++ /dev/null @@ -1,115 +0,0 @@ -use anyhow::{Result, ensure}; -use std::io::{Read, Seek, SeekFrom}; - -pub fn get_apk_signature(apk: &str) -> Result<(u32, String)> { - let mut buffer = [0u8; 0x10]; - let mut size4 = [0u8; 4]; - let mut size8 = [0u8; 8]; - let mut size_of_block = [0u8; 8]; - - let mut f = std::fs::File::open(apk)?; - - let mut i = 0; - loop { - let mut n = [0u8; 2]; - f.seek(SeekFrom::End(-i - 2))?; - f.read_exact(&mut n)?; - - let n = u16::from_le_bytes(n); - if i64::from(n) == i { - f.seek(SeekFrom::Current(-22))?; - f.read_exact(&mut size4)?; - - if u32::from_le_bytes(size4) ^ 0xcafe_babe_u32 == 0xccfb_f1ee_u32 { - if i > 0 { - println!("warning: comment length is {i}"); - } - break; - } - } - - ensure!(n != 0xffff, "not a zip file"); - - i += 1; - } - - f.seek(SeekFrom::Current(12))?; - // offset - f.read_exact(&mut size4)?; - f.seek(SeekFrom::Start(u64::from(u32::from_le_bytes(size4)) - 0x18))?; - - f.read_exact(&mut size8)?; - f.read_exact(&mut buffer)?; - - ensure!(&buffer == b"APK Sig Block 42", "Can not found sig block"); - - let pos = u64::from(u32::from_le_bytes(size4)) - (u64::from_le_bytes(size8) + 0x8); - f.seek(SeekFrom::Start(pos))?; - f.read_exact(&mut size_of_block)?; - - ensure!(size_of_block == size8, "not a signed apk"); - - let mut v2_signing: Option<(u32, String)> = None; - let mut v3_signing_exist = false; - let mut v3_1_signing_exist = false; - - loop { - let mut id = [0u8; 4]; - let mut offset = 4u32; - - f.read_exact(&mut size8)?; // sequence length - if size8 == size_of_block { - break; - } - - f.read_exact(&mut id)?; // id - - let id = u32::from_le_bytes(id); - if id == 0x7109_871a_u32 { - v2_signing = Some(calc_cert_sha256(&mut f, &mut size4, &mut offset)?); - } else if id == 0xf053_68c0_u32 { - // v3 signature scheme - v3_signing_exist = true; - } else if id == 0x1b93_ad61_u32 { - // v3.1 signature scheme: credits to vvb2060 - v3_1_signing_exist = true; - } - - f.seek(SeekFrom::Current( - i64::from_le_bytes(size8) - i64::from(offset), - ))?; - } - - if v3_signing_exist || v3_1_signing_exist { - return Err(anyhow::anyhow!("Unexpected v3 signature found!",)); - } - - v2_signing.ok_or(anyhow::anyhow!("No signature found!")) -} - -fn calc_cert_sha256( - f: &mut std::fs::File, - size4: &mut [u8; 4], - offset: &mut u32, -) -> Result<(u32, String)> { - f.read_exact(size4)?; // signer-sequence length - f.read_exact(size4)?; // signer length - f.read_exact(size4)?; // signed data length - *offset += 0x4 * 3; - - f.read_exact(size4)?; // digests-sequence length - let pos = u32::from_le_bytes(*size4); // skip digests - f.seek(SeekFrom::Current(i64::from(pos)))?; - *offset += 0x4 + pos; - - f.read_exact(size4)?; // certificates length - f.read_exact(size4)?; // certificate length - *offset += 0x4 * 2; - - let cert_len = u32::from_le_bytes(*size4); - let mut cert: Vec = vec![0; cert_len as usize]; - f.read_exact(&mut cert)?; - *offset += cert_len; - - Ok((cert_len, sha256::digest(&cert))) -} diff --git a/userspace/ksud/src/assets.rs b/userspace/ksud/src/assets.rs deleted file mode 100644 index e8255c82..00000000 --- a/userspace/ksud/src/assets.rs +++ /dev/null @@ -1,50 +0,0 @@ -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(all(target_arch = "x86_64", target_os = "android"))] -#[derive(RustEmbed)] -#[folder = "bin/x86_64"] -struct Asset; - -// 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/aarch64"] -struct Asset; - -pub fn ensure_binaries(ignore_if_exist: bool) -> Result<()> { - for file in Asset::iter() { - 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/banner b/userspace/ksud/src/banner deleted file mode 100644 index f2812bf2..00000000 --- a/userspace/ksud/src/banner +++ /dev/null @@ -1,10 +0,0 @@ - ____ _ _ ____ _ _ -/ ___| _ _| | _(_) ___|| | | | -\___ \| | | | |/ / \___ \| | | | - ___) | |_| | <| |___) | |_| | -|____/ \__,_|_|\_\_|____/ \___/ - _ _ _ _ - | | | | | |_ _ __ __ _ - | | | | | __| '__/ _\ | - | |_| | | |_| | | (_| | - \___/|_|\__|_| \__,_| \ No newline at end of file diff --git a/userspace/ksud/src/boot_patch.rs b/userspace/ksud/src/boot_patch.rs deleted file mode 100644 index b0f2caed..00000000 --- a/userspace/ksud/src/boot_patch.rs +++ /dev/null @@ -1,714 +0,0 @@ -#[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::Context; -use anyhow::Result; -use anyhow::anyhow; -use anyhow::bail; -use anyhow::ensure; -use regex_lite::Regex; -use which::which; - -use crate::defs; -use crate::defs::BACKUP_FILENAME; -use crate::defs::{KSU_BACKUP_DIR, KSU_BACKUP_FILE_PREFIX}; -use crate::{assets, utils}; - -#[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().is_some_and(|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::{File, copy}; - 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 deleted file mode 100644 index a336cbe4..00000000 --- a/userspace/ksud/src/cli.rs +++ /dev/null @@ -1,390 +0,0 @@ -use anyhow::{Ok, Result}; -use clap::Parser; -use std::path::{Path, PathBuf}; - -#[cfg(target_os = "android")] -use android_logger::Config; -#[cfg(target_os = "android")] -use log::LevelFilter; - -use crate::defs::KSUD_VERBOSE_LOG_FILE; -use crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, utils}; - -/// KernelSU userspace cli -#[derive(Parser, Debug)] -#[command(author, version = defs::VERSION_NAME, about, long_about = None)] -struct Args { - #[command(subcommand)] - command: Commands, - - #[arg(short, long, default_value_t = cfg!(debug_assertions))] - verbose: bool, -} - -#[derive(clap::Subcommand, Debug)] -enum Commands { - /// Manage KernelSU modules - Module { - #[command(subcommand)] - command: Module, - }, - - /// Trigger `post-fs-data` event - PostFsData, - - /// Trigger `service` event - Services, - - /// Trigger `boot-complete` event - BootCompleted, - - /// Install KernelSU userspace component to system - 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 { - #[command(subcommand)] - command: Sepolicy, - }, - - /// Manage App Profiles - Profile { - #[command(subcommand)] - 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. - SetManager { - /// manager package name - #[arg(default_value_t = String::from("me.weishu.kernelsu"))] - apk: String, - }, - - /// Get apk size and hash - GetSign { - /// apk path - apk: String, - }, - - /// Root Shell - Su { - /// switch to gloabl mount namespace - #[arg(short, long, default_value = "false")] - global_mnt: bool, - }, - - /// Get kernel version - Version, - - Mount, - - /// For testing - Test, -} - -#[derive(clap::Subcommand, Debug)] -enum Sepolicy { - /// Patch sepolicy - Patch { - /// sepolicy statements - sepolicy: String, - }, - - /// Apply sepolicy from file - Apply { - /// sepolicy file path - file: String, - }, - - /// Check if sepolicy statement is supported/valid - Check { - /// sepolicy statements - sepolicy: String, - }, -} - -#[derive(clap::Subcommand, Debug)] -enum Module { - /// Install module - Install { - /// module zip file path - zip: String, - }, - - /// Uninstall module - Uninstall { - /// module id - id: String, - }, - - /// Restore module - Restore { - /// module id - id: String, - }, - - /// enable module - Enable { - /// module id - id: String, - }, - - /// disable module - Disable { - // module id - id: String, - }, - - /// run action for module - Action { - // module id - id: String, - }, - - /// list all modules - List, -} - -#[derive(clap::Subcommand, Debug)] -enum Profile { - /// get root profile's selinux policy of - GetSepolicy { - /// package name - package: String, - }, - - /// set root profile's selinux policy of to - SetSepolicy { - /// package name - package: String, - /// policy statements - policy: String, - }, - - /// get template of - GetTemplate { - /// template id - id: String, - }, - - /// set template of to