[
  {
    "path": ".gitattributes",
    "content": "*.bat eol=crlf"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: tiann\npatreon: weishu\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug report\ndescription: Create a report to help us improve KernelSU\nlabels: [Bug]\n\nbody:\n  - type: checkboxes\n    attributes:\n       label: Please check before submitting an issue\n       options:\n          - label: I have searched the issues and haven't found anything relevant\n            required: true\n            \n          - label: I will upload bugreport file in KernelSU Manager - Settings - Report log\n            required: true\n            \n          - label: I know how to reproduce the issue which may not be specific to my device\n            required: false\n\n\n  - type: textarea\n    attributes:\n        label: Describe the bug\n        description: A clear and concise description of what the bug is\n    validations:\n        required: true\n      \n\n  - type: textarea\n    attributes:\n        label: To Reproduce\n        description: Steps to reproduce the behaviour\n        placeholder: |\n          - 1. Go to '...'\n          - 2. Click on '....'\n          - 3. Scroll down to '....'\n          - 4. See error\n          \n\n  - type: textarea\n    attributes:\n        label: Expected behavior\n        description: A clear and concise description of what you expected to happen.\n    \n    \n  - type: textarea\n    attributes:\n        label: Screenshots\n        description: If applicable, add screenshots to help explain your problem.\n        \n        \n  - type: textarea\n    attributes:\n        label: Logs\n        description: If applicable, add crash or any other logs to help us figure out the problem.\n        \n        \n  - type: textarea\n    attributes:\n        label: Device info\n        value: |\n          - Device:\n          - OS Version:\n          - KernelSU Version:\n          - Kernel Version:\n    validations:\n        required: true\n\n\n  - type: textarea\n    attributes:\n        label: Additional context\n        description: Add any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: Feature Request\n    url: https://github.com/tiann/KernelSU/issues/1705\n    about: \"We do not accept external Feature Requests, see this link for more details.\"\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/custom.yml",
    "content": "name: Custom issue template\ndescription: WARNING! If you are reporting a bug but use this template, the issue will be closed directly.\ntitle: '[Custom]'\nbody:\n  - type: textarea\n    id: description\n    attributes:\n      label: \"Describe your problem.\"\n    validations:\n      required: true\n\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: github-actions\n    directory: /\n    schedule:\n      interval: weekly\n    groups:\n      actions:\n        patterns:\n          - \"*\"\n  - package-ecosystem: cargo\n    directory: userspace/ksud\n    schedule:\n      interval: weekly\n    allow:\n      - dependency-type: \"all\"\n    groups:\n      crates:\n        patterns:\n          - \"*\"\n  - package-ecosystem: cargo\n    directory: userspace/ksuinit\n    schedule:\n      interval: weekly\n    allow:\n      - dependency-type: \"all\"\n    groups:\n      crates:\n        patterns:\n          - \"*\"\n  - package-ecosystem: gradle\n    directory: manager\n    schedule:\n      interval: weekly\n    groups:\n      maven:\n        patterns:\n          - \"*\"\n  - package-ecosystem: npm\n    directory: website\n    schedule:\n      interval: weekly\n    allow:\n      - dependency-type: \"all\"\n    groups:\n      npm:\n        patterns:\n          - \"*\"\n"
  },
  {
    "path": ".github/workflows/build-lkm.yml",
    "content": "name: Build LKM for KernelSU\non:\n  workflow_call:\n    inputs:\n      expected_size2:\n        description: 'Second expected signature size (for PR builds)'\n        required: false\n        default: ''\n        type: string\n      expected_hash2:\n        description: 'Second expected signature hash (for PR builds)'\n        required: false\n        default: ''\n        type: string\n  workflow_dispatch:\njobs:\n  build-lkm:\n    strategy:\n      matrix:\n        kmi:\n          - android12-5.10\n          - android13-5.10\n          - android13-5.15\n          - android14-5.15\n          - android14-6.1\n          - android15-6.6\n          - android16-6.12\n    uses: ./.github/workflows/ddk-lkm.yml\n    with:\n      kmi: ${{ matrix.kmi }}\n      ddk_release: '20260313'\n      expected_size2: ${{ inputs.expected_size2 || '' }}\n      expected_hash2: ${{ inputs.expected_hash2 || '' }}\n"
  },
  {
    "path": ".github/workflows/build-manager.yml",
    "content": "name: Build Manager\n\non:\n  push:\n    branches: [ \"main\", \"dev\", \"ci\" ]\n    paths:\n      - '.github/workflows/build-manager.yml'\n      - '.github/workflows/build-lkm.yml'\n      - '.github/workflows/ddk-lkm.yml'\n      - '.github/workflows/ksud.yml'\n      - 'manager/**'\n      - 'kernel/**'\n      - 'userspace/**'\n  pull_request:\n    branches: [ \"main\", \"dev\" ]\n    paths:\n      - '.github/workflows/build-manager.yml'\n      - '.github/workflows/build-lkm.yml'\n      - '.github/workflows/ddk-lkm.yml'\n      - '.github/workflows/ksud.yml'\n      - 'manager/**'\n      - 'kernel/**'\n      - 'userspace/**'\n  workflow_call:\n\njobs:\n  generate-key:\n    runs-on: ubuntu-latest\n    outputs:\n      expected_size: ${{ steps.extract.outputs.expected_size }}\n      expected_hash: ${{ steps.extract.outputs.expected_hash }}\n      keystore: ${{ steps.gen.outputs.keystore }}\n      keystore_password: ${{ steps.gen.outputs.keystore_password }}\n      key_password: ${{ steps.gen.outputs.key_password }}\n    steps:\n      - name: Generate temporary keystore\n        if: github.event_name == 'pull_request'\n        id: gen\n        run: |\n          KEYSTORE_PASSWORD=$(openssl rand -hex 32)\n          KEY_PASSWORD=$(openssl rand -hex 32)\n          echo \"keystore_password=$KEYSTORE_PASSWORD\" >> $GITHUB_OUTPUT\n          echo \"key_password=$KEY_PASSWORD\" >> $GITHUB_OUTPUT\n\n          keytool -genkeypair \\\n            -alias pr-key \\\n            -keyalg RSA -keysize 2048 \\\n            -validity 1 \\\n            -storepass \"$KEYSTORE_PASSWORD\" \\\n            -keypass \"$KEY_PASSWORD\" \\\n            -dname \"CN=KernelSU PR Build\" \\\n            -storetype JKS \\\n            -keystore pr-key.jks\n\n          echo \"keystore=$(base64 -w 0 pr-key.jks)\" >> $GITHUB_OUTPUT\n\n      - name: Extract certificate hash and size\n        if: github.event_name == 'pull_request'\n        id: extract\n        env:\n          STORE_PASS: ${{ steps.gen.outputs.keystore_password }}\n        run: |\n          # Export DER certificate\n          keytool -exportcert \\\n            -alias pr-key \\\n            -keystore pr-key.jks \\\n            -storepass \"$STORE_PASS\" \\\n            -file pr-cert.der\n\n          # Calculate size in hex\n          SIZE_DEC=$(stat -c%s pr-cert.der)\n          SIZE_HEX=$(printf '0x%04x' \"$SIZE_DEC\")\n          echo \"expected_size=$SIZE_HEX\" >> $GITHUB_OUTPUT\n\n          # Calculate SHA256 hash\n          HASH=$(sha256sum pr-cert.der | awk '{print $1}')\n          echo \"expected_hash=$HASH\" >> $GITHUB_OUTPUT\n\n          echo \"Certificate size: $SIZE_HEX ($SIZE_DEC bytes)\"\n          echo \"Certificate hash: $HASH\"\n\n  build-lkm:\n    needs: generate-key\n    uses: ./.github/workflows/build-lkm.yml\n    with:\n      expected_size2: ${{ needs.generate-key.outputs.expected_size || '' }}\n      expected_hash2: ${{ needs.generate-key.outputs.expected_hash || '' }}\n    secrets: inherit\n\n  build-ksuinit:\n    uses: ./.github/workflows/ksuinit.yml\n\n  build-ksud:\n    needs: [build-lkm, build-ksuinit]\n    strategy:\n      matrix:\n        include:\n          - target: aarch64-linux-android\n            os: ubuntu-latest\n          - target: x86_64-linux-android\n            os: ubuntu-latest\n    uses: ./.github/workflows/ksud.yml\n    with:\n      target: ${{ matrix.target }}\n      os: ${{ matrix.os }}\n\n  build-ksud-extra:\n    needs: [build-lkm, build-ksuinit]\n    strategy:\n      matrix:\n        include:\n          - target: x86_64-pc-windows-gnu # windows pc\n            os: ubuntu-latest\n          - target: x86_64-apple-darwin # Intel mac\n            os: macos-latest\n          - target: aarch64-apple-darwin # M chip mac\n            os: macos-latest\n          - target: aarch64-unknown-linux-musl # arm64 Linux\n            os: ubuntu-latest\n          - target: x86_64-unknown-linux-musl # x86 Linux\n            os: ubuntu-latest\n    uses: ./.github/workflows/ksud.yml\n    with:\n      target: ${{ matrix.target }}\n      os: ${{ matrix.os }}\n\n  build-manager:\n    needs: [build-ksud, generate-key]\n    if: always() && needs.build-ksud.result == 'success' && needs.generate-key.result == 'success'\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./manager\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0\n\n      - name: Setup need_upload\n        id: need_upload\n        run: |\n          if [ ! -z \"${{ secrets.BOT_TOKEN }}\" ]; then\n            echo \"UPLOAD=true\" >> $GITHUB_OUTPUT\n          else\n            echo \"UPLOAD=false\" >> $GITHUB_OUTPUT\n          fi\n\n      - name: Write key\n        if: ${{ ( github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' )) || github.ref_type == 'tag' }}\n        run: |\n          if [ ! -z \"${{ secrets.KEYSTORE }}\" ]; then\n            {\n              echo KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}'\n              echo KEY_ALIAS='${{ secrets.KEY_ALIAS }}'\n              echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}'\n              echo KEYSTORE_FILE='key.jks'\n            } >> gradle.properties\n            echo ${{ secrets.KEYSTORE }} | base64 -d > key.jks\n          fi\n\n      - name: Write PR key\n        if: github.event_name == 'pull_request'\n        env:\n          PR_KEYSTORE: ${{ needs.generate-key.outputs.keystore }}\n          PR_KEYSTORE_PASSWORD: ${{ needs.generate-key.outputs.keystore_password }}\n          PR_KEY_PASSWORD: ${{ needs.generate-key.outputs.key_password }}\n        run: |\n          echo \"$PR_KEYSTORE\" | base64 -d > pr-key.jks\n          {\n            echo KEYSTORE_PASSWORD=\"$PR_KEYSTORE_PASSWORD\"\n            echo KEY_ALIAS='pr-key'\n            echo KEY_PASSWORD=\"$PR_KEY_PASSWORD\"\n            echo KEYSTORE_FILE='pr-key.jks'\n          } >> gradle.properties\n\n      - name: Setup Java\n        uses: actions/setup-java@v5\n        with:\n          distribution: temurin\n          java-version: 21\n\n      - name: Setup Gradle\n        uses: gradle/actions/setup-gradle@v5\n\n      - name: Setup Android SDK\n        uses: android-actions/setup-android@v3\n\n      - name: Download arm64 ksud\n        uses: actions/download-artifact@v7\n        with:\n          name: ksud-aarch64-linux-android\n          path: .\n\n      - name: Download x86_64 ksud\n        uses: actions/download-artifact@v7\n        with:\n          name: ksud-x86_64-linux-android\n          path: .\n\n      - name: Copy ksud to app jniLibs\n        run: |\n          mkdir -p app/src/main/jniLibs/arm64-v8a\n          mkdir -p app/src/main/jniLibs/x86_64\n          cp -f ../aarch64-linux-android/release/ksud ../manager/app/src/main/jniLibs/arm64-v8a/libksud.so\n          cp -f ../x86_64-linux-android/release/ksud ../manager/app/src/main/jniLibs/x86_64/libksud.so\n\n      - name: Build with Gradle\n        run: |\n          if [ \"${{ github.event_name }}\" = \"pull_request\" ]; then\n            ./gradlew clean assembleRelease -PIS_PR_BUILD=true\n          else\n            ./gradlew clean assembleRelease\n          fi\n\n      - name: Upload build artifact\n        if: ${{ github.event_name == 'pull_request' || (github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')) || github.ref_type == 'tag' }}\n        uses: actions/upload-artifact@v6\n        with:\n          name: manager\n          path: manager/app/build/outputs/apk/release/*.apk\n\n      - name: Upload mappings\n        uses: actions/upload-artifact@v6\n        if: ${{ ( github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' )) || github.ref_type == 'tag' }}\n        with:\n          name: \"mappings\"\n          path: \"manager/app/build/outputs/mapping/release/\"\n\n      - name: Bot session cache\n        if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'\n        id: bot_session_cache\n        uses: actions/cache@v5\n        with:\n          path: scripts/ksubot.session\n          key: ${{ runner.os }}-bot-session\n\n      - name: Upload to telegram\n        if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true'\n        env:\n          CHAT_ID: ${{ secrets.CHAT_ID }}\n          BOT_TOKEN: ${{ secrets.BOT_TOKEN }}\n          MESSAGE_THREAD_ID: ${{ secrets.MESSAGE_THREAD_ID }}\n          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}\n          COMMIT_URL: ${{ github.event.head_commit.url }}\n          RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\n          TITLE: Manager\n          BRANCH: ${{ github.ref_name }}\n        run: |\n          if [ ! -z \"${{ secrets.BOT_TOKEN }}\" ]; then\n            export VERSION=$(git rev-list --count HEAD)\n            APK=$(find ./app/build/outputs/apk/release -name \"*.apk\")\n            pip3 install telethon\n            python3 $GITHUB_WORKSPACE/scripts/ksubot.py $APK\n          fi\n"
  },
  {
    "path": ".github/workflows/clang-format.yml",
    "content": "name: ClangFormat check\n\non:\n  push:\n    branches:\n      - 'main'\n    paths:\n      - '.github/workflows/clang-format.yml'\n      - 'kernel/**/*.c'\n      - 'kernel/**/*.h'\n  pull_request:\n    branches:\n      - 'main'\n    paths:\n      - '.github/workflows/clang-format.yml'\n      - 'kernel/**/*.c'\n      - 'kernel/**/*.h'\n      - 'kernel/.clang-format'\n\npermissions:\n  checks: write\n\njobs:\n  format:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: check\n        working-directory: kernel\n        run: make check-format\n"
  },
  {
    "path": ".github/workflows/clippy.yml",
    "content": "name: Clippy check\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - '.github/workflows/clippy.yml'\n      - 'userspace/**'\n  pull_request:\n    branches:\n      - main\n    paths:\n      - '.github/workflows/clippy.yml'\n      - 'userspace/**'\n\nenv:\n  RUSTFLAGS: '-Dwarnings'\n  CROSS_NO_WARNINGS: '0'\n\njobs:\n  clippy:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - run: rustup update stable\n      - uses: Swatinem/rust-cache@v2\n        with:\n          workspaces: userspace/ksud\n\n      - name: Install cross\n        run: |\n          RUSTFLAGS=\"\" cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1\n\n      - name: Run clippy\n        run: |\n          cross clippy --manifest-path userspace/ksud/Cargo.toml --target aarch64-linux-android --release\n          cross clippy --manifest-path userspace/ksud/Cargo.toml --target x86_64-linux-android --release\n          cross clippy --manifest-path userspace/ksuinit/Cargo.toml --target aarch64-linux-android --release\n          cross clippy --manifest-path userspace/ksuinit/Cargo.toml --target x86_64-linux-android --release\n"
  },
  {
    "path": ".github/workflows/ddk-lkm.yml",
    "content": "name: Build KernelSU Kernel Module\n\non:\n  workflow_call:\n    inputs:\n      kmi:\n        description: 'KMI version'\n        required: true\n        type: string\n      ddk_release:\n        description: 'DDK release version'\n        required: false\n        default: '20260313'\n        type: string\n      expected_size2:\n        description: 'Second expected signature size (for PR builds)'\n        required: false\n        default: ''\n        type: string\n      expected_hash2:\n        description: 'Second expected signature hash (for PR builds)'\n        required: false\n        default: ''\n        type: string\n\njobs:\n  build-kernelsu-ko:\n    name: Build kernelsu.ko for ${{ inputs.kmi }}\n    runs-on: ubuntu-latest\n    container:\n      image: ghcr.io/ylarod/ddk-min:${{ inputs.kmi }}-${{ inputs.ddk_release }}\n      options: --privileged\n\n    steps:\n    - name: Checkout source code\n      uses: actions/checkout@v6\n\n    - name: Build kernelsu.ko\n      env:\n        EXPECTED_SIZE2: ${{ inputs.expected_size2 }}\n        EXPECTED_HASH2: ${{ inputs.expected_hash2 }}\n      run: |\n        git config --global --add safe.directory /__w/KernelSU/KernelSU\n\n        cd kernel\n\n        echo \"=== Building kernelsu.ko for KMI: ${{ inputs.kmi }} ===\"\n\n        EXTRA_ARGS=\"\"\n        if [ -n \"$EXPECTED_SIZE2\" ] && [ -z \"$EXPECTED_HASH2\" ]; then\n          echo \"ERROR: expected_hash2 must be provided when expected_size2 is set.\"\n          exit 1\n        fi\n        if [ -z \"$EXPECTED_SIZE2\" ] && [ -n \"$EXPECTED_HASH2\" ]; then\n          echo \"ERROR: expected_size2 must be provided when expected_hash2 is set.\"\n          exit 1\n        fi\n        if [ -n \"$EXPECTED_SIZE2\" ]; then\n          EXTRA_ARGS=\"KSU_EXPECTED_SIZE2=$EXPECTED_SIZE2 KSU_EXPECTED_HASH2=$EXPECTED_HASH2\"\n        fi\n        CONFIG_KSU=m CC=clang make $EXTRA_ARGS\n        \n        echo \"=== Build completed ===\"\n\n        # Create output directory in GitHub workspace\n        mkdir -p /github/workspace/out\n\n        # Copy with KMI-specific naming\n        OUTPUT_NAME=\"${{ inputs.kmi }}_kernelsu.ko\"\n        cp kernelsu.ko \"/github/workspace/out/$OUTPUT_NAME\"\n        \n        echo \"Copied to: /github/workspace/out/$OUTPUT_NAME\"\n        ls -la \"/github/workspace/out/$OUTPUT_NAME\"\n        echo \"Size: $(du -h \"/github/workspace/out/$OUTPUT_NAME\" | cut -f1)\"\n        llvm-strip -d \"/github/workspace/out/$OUTPUT_NAME\"\n        echo \"Size after stripping: $(du -h \"/github/workspace/out/$OUTPUT_NAME\" | cut -f1)\"\n\n    - name: Upload kernelsu.ko artifact\n      uses: actions/upload-artifact@v6\n      with:\n        name: ${{ inputs.kmi }}-lkm\n        path: /github/workspace/out/${{ inputs.kmi }}_kernelsu.ko\n"
  },
  {
    "path": ".github/workflows/deploy-website.yml",
    "content": "name: Deploy Website\n\non:\n  push:\n    branches:\n      - main\n      - website\n    paths:\n      - '.github/workflows/deploy-website.yml'\n      - 'website/**'\n  workflow_dispatch:\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\n# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.\n# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.\nconcurrency:\n  group: pages\n  cancel-in-progress: false\n\njobs:\n  # Build job\n  build:\n    runs-on: ubuntu-latest\n    defaults:\n      run:\n        working-directory: ./website\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v6\n        with:\n          fetch-depth: 0 # Not needed if lastUpdated is not enabled\n      - name: Setup Node\n        uses: actions/setup-node@v6\n        with:\n          node-version: latest\n          cache: yarn # or pnpm / yarn\n          cache-dependency-path: website/yarn.lock\n      - name: Setup Pages\n        uses: actions/configure-pages@v5\n      - name: Install dependencies\n        run: yarn install --frozen-lockfile\n      - name: Build with VitePress\n        run: |\n          yarn docs:build\n          touch docs/.vitepress/dist/.nojekyll\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v4\n        with:\n          path: website/docs/.vitepress/dist\n\n  # Deployment job\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    needs: build\n    runs-on: ubuntu-latest\n    name: Deploy\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4"
  },
  {
    "path": ".github/workflows/ksud.yml",
    "content": "name: Build ksud\non:\n  workflow_call:\n    inputs:\n      target:\n        required: true\n        type: string\n      os:\n        required: false\n        type: string\n        default: ubuntu-latest\n      pack_lkm:\n        required: false\n        type: boolean\n        default: true\n      pack_ksuinit:\n        required: false\n        type: boolean\n        default: true\n      use_cache:\n        required: false\n        type: boolean\n        default: true\njobs:\n  build:\n    runs-on: ${{ inputs.os }}\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n\n    - name: Download artifacts\n      uses: actions/download-artifact@v7\n    \n    - name: Prepare LKM fies\n      if: ${{ inputs.pack_lkm }}\n      run: |\n        cp android*-lkm/*_kernelsu.ko ./userspace/ksud/bin/aarch64/\n    \n    - name: Prepare ksuinit\n      if: ${{ inputs.pack_ksuinit }}\n      run: |\n        mv ksuinit/*/release/ksuinit ./userspace/ksud/bin/aarch64/\n        \n    - name: Setup rustup\n      run: |\n        rustup update stable\n        rustup target add x86_64-apple-darwin\n        rustup target add aarch64-apple-darwin\n    - uses: Swatinem/rust-cache@v2\n      with:\n        workspaces: userspace/ksud\n        cache-targets: false\n\n    - name: Install cross\n      run: |\n        RUSTFLAGS=\"\" cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1\n\n    - name: Build ksud\n      run: CROSS_NO_WARNINGS=0 cross build --target ${{ inputs.target }} --release --manifest-path ./userspace/ksud/Cargo.toml\n\n    - name: Upload ksud artifact\n      uses: actions/upload-artifact@v6\n      with:\n        name: ksud-${{ inputs.target }}\n        path: userspace/ksud/target/**/release/ksud*\n"
  },
  {
    "path": ".github/workflows/ksuinit.yml",
    "content": "name: Build ksuinit\non:\n  workflow_call:\n    inputs:\n      os:\n        required: false\n        type: string\n        default: ubuntu-latest\njobs:\n  build:\n    runs-on: ${{ inputs.os }}\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n        \n    - name: Setup rustup\n      run: |\n        rustup update stable\n        rustup target add aarch64-unknown-linux-musl\n\n    - uses: Swatinem/rust-cache@v2\n      with:\n        workspaces: userspace/ksuinit\n        cache-targets: false\n\n    - name: Build ksuinit\n      run: |\n        LLVM_BIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin\n        export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=\"$LLVM_BIN/aarch64-linux-android26-clang\"\n        export RUSTFLAGS=\"-C link-arg=-no-pie\"\n        cd userspace/ksuinit\n        cargo build --target=aarch64-unknown-linux-musl --release\n\n    - name: Upload ksuinit artifact\n      uses: actions/upload-artifact@v6\n      with:\n        name: ksuinit\n        path: userspace/ksuinit/target/**/release/ksuinit*\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\non:\n  push:\n    tags:\n      - \"v*\"\n  workflow_dispatch:\n\njobs:\n  build-manager:\n    uses: ./.github/workflows/build-manager.yml\n    secrets: inherit\n  release:\n    needs:\n      - build-manager\n    runs-on: ubuntu-latest\n    steps:\n      - name: Download artifacts\n        uses: actions/download-artifact@v7\n\n      - name: Rename ksud\n        run: |\n          mkdir -p ksud\n          for dir in ./ksud-*; do\n              if [ -d \"$dir\" ]; then\n                 echo \"----- Rename $dir -----\"\n                 ksud_platform_name=$(basename \"$dir\")\n                 find \"$dir\" -type f -name \"ksud\" -path \"*/release/*\" | while read -r ksud_file; do\n                   if [ -f \"$ksud_file\" ]; then\n                     mv \"$ksud_file\" \"ksud/$ksud_platform_name\"\n                   fi\n                 done\n              fi\n          done\n\n      - name: Display structure of downloaded files\n        run: ls -R\n\n      - name: release\n        uses: softprops/action-gh-release@v2\n        with:\n          files: |\n            manager/*.apk\n            android*-lkm/*_kernelsu.ko\n            ksud/ksud-*\n            ksuinit/**\n"
  },
  {
    "path": ".github/workflows/rustfmt.yml",
    "content": "name: Rustfmt check\n\non:\n  push:\n    branches:\n      - 'main'\n    paths:\n      - '.github/workflows/rustfmt.yml'\n      - 'userspace/**'\n  pull_request:\n    branches:\n      - 'main'\n    paths:\n      - '.github/workflows/rustfmt.yml'\n      - 'userspace/**'\n\npermissions:\n  checks: write\n\njobs:\n  format:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - uses: dtolnay/rust-toolchain@nightly\n        with:\n          components: rustfmt\n\n      - uses: LoliGothick/rustfmt-check@master\n        with:\n          token: ${{ github.token }}\n          working-directory: userspace/ksud\n\n      - uses: LoliGothick/rustfmt-check@master\n        with:\n          token: ${{ github.token }}\n          working-directory: userspace/ksuinit\n"
  },
  {
    "path": ".github/workflows/shellcheck.yml",
    "content": "name: ShellCheck\n\non:\n  push:\n    branches:\n      - 'main'\n    paths:\n      - '.github/workflows/shellcheck.yml'\n      - '**/*.sh'\n  pull_request:\n    branches:\n      - 'main'\n    paths:\n      - '.github/workflows/shellcheck.yml'\n      - '**/*.sh'\n\njobs:\n  shellcheck:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Run ShellCheck\n        uses: ludeeus/action-shellcheck@2.0.0\n        with:\n          ignore_names: gradlew\n          ignore_paths: ./userspace/ksud/src/installer.sh\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\n.vscode\nCLAUDE.md\nAGENTS.md\n.DS_Store\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Reporting Security Issues\n\nThe 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.\n\nTo 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.\n\nThe 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.\n"
  },
  {
    "path": "docs/README.md",
    "content": "**English** | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\nA kernel-based root solution for Android devices.\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## Features\n\n1. Kernel-based `su` and root access management.\n2. Module system based on [metamodules](https://kernelsu.org/guide/metamodule.html): Pluggable infrastructure for systemless modifications.\n3. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock up the root power in a cage.\n\n## Compatibility state\n\nKernelSU officially supports Android GKI 2.0 devices (kernel 5.10+). Older kernels (4.14+) are also supported, but the kernel will need to be built manually.\n\nWith this, WSA, ChromeOS, and container-based Android are all supported.\n\nCurrently, only the `arm64-v8a` and `x86_64` architectures are supported.\n\n## Usage\n\n- [Installation](https://kernelsu.org/guide/installation.html)\n- [How to build](https://kernelsu.org/guide/how-to-build.html)\n- [Official website](https://kernelsu.org/)\n\n## Translation\n\nTo help translate KernelSU or improve existing translations, please use [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR of Manager's translation is no longer accepted, because it will conflict with Weblate.\n\n## Discussion\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## Security\n\nFor information on reporting security vulnerabilities in KernelSU, see [SECURITY.md](/SECURITY.md).\n\n## License\n\n- Files under the `kernel` directory are [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\n- All other parts except the `kernel` directory are [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html).\n\n## Credits\n\n- [Kernel-Assisted Superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): The KernelSU idea.\n- [Magisk](https://github.com/topjohnwu/Magisk): The powerful root tool.\n- [genuine](https://github.com/brevent/genuine/): APK v2 signature validation.\n- [Diamorphine](https://github.com/m0nad/Diamorphine): Some rootkit skills.\n"
  },
  {
    "path": "docs/README_CN.md",
    "content": "[English](README.md) | [Español](README_ES.md) | **简体中文** | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\n一个 Android 上基于内核的 root 方案。\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## 特性\n\n- 基于内核的 `su` 和权限管理。\n- 基于 [metamodules](https://kernelsu.org/zh_CN/guide/metamodule.html) 的模块系统：可插拔的模块架构。\n- [App Profile](https://kernelsu.org/zh_CN/guide/app-profile.html): 把 Root 权限关进笼子里。\n\n## 兼容状态\n\nKernelSU 官方支持 GKI 2.0 的设备（内核版本5.10以上）；旧内核也是兼容的（最低4.14+），不过需要自己编译内核。\n\nWSA, ChromeOS 和运行在容器上的 Android 也可以与 KernelSU 一起工作。\n\n目前支持架构 : `arm64-v8a` 和 `x86_64`。\n\n## 使用方法\n\n- [安装教程](https://kernelsu.org/zh_CN/guide/installation.html)\n- [如何构建？](https://kernelsu.org/zh_CN/guide/how-to-build.html)\n- [官方网站](https://kernelsu.org/zh_CN/)\n\n## 参与翻译\n\n要将 KernelSU 翻译成您的语言，或完善现有的翻译，请使用 [Weblate](https://hosted.weblate.org/engage/kernelsu/)。现已不再接受有关管理器翻译的PR，因为这会与Weblate冲突。\n\n## 讨论\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## 安全性\n\n有关报告 KernelSU 安全漏洞的信息，请参阅 [SECURITY.md](/SECURITY.md)。\n\n## 许可证\n\n- 目录 `kernel` 下所有文件为 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。\n- 除 `kernel` 目录的其他部分均为 [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)。\n\n## 鸣谢\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/)：KernelSU 的灵感。\n- [Magisk](https://github.com/topjohnwu/Magisk)：强大的 root 工具箱。\n- [genuine](https://github.com/brevent/genuine/)：apk v2 签名验证。\n- [Diamorphine](https://github.com/m0nad/Diamorphine)：一些 rootkit 技巧。\n"
  },
  {
    "path": "docs/README_ES.md",
    "content": "[English](README.md) | **Español** | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\nUna solución root basada en el kernel para dispositivos Android.\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localización-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Seguir-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/Licencia-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## Características\n\n1. Binario `su` basado en el kernel y gestión de acceso root.\n2. Sistema de módulos basado en [metamodules](https://kernelsu.org/guide/metamodule.html): Infraestructura conectable para modificaciones sin sistema.\n\n## Estado de compatibilidad\n\n**KernelSU** soporta de forma oficial dispositivos Android con **GKI 2.0** (a partir de la versión **5.10** del kernel). Los kernels antiguos (a partir de la versión **4.14**) también son compatibles, pero necesitas compilarlos por tu cuenta.\n\nCon esto, WSA, ChromeOS y Android basado en contenedores están todos compatibles.\n\nActualmente, solo se admiten las arquitecturas `arm64-v8a` y `x86_64`.\n\n## Uso\n\n- [¿Cómo instalarlo?](https://kernelsu.org/guide/installation.html)\n- [¿Cómo compilarlo?](https://kernelsu.org/guide/how-to-build.html)\n- [Site oficial](https://kernelsu.org/)\n\n## Traducción\n\nPara ayudar a traducir KernelSU o mejorar las traducciones existentes, utilice [Weblate](https://hosted.weblate.org/engage/kernelsu/). Ya no se aceptan PR de la traducción de Manager porque entrará en conflicto con Weblate.\n\n## Discusión\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## Seguridad\n\nPara obtener información sobre cómo informar vulnerabilidades de seguridad en KernelSU, consulte [SECURITY.md](/SECURITY.md).\n\n##  Licencia\n\n- Los archivos bajo el directorio `kernel` están licenciados bajo [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\n- Todas las demás partes, a excepción del directorio `kernel`, están licenciados bajo [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html).\n\n## Créditos\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): la idea de KernelSU.\n- [Magisk](https://github.com/topjohnwu/Magisk): la poderosa herramienta root.\n- [genuine](https://github.com/brevent/genuine/): validación de firma apk v2.\n- [Diamorphine](https://github.com/m0nad/Diamorphine): algunas habilidades de rootkit.\n"
  },
  {
    "path": "docs/README_ID.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | **Indonesia** | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\nSolusi root berbasis Kernel untuk perangkat Android.\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## Fitur\n\n1. Manajemen akses root dan `su` berbasis kernel.\n2. Sistem modul berdasarkan [metamodules](https://kernelsu.org/id_ID/guide/metamodule.html): Infrastruktur pluggable untuk modifikasi systemless.\n3. [Profil Aplikasi](https://kernelsu.org/guide/app-profile.html): Kunci daya root di dalam sangkar.\n\n## Status Kompatibilitas\n\nKernelSU secara resmi mendukung perangkat Android GKI 2.0 (dengan kernel 5.10+), kernel lama (4.14+) juga kompatibel, tetapi Anda perlu membuat kernel sendiri.\n\nWSA, ChromeOS, dan Android berbasis wadah juga dapat bekerja dengan KernelSU terintegrasi.\n\nDan ABI yang didukung saat ini adalah: `arm64-v8a` dan `x86_64`\n\n## Penggunaan\n\n- [Petunjuk Instalasi](https://kernelsu.org/id_ID/guide/installation.html)\n- [Bagaimana cara membuat?](https://kernelsu.org/id_ID/guide/how-to-build.html)\n- [Situs Web Resmi](https://kernelsu.org/id_ID/)\n\n## Terjemahan\n\nUntuk menerjemahkan KernelSU ke dalam bahasa Anda atau menyempurnakan terjemahan yang sudah ada, harap gunakan [Weblat](https://hosted.weblate.org/engage/kernelsu/).\n\n## Diskusi\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## Lisensi\n\n- File di bawah direktori `kernel` adalah [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\n- Semua bagian lain kecuali direktori `kernel` adalah [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html).\n\n## Kredit\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): ide KernelSU.\n- [Magisk](https://github.com/topjohnwu/Magisk): alat root yang ampuh.\n- [genuine](https://github.com/brevent/genuine/): validasi tanda tangan apk v2.\n- [Diamorphine](https://github.com/m0nad/Diamorphine): beberapa keterampilan rootkit.\n"
  },
  {
    "path": "docs/README_IN.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) |  **हिंदी** | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\nAndroid उपकरणों के लिए कर्नेल-आधारित रूट समाधान।\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## विशेषताएँ\n\n1. कर्नेल-आधारित `su` और रूट एक्सेस प्रबंधन।\n2. [metamodules](https://kernelsu.org/guide/metamodule.html) पर आधारित मॉड्यूल प्रणाली: Systemless संशोधनों के लिए प्लगेबल इंफ्रास्ट्रक्चर।\n3. [App Profile](https://kernelsu.org/guide/app-profile.html): Root शक्ति को पिंजरे में बंद कर दो।\n\n## अनुकूलता अवस्था\n\nKernelSU आधिकारिक तौर पर Android GKI 2.0 डिवाइस (कर्नेल 5.10+) का समर्थन करता है। पुराने कर्नेल (4.14+) भी संगत हैं, लेकिन कर्नेल को मैन्युअल रूप से बनाना होगा।\n\nइसके साथ, WSA, ChromeOS और कंटेनर-आधारित Android सभी समर्थित हैं।\n\nवर्तमान में, केवल `arm64-v8a` और `x86_64` समर्थित हैं।\n\n## प्रयोग\n\n- [स्थापना निर्देश](https://kernelsu.org/guide/installation.html)\n- [कैसे बनाना है ?](https://kernelsu.org/guide/how-to-build.html)\n- [आधिकारिक वेबसाइट](https://kernelsu.org/)\n\n## अनुवाद करना\n\nKernelSU का अनुवाद करने या मौजूदा अनुवादों को बेहतर बनाने में सहायता के लिए, कृपया इसका उपयोग करें [Weblate](https://hosted.weblate.org/engage/kernelsu/).\n\n## बहस\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## लाइसेंस\n\n- `Kernel` निर्देशिका के अंतर्गत फ़ाइलें हैं [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n- `Kernel` निर्देशिका को छोड़कर अन्य सभी भाग हैं [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)\n\n## आभार सूची\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU विचार।\n- [Magisk](https://github.com/topjohnwu/Magisk): शक्तिशाली root उपकरण।\n- [genuine](https://github.com/brevent/genuine/): apk v2 हस्ताक्षर सत्यापन।\n- [Diamorphine](https://github.com/m0nad/Diamorphine): कुछ रूटकिट कौशल।\n"
  },
  {
    "path": "docs/README_IT.md",
    "content": "[English](REAME.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | **Italiano**\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\nUna soluzione per il root basata sul kernel per i dispositivi Android. \n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Canale Telegraml](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![Licenza componenti kernel: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![Licenza elementi non kern](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## Funzionalità\n\n1. `su` e accesso root basato sul kernel.\n2. Sistema di moduli basato su [metamodules](https://kernelsu.org/guide/metamodule.html): Infrastruttura modulare per modifiche systemless.\n3. [App profile](https://kernelsu.org/guide/app-profile.html): Limita i poteri dell'accesso root a permessi specifici.\n\n## Compatibilità\n\nKernelSU supporta ufficialmente i dispositivi Android GKI 2.0 (kernel 5.10 o superiore). I kernel precedenti (kernel 4.14+) sono anche compatibili, ma il kernel deve essere compilato manualmente.\n\nQuesto implica che WSA, ChromeOS e tutti le varianti di Android basate su container e virtualizzazione sono supportate.\n\nAllo stato attuale solo le architetture a 64-bit ARM (arm64-v8a) e x86 (x86_64) sono supportate.\n\n## Utilizzo\n\n- [Istruzioni per l'installazione](https://kernelsu.org/guide/installation.html)\n- [Come compilare manualmente?](https://kernelsu.org/guide/how-to-build.html)\n- [Sito web ufficiale](https://kernelsu.org/)\n\n## Traduzioni\n\nPer aiutare a tradurre KernelSU o migliorare le traduzioni esistenti, si è pregati di utilizzare \nTo help translate KernelSU or improve existing translations, please use [Weblate](https://hosted.weblate.org/engage/kernelsu/). Le richieste di pull delle traduzioni del manager non saranno più accettate perché sarebbero in conflitto con Weblate.\n\n## Discussione\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## Securezza\n\nPer informazioni riguardo la segnalazione di vulnerabilità di sicurezza per KernelSU, leggi [SECURITY.md](/SECURITY.md).\n\n## Licenza\n\n- I file nella cartella `kernel` sono forniti secondo la licenza [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\n- Tutte le altre parti, ad eccezione della certella `kernel`, seguono la licenza [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html).\n\n## Riconoscimenti e attribuzioni\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): l'idea alla base di KernelSU.\n- [Magisk](https://github.com/topjohnwu/Magisk): la potente utilità per il root.\n- [genuine](https://github.com/brevent/genuine/): verifica della firma apk v2.\n- [Diamorphine](https://github.com/m0nad/Diamorphine): alcune capacità di rootkit.\n"
  },
  {
    "path": "docs/README_IW.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | **עברית** | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\r\n\r\n# KernelSU\r\n\r\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\r\n\r\nפתרון לניהול root מבוסס על Kernel עבור מכשירי Android.\r\n\r\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\r\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\r\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\r\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\r\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\r\n\r\n## תכונות\r\n\r\n1. ניהול root ו־`su` מבוססים על Kernel.\r\n2. מערכת מודולים מבוססת [metamodules](https://kernelsu.org/guide/metamodule.html): תשתית מודולרית לשינויים systemless.\r\n3. [פרופיל אפליקציה](https://kernelsu.org/guide/app-profile.html): נעילת גישת root בכלוב.\r\n\r\n## מצב תאימות\r\n\r\nKernelSU תומך במכשירי Android GKI 2.0 (kernel 5.10+) באופן רשמי. לליבות ישנות (4.14+) יש גם תאימות, אך יידרש לבנות את הליבה באופן ידני.\r\n\r\nבאמצעות זה, תמיכה זמינה גם ל-WSA, ChromeOS ומכשירי Android המבוססים על מיכלים.\r\n\r\nכרגע, רק `arm64-v8a` ו־`x86_64` נתמכים.\r\n\r\n## שימוש\r\n\r\n- [הוראות התקנה](https://kernelsu.org/guide/installation.html)\r\n- [איך לבנות?](https://kernelsu.org/guide/how-to-build.html)\r\n- [האתר רשמי](https://kernelsu.org/)\r\n\r\n## תרגום\r\n\r\nכדי לעזור בתרגום של KernelSU או לשפר תרגומים קיימים, יש להשתמש ב-[Weblate](https://hosted.weblate.org/engage/kernelsu/).\r\n\r\n## דיון\r\n\r\n- Telegram: [@KernelSU](https://t.me/KernelSU)\r\n\r\n## רשיון\r\n\r\n- קבצים תחת הספרייה `kernel` מוגנים על פי [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\r\n- כל החלקים האחרים, למעט הספרייה `kernel`, מוגנים על פי [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html).\r\n\r\n## קרדיטים\r\n\r\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): הרעיון של KernelSU.\r\n- [Magisk](https://github.com/topjohnwu/Magisk): הכלי הסופר חזק לניהול root.\r\n- [genuine](https://github.com/brevent/genuine/): אימות חתימת apk v2.\r\n- [Diamorphine](https://github.com/m0nad/Diamorphine): כמה יכולות רוט.\r\n"
  },
  {
    "path": "docs/README_JP.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | **日本語** | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\nAndroid におけるカーネルベースの root ソリューションです。\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## 特徴\n\n1. カーネルベースの `su` と権限管理。\n2. [metamodules](https://kernelsu.org/ja_JP/guide/metamodule.html) に基づくモジュールシステム: プラグイン可能なシステムレス変更インフラストラクチャ。\n3. [アプリのプロファイル](https://kernelsu.org/guide/app-profile.html): root の権限をケージ内に閉じ込めます。\n\n## 対応状況\n\nKernelSU は GKI 2.0 デバイス（カーネルバージョン 5.10 以上）を公式にサポートしています。古いカーネル（4.14以上）とも互換性がありますが、自分でカーネルをビルドする必要があります。\n\nWSA 、ChromeOS とコンテナ上で動作する Android でも KernelSU を統合して動かせます。\n\n現在サポートしているアーキテクチャは `arm64-v8a` および `x86_64` です。\n\n## 使用方法\n\n- [インストール方法はこちら](https://kernelsu.org/ja_JP/guide/installation.html)\n- [ビルド方法はこちら](https://kernelsu.org/guide/how-to-build.html)\n- [公式サイト](https://kernelsu.org/ja_JP/)\n\n## 翻訳\n\nKernelSU をあなたの言語に翻訳するか、既存の翻訳を改善するには、[Weblate](https://hosted.weblate.org/engage/kernelsu/) を使用してください。Manager翻訳した PR は、Weblate と競合するため受け入れられなくなりました。\n\n## ディスカッション\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## ライセンス\n\n- `kernel` ディレクトリの下にあるすべてのファイル： [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。\n- `kernel` ディレクトリ以外のすべてのファイル： [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)。\n\n## クレジット\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/)：KernelSU のアイデア元。\n- [Magisk](https://github.com/topjohnwu/Magisk)：強力な root ツール。\n- [genuine](https://github.com/brevent/genuine/)：apk v2 の署名検証。\n- [Diamorphine](https://github.com/m0nad/Diamorphine): rootkit のスキル。\n"
  },
  {
    "path": "docs/README_KR.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | **한국어** | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\n안드로이드 기기에서 사용되는 커널 기반 루팅 솔루션입니다.\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## 기능들\n\n1. 커널 기반 `su` 및 루트 액세스 관리.\n2. [metamodules](https://kernelsu.org/guide/metamodule.html) 기반 모듈 시스템: 플러그인 가능한 시스템리스 수정 인프라.\n3. [App Profile](https://kernelsu.org/guide/app-profile.html): 루트 권한을 케이지에 가둡니다.\n\n## 호환 상태\n\nKernelSU는 공식적으로 안드로이드 GKI 2.0 디바이스(커널 5.10 이상)를 지원합니다. 오래된 커널(4.14 이상)도 사용할 수 있지만, 커널을 수동으로 빌드해야 합니다.\n\nKernelSU는 WSA, ChromeOS, 컨테이너 기반 안드로이드 모두를 지원합니다.\n\n현재는 `arm64-v8a`와 `x86_64`만 지원됩니다.\n\n## 사용 방법\n\n- [설치 방법](https://kernelsu.org/guide/installation.html)\n- [어떻게 빌드하나요?](https://kernelsu.org/guide/how-to-build.html)\n- [공식 웹사이트](https://kernelsu.org/)\n\n## 번역\n\nKernelSU 번역을 돕거나 기존 번역을 개선하려면 [Weblate](https://hosted.weblate.org/engage/kernelsu/)를 이용해 주세요. 매니저의 번역은 Weblate와 충돌할 수 있으므로 더 이상 허용되지 않습니다.\n\n## 토론\n\n- 텔레그램: [@KernelSU](https://t.me/KernelSU)\n\n## 보안\n\nKernelSU의 보안 취약점 보고에 대한 자세한 내용은 [SECURITY.md](/SECURITY.md)를 참조하세요.\n\n## 저작권\n\n- `kernel` 디렉터리 아래의 파일은 [GPL-2.0 전용](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)입니다.\n- `kernel` 디렉토리를 제외한 다른 모든 부분은 [GPL-3.0-이상](https://www.gnu.org/licenses/gpl-3.0.html)입니다.\n\n## 크래딧\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU의 아이디어.\n- [Magisk](https://github.com/topjohnwu/Magisk): 강력한 루팅 도구.\n- [genuine](https://github.com/brevent/genuine/): apk v2 서명 유효성 검사.\n- [Diamorphine](https://github.com/m0nad/Diamorphine): 일부 rootkit 스킬.\n"
  },
  {
    "path": "docs/README_PL.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | **Polski** | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\r\n\r\n# KernelSU\r\n\r\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\r\n\r\nRozwiązanie root oparte na jądrze dla urządzeń z systemem Android.\r\n\r\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\r\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\r\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\r\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\r\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\r\n\r\n## Cechy\r\n\r\n1. Oparte na jądrze `su` i zarządzanie dostępem roota.\r\n2. System modułów oparty na [metamodules](https://kernelsu.org/guide/metamodule.html): Wtykowa infrastruktura dla modyfikacji systemless.\r\n\r\n## Kompatybilność\r\n\r\nKernelSU oficjalnie obsługuje urządzenia z Androidem GKI 2.0 (z jądrem 5.10+), starsze jądra (4.14+) są również kompatybilne, ale musisz sam skompilować jądro.\r\n\r\nWSA i Android oparty na kontenerach również powinny działać ze zintegrowanym KernelSU.\r\n\r\nAktualnie obsługiwane ABI to : `arm64-v8a` i `x86_64`.\r\n\r\n## Użycie\r\n\r\n- [Instalacja](https://kernelsu.org/guide/installation.html)\r\n- [Jak skompilować?](https://kernelsu.org/guide/how-to-build.html)\r\n\r\n## Tłumaczenie\r\n\r\nAby pomóc w tłumaczeniu KernelSU lub ulepszyć istniejące tłumaczenia, użyj [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR tłumaczenia Managera nie jest już akceptowany, ponieważ będzie kolidował z Weblate.\r\n\r\n## Dyskusja\r\n\r\n- Telegram: [@KernelSU](https://t.me/KernelSU)\r\n\r\n## Bezpieczeństwo\r\n\r\nInformacje na temat zgłaszania luk w zabezpieczeniach w KernelSU można znaleźć w pliku [SECURITY.md](/SECURITY.md).\r\n\r\n## Licencja\r\n\r\n- Pliki w katalogu `kernel` są na licencji [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\r\n- Wszystkie inne części poza katalogiem `kernel` są na licencji [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html).\r\n\r\n## Podziękowania\r\n\r\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): pomysłodawca KernelSU.\r\n- [Magisk](https://github.com/topjohnwu/Magisk): implementacja sepolicy.\r\n- [genuine](https://github.com/brevent/genuine/): walidacja podpisu apk v2.\r\n- [Diamorphine](https://github.com/m0nad/Diamorphine): cenna znajomość rootkitów.\r\n"
  },
  {
    "path": "docs/README_PT-BR.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | **Português (Brasil)** | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\nUma solução root baseada em kernel para dispositivos Android.\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localização-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Seguir-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/Licença-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## Características\n\n1. `su` e gerenciamento de acesso root baseado em kernel.\n2. Sistema de módulos baseado em [metamodules](https://kernelsu.org/pt_BR/guide/metamodule.html): Infraestrutura plugável para modificações systemless.\n3. [Perfil do Aplicativo](https://kernelsu.org/pt_BR/guide/app-profile.html): Tranque o poder root em uma gaiola.\n\n## Estado de compatibilidade\n\nO KernelSU oferece suporte oficial a dispositivos Android GKI 2.0 (kernel 5.10+). Kernels mais antigos (4.14+) também são compatíveis, mas será necessário construir o kernel manualmente.\n\nCom isso, WSA, ChromeOS e Android baseado em contêiner são todos suportados.\n\nAtualmente, apenas as arquiteturas `arm64-v8a` e `x86_64` são compatíveis.\n\n## Uso\n\n - [Instalação](https://kernelsu.org/pt_BR/guide/installation.html)\n - [Como compilar](https://kernelsu.org/pt_BR/guide/how-to-build.html)\n - [Site oficial](https://kernelsu.org/pt_BR/)\n\n## Tradução\n\nPara contribuir com a tradução do KernelSU ou aprimorar traduções existentes, por favor, use o [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR para a tradução do Manager não são mais aceitas, pois podem entrar em conflito com o Weblate.\n\n## Discussão\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## Segurança\n\nPara obter informações sobre como relatar vulnerabilidades de segurança do KernelSU, consulte [SECURITY.md](/SECURITY.md).\n\n## Licença\n\n- Os arquivos no diretório `kernel` são [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\n- Todas as outras partes, exceto o diretório `kernel` são [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html).\n\n## Créditos\n\n- [Kernel-Assisted Superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): A ideia do KernelSU.\n- [Magisk](https://github.com/topjohnwu/Magisk): A poderosa ferramenta root.\n- [genuine](https://github.com/brevent/genuine/): Validação de assinatura APK v2.\n- [Diamorphine](https://github.com/m0nad/Diamorphine): Algumas habilidades de rootkit.\n"
  },
  {
    "path": "docs/README_RU.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | **Русский** | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\nРешение на основе ядра root для Android-устройств.\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## Особенности\n\n1. Управление `su` и root-доступом на основе ядра.\n2. Система модулей на основе [metamodules](https://kernelsu.org/ru_RU/guide/metamodule.html): Подключаемая инфраструктура для безсистемных модификаций.\n3. [Профиль приложений](https://kernelsu.org/ru_RU/guide/app-profile.html): Запри корневую силу в клетке.\n\n## Совместимость\n\nKernelSU официально поддерживает устройства на базе Android GKI 2.0 (с ядром 5.10+), старые ядра (4.14+) также совместимы, но для этого необходимо собрать ядро самостоятельно.\n\nWSA и Android на основе контейнеров также должны работать с интегрированным KernelSU.\n\nВ настоящее время поддерживаются следующие ABI: `arm64-v8a` и `x86_64`.\n\n## Использование\n\n- [Установка](https://kernelsu.org/ru_RU/guide/installation.html)\n- [Как собрать?](https://kernelsu.org/ru_RU/guide/how-to-build.html)\n- [официальный сайт](https://kernelsu.org/ru_RU/)\n\n## Обсуждение\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## Лицензия\n\n- Файлы в директории `kernel` [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\n- Все остальные части, кроме директории `kernel` [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html).\n\n## Благодарности\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): идея KernelSU.\n- [Magisk](https://github.com/topjohnwu/Magisk): реализация sepolicy.\n- [genuine](https://github.com/brevent/genuine/): проверка подписи apk v2.\n- [Diamorphine](https://github.com/m0nad/Diamorphine): некоторые навыки руткита.\n"
  },
  {
    "path": "docs/README_TR.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | **Türkçe** | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\nAndroid cihazlar için kernel tabanlı root çözümü.\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## Özellikler\n\n1. Kernel-tabanlı `su` ve root erişimi yönetimi.\n2. [metamodules](https://kernelsu.org/guide/metamodule.html)'ye dayalı modül sistemi: Systemless modifikasyonlar için takılabilir altyapı.\n3. [Uygulama profili](https://kernelsu.org/guide/app-profile.html): Root gücünü bir kafese kapatın.\n\n## Uyumluluk Durumu\n\nKernelSU resmi olarak Android GKI 2.0 cihazlarını (5.10+ kernelli) destekler, eski kernellerle de (4.14+) uyumludur, ancak kerneli kendinizin derlemeniz gerekir.\n\nBununla birlikte; WSA, ChromeOS ve konteyner tabanlı Android'in tamamı desteklenmektedir.\n\nŞimdilik sadece `arm64-v8a` ve `x86_64` desteklenmektedir.\n\n## Kullanım\n\n- [Yükleme yönergeleri](https://kernelsu.org/guide/installation.html)\n- [Nasıl derlenir?](https://kernelsu.org/guide/how-to-build.html)\n- [Resmi WEB sitesi](https://kernelsu.org/)\n\n## Çeviri\n\nKernelSU'nun başka dillere çevrilmesine veya mevcut çevirilerin iyileştirilmesine yardımcı olmak için lütfen [Weblate](https://hosted.weblate.org/engage/kernelsu/) kullanın. Yönetici uygulamasının PR ile çevirisi, Weblate ile çakışacağından artık kabul edilmeyecektir.\n\n## Tartışma\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## Güvenlik\n\nKernelSU'daki güvenlik açıklarını bildirme hakkında bilgi için, bkz [SECURITY.md](/SECURITY.md).\n\n## Lisans\n\n- `kernel` klasöründeki dosyalar [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) lisansı altındadır.\n- `kernel` klasörü dışındaki bütün diğer bölümler [GPL-3-veya-sonraki](https://www.gnu.org/licenses/gpl-3.0.html) lisansı altındadır.\n\n## Krediler\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU fikri.\n- [Magisk](https://github.com/topjohnwu/Magisk): güçlü root aracı.\n- [genuine](https://github.com/brevent/genuine/): apk v2 imza doğrulaması.\n- [Diamorphine](https://github.com/m0nad/Diamorphine): bazı rootkit becerileri.\n"
  },
  {
    "path": "docs/README_TW.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | **繁體中文** | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"標誌\">\n\n一套基於 Android 裝置核心的 Root 解決方案。\n\n[![最新版本](https://img.shields.io/github/v/release/tiann/KernelSU?label=%e7%99%bc%e8%a1%8c%e7%89%88%e6%9c%ac&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/%e6%9c%ac%e5%9c%9f%e5%8c%96%e7%bf%bb%e8%ad%af-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![頻道](https://img.shields.io/badge/%e8%bf%bd%e8%b9%a4-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![授權條款：《GPL v2》](https://img.shields.io/badge/%e6%8e%88%e6%ac%8a%e6%a2%9d%e6%ac%be-%E3%80%8AGPL%20v2%E3%80%8B-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub 授權條款](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## 特色功能\n\n1. 以核心內 `su` 管理 Root 存取。\n2. 以 [metamodules](https://kernelsu.org/zh_TW/guide/metamodule.html) 運作模組系統：可插拔的無系統修改基礎架構。\n3. [App Profile](https://kernelsu.org/zh_TW/guide/app-profile.html)：使 Root 掌握的生殺大權受制於此。\n\n## 相容事態\n\n理論上採以 Android GKI 2.0 的裝置（核心版本 5.10+），皆受 KernelSU 支援；採以老舊核心版本（4.14+）的裝置在手動建置核心後，亦受支援。\n\n另可在 WSA、ChromeOS 一類的容器式 Android 中運作。\n\n目前僅適用 `arm64-v8a` 以及 `x86_64` 架構。\n\n## 使用手冊\n\n- [安裝教學](https://kernelsu.org/zh_TW/guide/installation.html)\n- [如何建置 KernelSU？](https://kernelsu.org/zh_TW/guide/how-to-build.html)\n- [官方網站](https://kernelsu.org/zh_TW/)\n\n## 多語翻譯\n\n欲要協助 KernelSU 邁向多語化，抑或改進翻譯品質，請前往 [Weblate](https://hosted.weblate.org/engage/kernelsu/) 進行翻譯。為避免與 Weblate 上的翻譯發生衝突，現已不再受理翻譯相關的管理工具 PR。\n\n## 綜合討論\n\n- Telegram：[@KernelSU](https://t.me/KernelSU)\n\n## 安全政策\n\n欲要得知、回報 KernelSU 的安全性漏洞，請參閱 [SECURITY.md](/SECURITY.md)。\n\n## 授權條款\n\n- 位於 `kernel` 資料夾的檔案以[《GPL-2.0-only》](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)規範。\n- 非位於 `kernel` 資料夾的其他檔案以[《GPL-3.0-or-later》](https://www.gnu.org/licenses/gpl-3.0.html)規範。\n\n## 致謝名單\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/)：KernelSU 的靈感來源。\n- [Magisk](https://github.com/topjohnwu/Magisk)：強而有力的 Root 工具。\n- [genuine](https://github.com/brevent/genuine/)：用於確效 Apk v2 簽章。\n- [Diamorphine](https://github.com/m0nad/Diamorphine): 用於增進 Rootkit 技巧。\n"
  },
  {
    "path": "docs/README_VI.md",
    "content": "[English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | **Tiếng Việt** | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md)\n\n# KernelSU\n\n<img src=\"https://kernelsu.org/logo.png\" style=\"width: 96px;\" alt=\"logo\">\n\nGiải pháp root thông qua thay đổi trên Kernel hệ điều hành cho các thiết bị Android.\n\n[![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest)\n[![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu)\n[![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU)\n[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)\n[![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE)\n\n## Tính năng\n\n1. Hỗ trợ gói thực thi `su` và quản lý quyền root.\n2. Hệ thống mô-đun thông qua [metamodules](https://kernelsu.org/vi_VN/guide/metamodule.html): Cơ sở hạ tầng có thể cắm cho các sửa đổi systemless.\n3. [App Profile](https://kernelsu.org/guide/app-profile.html): Hạn chế quyền root của ứng dụng.\n\n## Tình trạng tương thích\n\nKernelSU chính thức hỗ trợ các thiết bị Android với kernel GKI 2.0 (phiên bản kernel 5.10+), các phiên bản kernel cũ hơn (4.14+) cũng tương thích, nhưng bạn cần phải tự biên dịch.\n\nWSA, ChromeOS và Android dựa trên container(container-based) cũng được hỗ trợ bởi KernelSU.\n\nHiên tại Giao diện nhị phân của ứng dụng (ABI) được hỗ trợ bao gồm `arm64-v8a` và `x86_64`.\n\n## Sử dụng\n\n- [Hướng dẫn cài đặt](https://kernelsu.org/vi_VN/guide/installation.html)\n- [Cách để build?](https://kernelsu.org/vi_VN/guide/how-to-build.html)\n- [Website Chính Thức](https://kernelsu.org/vi_VN/)\n\n## Hỗ trợ dịch\n\nNếu bạn muốn hỗ trợ dịch KernelSU sang một ngôn ngữ khác hoặc cải thiện các bản dịch trước, vui lòng sử dụng [Weblate](https://hosted.weblate.org/engage/kernelsu/).\n\n## Thảo luận\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n\n## Giấy phép\n\n- Tất cả các file trong thư mục `kernel` dùng giấy phép [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html).\n- Tất cả các thành phần khác ngoại trừ thư mục `kernel` dùng giấy phép [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html).\n\n## Lời cảm ơn\n\n- [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): ý tưởng cho KernelSU.\n- [Magisk](https://github.com/topjohnwu/Magisk): công cụ root mạnh mẽ.\n- [genuine](https://github.com/brevent/genuine/): phương pháp xác thực apk v2.\n- [Diamorphine](https://github.com/m0nad/Diamorphine): các phương pháp ẩn của rootkit.\n"
  },
  {
    "path": "fastlane/metadata/android/en-US/full_description.txt",
    "content": "<p><i>KernelSU</i> is a Kernel based root solution for Android devices. It features kernel-based <code>su</code> and root access management as well as a Module system based on overlayfs (similar to Magisk). <i>KernelSU</i> works whitelist-based: Only App that is granted root permission can access <code>su</code>, other apps cannot perceive <code>su</code>.</p><p><i>KernelSU</i> officially supports Android GKI 2.0 devices(with kernel 5.10+), old kernels(4.14+) is also compatiable, but you need to build kernel yourself. WSA and containter-based Android should also work with <i>KernelSU</i> integrated.</p><p>Current supported ABIs are: <code>arm64-v8a</code> and <code>x86_64</code>.</p>"
  },
  {
    "path": "fastlane/metadata/android/en-US/short_description.txt",
    "content": "Kernel based root solution for Android"
  },
  {
    "path": "js/README.md",
    "content": "# Library for KernelSU's module WebUI\n\n## Install\n\n```sh\nyarn add kernelsu\n```\n\n## API\n\n### exec\n\nSpawns a **root** shell and runs a command within that shell, returning a Promise that resolves with the `stdout` and `stderr` outputs upon completion.\n\n- `command` `<string>` The command to run, with space-separated arguments.\n- `options` `<Object>`\n  - `cwd` - Current working directory of the child process.\n  - `env` - Environment key-value pairs.\n\n```javascript\nimport { exec } from 'kernelsu';\n\nconst { errno, stdout, stderr } = await exec('ls -l', { cwd: '/tmp' });\nif (errno === 0) {\n    // success\n    console.log(stdout);\n}\n```\n\n### spawn\n\nSpawns a new process using the given `command` in **root** shell, with command-line arguments in `args`. If omitted, `args` defaults to an empty array.\n\nReturns a `ChildProcess` instance. Instances of `ChildProcess` represent spawned child processes.\n\n- `command` `<string>` The command to run.\n- `args` `<string[]>` List of string arguments.\n- `options` `<Object>`:\n  - `cwd` `<string>` - Current working directory of the child process.\n  - `env` `<Object>` - Environment key-value pairs.\n\nExample of running `ls -lh /data`, capturing `stdout`, `stderr`, and the exit code:\n\n```javascript\nimport { spawn } from 'kernelsu';\n\nconst ls = spawn('ls', ['-lh', '/data']);\n\nls.stdout.on('data', (data) => {\n  console.log(`stdout: ${data}`);\n});\n\nls.stderr.on('data', (data) => {\n  console.log(`stderr: ${data}`);\n});\n\nls.on('exit', (code) => {\n  console.log(`child process exited with code ${code}`);\n});\n```\n\n#### ChildProcess\n\n##### Event 'exit'\n\n- `code` `<number>` The exit code if the child process exited on its own.\n\nThe `'exit'` event is emitted when the child process ends. If the process exits, `code` contains the final exit code; otherwise, it is null.\n\n##### Event 'error'\n\n- `err` `<Error>` The error.\n\nThe `'error'` event is emitted whenever:\n\n- The process could not be spawned.\n- The process could not be killed.\n\n##### `stdout`\n\nA `Readable Stream` that represents the child process's `stdout`.\n\n```javascript\nconst subprocess = spawn('ls');\n\nsubprocess.stdout.on('data', (data) => {\n  console.log(`Received chunk ${data}`);\n});\n```\n\n#### `stderr`\n\nA `Readable Stream` that represents the child process's `stderr`.\n\n### fullScreen\n\nRequest the WebView enter/exit full screen.\n\n```javascript\nimport { fullScreen } from 'kernelsu';\nfullScreen(true);\n```\n\n### enableEdgeToEdge\n\nRequest the WebView to set padding to 0 or safeDrawing insets\n\n- tips: this is disabled by default but if you request resource from `internal/insets.css`, this will be enabled automatically.\n- To get insets value and enable this automatically, you can\n  - add `@import \"https://mui.kernelsu.org/internal/insets.css\";` in css OR\n  - add `<link rel=\"stylesheet\" type=\"text/css\" href=\"/internal/insets.css\" />` in html.\n\n```javascript\nimport { enableEdgeToEdge } from 'kernelsu';\nenableEdgeToEdge(true);\n```\n\n### toast\n\nShow a toast message.\n\n```javascript\nimport { toast } from 'kernelsu';\ntoast('Hello, world!');\n```\n\n### moduleInfo\n\nGet module info.\n\n```javascript\nimport { moduleInfo } from 'kernelsu';\n// print moduleId in console\nconsole.log(moduleInfo());\n```\n\n### listPackages\n\nList installed packages.\n\nReturns an array of package names.\n\n- `type` `<string>` The type of packages to list: \"user\", \"system\", or \"all\".\n\n```javascript\nimport { listPackages } from 'kernelsu';\n// list user packages\nconst packages = listPackages(\"user\");\n```\n\n- tips: when `listPackages` api is available, you can use ksu://icon/{packageName} to get app icon.\n\n``` javascript\nimg.src = \"ksu://icon/\" + packageName;\n```\n\n### getPackagesInfo\n\nGet information for a list of packages.\n\nReturns an array of `PackagesInfo` objects.\n\n- `packages` `<string[]>` The list of package names.\n\n```javascript\nimport { getPackagesInfo } from 'kernelsu';\nconst packages = getPackagesInfo(['com.android.settings', 'com.android.shell']);\n```\n\n#### PackagesInfo\n\nAn object contains:\n\n- `packageName` `<string>` Package name of the application.\n- `versionName` `<string>` Version of the application.\n- `versionCode` `<number>` Version code of the application.\n- `appLabel` `<string>` Display name of the application.\n- `isSystem` `<boolean>` Whether the application is a system app.\n- `uid` `<number>` UID of the application.\n\n### exit\n\nExit the current WebUI activity.\n\n```javascript\nimport { exit } from 'kernelsu';\nexit();\n```\n"
  },
  {
    "path": "js/index.d.ts",
    "content": "interface ExecOptions {\n    cwd?: string,\n    env?: { [key: string]: string }\n}\n\ninterface ExecResults {\n    errno: number,\n    stdout: string,\n    stderr: string\n}\n\ndeclare function exec(command: string): Promise<ExecResults>;\ndeclare function exec(command: string, options: ExecOptions): Promise<ExecResults>;\n\ninterface SpawnOptions {\n    cwd?: string,\n    env?: { [key: string]: string }\n}\n\ninterface Stdio {\n    on(event: 'data', callback: (data: string) => void)\n}\n\ninterface ChildProcess {\n    stdout: Stdio,\n    stderr: Stdio,\n    on(event: 'exit', callback: (code: number) => void)\n    on(event: 'error', callback: (err: any) => void)\n}\n\ndeclare function spawn(command: string): ChildProcess;\ndeclare function spawn(command: string, args: string[]): ChildProcess;\ndeclare function spawn(command: string, options: SpawnOptions): ChildProcess;\ndeclare function spawn(command: string, args: string[], options: SpawnOptions): ChildProcess;\n\ndeclare function fullScreen(isFullScreen: boolean);\n\ndeclare function enableEdgeToEdge(enable: boolean);\n\ndeclare function toast(message: string);\n\ndeclare function moduleInfo(): string;\n\ninterface PackagesInfo {\n    packageName: string;\n    versionName: string;\n    versionCode: number;\n    appLabel: string;\n    isSystem: boolean;\n    uid: number;\n}\n\ndeclare function listPackages(type: string): string[];\n\ndeclare function getPackagesInfo(packages: string[]): PackagesInfo[];\n\ndeclare function exit();\n\nexport {\n    exec,\n    spawn,\n    fullScreen,\n    enableEdgeToEdge,\n    toast,\n    moduleInfo,\n    listPackages,\n    getPackagesInfo,\n    exit,\n}\n"
  },
  {
    "path": "js/index.js",
    "content": "let callbackCounter = 0;\nfunction getUniqueCallbackName(prefix) {\n  return `${prefix}_callback_${Date.now()}_${callbackCounter++}`;\n}\n\nexport function exec(command, options) {\n  if (typeof options === \"undefined\") {\n    options = {};\n  }\n\n  return new Promise((resolve, reject) => {\n    // Generate a unique callback function name\n    const callbackFuncName = getUniqueCallbackName(\"exec\");\n\n    // Define the success callback function\n    window[callbackFuncName] = (errno, stdout, stderr) => {\n      resolve({ errno, stdout, stderr });\n      cleanup(callbackFuncName);\n    };\n\n    function cleanup(successName) {\n      delete window[successName];\n    }\n\n    try {\n      ksu.exec(command, JSON.stringify(options), callbackFuncName);\n    } catch (error) {\n      reject(error);\n      cleanup(callbackFuncName);\n    }\n  });\n}\n\nfunction Stdio() {\n    this.listeners = {};\n  }\n  \n  Stdio.prototype.on = function (event, listener) {\n    if (!this.listeners[event]) {\n      this.listeners[event] = [];\n    }\n    this.listeners[event].push(listener);\n  };\n  \n  Stdio.prototype.emit = function (event, ...args) {\n    if (this.listeners[event]) {\n      this.listeners[event].forEach((listener) => listener(...args));\n    }\n  };\n  \n  function ChildProcess() {\n    this.listeners = {};\n    this.stdin = new Stdio();\n    this.stdout = new Stdio();\n    this.stderr = new Stdio();\n  }\n  \n  ChildProcess.prototype.on = function (event, listener) {\n    if (!this.listeners[event]) {\n      this.listeners[event] = [];\n    }\n    this.listeners[event].push(listener);\n  };\n  \n  ChildProcess.prototype.emit = function (event, ...args) {\n    if (this.listeners[event]) {\n      this.listeners[event].forEach((listener) => listener(...args));\n    }\n  };\n  \n  export function spawn(command, args, options) {\n    if (typeof args === \"undefined\") {\n      args = [];\n    } else if (!(args instanceof Array)) {\n        // allow for (command, options) signature\n        options = args;\n    }\n    \n    if (typeof options === \"undefined\") {\n      options = {};\n    }\n  \n    const child = new ChildProcess();\n    const childCallbackName = getUniqueCallbackName(\"spawn\");\n    window[childCallbackName] = child;\n  \n    function cleanup(name) {\n      delete window[name];\n    }\n\n    child.on(\"exit\", code => {\n        cleanup(childCallbackName);\n    });\n\n    try {\n      ksu.spawn(\n        command,\n        JSON.stringify(args),\n        JSON.stringify(options),\n        childCallbackName\n      );\n    } catch (error) {\n      child.emit(\"error\", error);\n      cleanup(childCallbackName);\n    }\n    return child;\n  }\n\nexport function fullScreen(isFullScreen) {\n  ksu.fullScreen(isFullScreen);\n}\n\nexport function enableEdgeToEdge(enable) {\n  ksu.enableEdgeToEdge(enable);\n}\n\nexport function toast(message) {\n  ksu.toast(message);\n}\n\nexport function moduleInfo() {\n  return ksu.moduleInfo();\n}\n\nexport function listPackages(type) {\n  try {\n    return JSON.parse(ksu.listPackages(type));\n  } catch (error) {\n    return [];\n  }\n}\n\nexport function getPackagesInfo(packages) {\n  try {\n    if (typeof packages !== \"string\") {\n      packages = JSON.stringify(packages);\n    }\n    return JSON.parse(ksu.getPackagesInfo(packages));\n  } catch (error) {\n    return [];\n  }\n}\n\nexport function exit() {\n  ksu.exit();\n}\n"
  },
  {
    "path": "js/package.json",
    "content": "{\n  \"name\": \"kernelsu\",\n  \"version\": \"3.0.2\",\n  \"description\": \"Library for KernelSU's module WebUI\",\n  \"main\": \"index.js\",\n  \"types\": \"index.d.ts\",\n  \"scripts\": {\n    \"test\": \"npm run test\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/tiann/KernelSU.git\"\n  },\n  \"keywords\": [\n    \"su\",\n    \"kernelsu\",\n    \"module\",\n    \"webui\"\n  ],\n  \"author\": \"weishu\",\n  \"license\": \"Apache-2.0\",\n  \"bugs\": {\n    \"url\": \"https://github.com/tiann/KernelSU/issues\"\n  },\n  \"homepage\": \"https://github.com/tiann/KernelSU#readme\"\n}\n"
  },
  {
    "path": "justfile",
    "content": "alias bk := build_ksud\nalias bm := build_manager\n\nbuild_ksud:\n    cross build --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml\n\nbuild_manager: build_ksud\n    cp userspace/ksud/target/aarch64-linux-android/release/ksud manager/app/src/main/jniLibs/arm64-v8a/libksud.so\n    cd manager && ./gradlew aDebug\n\nclippy:\n    cargo fmt --manifest-path ./userspace/ksud/Cargo.toml\n    cross clippy --target x86_64-pc-windows-gnu --release --manifest-path ./userspace/ksud/Cargo.toml\n    cross clippy --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml\n"
  },
  {
    "path": "kernel/.clang-format",
    "content": "# SPDX-License-Identifier: GPL-2.0\n#\n# clang-format configuration file. Intended for clang-format >= 4.\n#\n# For more information, see:\n#\n#   Documentation/process/clang-format.rst\n#   https://clang.llvm.org/docs/ClangFormat.html\n#   https://clang.llvm.org/docs/ClangFormatStyleOptions.html\n#\n---\nAccessModifierOffset: -4\nAlignAfterOpenBracket: Align\nAlignConsecutiveAssignments: false\nAlignConsecutiveDeclarations: false\n#AlignEscapedNewlines: Left # Unknown to clang-format-4.0\nAlignOperands: true\nAlignTrailingComments: false\nAllowAllParametersOfDeclarationOnNextLine: false\nAllowShortBlocksOnASingleLine: false\nAllowShortCaseLabelsOnASingleLine: false\nAllowShortFunctionsOnASingleLine: None\nAllowShortIfStatementsOnASingleLine: false\nAllowShortLoopsOnASingleLine: false\nAlwaysBreakAfterDefinitionReturnType: None\nAlwaysBreakAfterReturnType: None\nAlwaysBreakBeforeMultilineStrings: false\nAlwaysBreakTemplateDeclarations: false\nBinPackArguments: true\nBinPackParameters: true\nBraceWrapping:\n  AfterClass: false\n  AfterControlStatement: false\n  AfterEnum: false\n  AfterFunction: true\n  AfterNamespace: true\n  AfterObjCDeclaration: false\n  AfterStruct: false\n  AfterUnion: false\n  #AfterExternBlock: false # Unknown to clang-format-5.0\n  BeforeCatch: false\n  BeforeElse: false\n  IndentBraces: false\n  #SplitEmptyFunction: true # Unknown to clang-format-4.0\n  #SplitEmptyRecord: true # Unknown to clang-format-4.0\n  #SplitEmptyNamespace: true # Unknown to clang-format-4.0\nBreakBeforeBinaryOperators: None\nBreakBeforeBraces: Custom\n#BreakBeforeInheritanceComma: false # Unknown to clang-format-4.0\nBreakBeforeTernaryOperators: false\nBreakConstructorInitializersBeforeComma: false\n#BreakConstructorInitializers: BeforeComma # Unknown to clang-format-4.0\nBreakAfterJavaFieldAnnotations: false\nBreakStringLiterals: false\nColumnLimit: 80\nCommentPragmas: '^ IWYU pragma:'\n#CompactNamespaces: false # Unknown to clang-format-4.0\nConstructorInitializerAllOnOneLineOrOnePerLine: false\nConstructorInitializerIndentWidth: 4\nContinuationIndentWidth: 4\nCpp11BracedListStyle: false\nDerivePointerAlignment: false\nDisableFormat: false\nExperimentalAutoDetectBinPacking: false\n#FixNamespaceComments: false # Unknown to clang-format-4.0\n\n# Taken from:\n#   git grep -h '^#define [^[:space:]]*for_each[^[:space:]]*(' include/ \\\n#   | sed \"s,^#define \\([^[:space:]]*for_each[^[:space:]]*\\)(.*$,  - '\\1',\" \\\n#   | sort | uniq\nForEachMacros:\n  - 'apei_estatus_for_each_section'\n  - 'ata_for_each_dev'\n  - 'ata_for_each_link'\n  - '__ata_qc_for_each'\n  - 'ata_qc_for_each'\n  - 'ata_qc_for_each_raw'\n  - 'ata_qc_for_each_with_internal'\n  - 'ax25_for_each'\n  - 'ax25_uid_for_each'\n  - '__bio_for_each_bvec'\n  - 'bio_for_each_bvec'\n  - 'bio_for_each_bvec_all'\n  - 'bio_for_each_integrity_vec'\n  - '__bio_for_each_segment'\n  - 'bio_for_each_segment'\n  - 'bio_for_each_segment_all'\n  - 'bio_list_for_each'\n  - 'bip_for_each_vec'\n  - 'bitmap_for_each_clear_region'\n  - 'bitmap_for_each_set_region'\n  - 'blkg_for_each_descendant_post'\n  - 'blkg_for_each_descendant_pre'\n  - 'blk_queue_for_each_rl'\n  - 'bond_for_each_slave'\n  - 'bond_for_each_slave_rcu'\n  - 'bpf_for_each_spilled_reg'\n  - 'btree_for_each_safe128'\n  - 'btree_for_each_safe32'\n  - 'btree_for_each_safe64'\n  - 'btree_for_each_safel'\n  - 'card_for_each_dev'\n  - 'cgroup_taskset_for_each'\n  - 'cgroup_taskset_for_each_leader'\n  - 'cpufreq_for_each_entry'\n  - 'cpufreq_for_each_entry_idx'\n  - 'cpufreq_for_each_valid_entry'\n  - 'cpufreq_for_each_valid_entry_idx'\n  - 'css_for_each_child'\n  - 'css_for_each_descendant_post'\n  - 'css_for_each_descendant_pre'\n  - 'device_for_each_child_node'\n  - 'dma_fence_chain_for_each'\n  - 'do_for_each_ftrace_op'\n  - 'drm_atomic_crtc_for_each_plane'\n  - 'drm_atomic_crtc_state_for_each_plane'\n  - 'drm_atomic_crtc_state_for_each_plane_state'\n  - 'drm_atomic_for_each_plane_damage'\n  - 'drm_client_for_each_connector_iter'\n  - 'drm_client_for_each_modeset'\n  - 'drm_connector_for_each_possible_encoder'\n  - 'drm_for_each_bridge_in_chain'\n  - 'drm_for_each_connector_iter'\n  - 'drm_for_each_crtc'\n  - 'drm_for_each_encoder'\n  - 'drm_for_each_encoder_mask'\n  - 'drm_for_each_fb'\n  - 'drm_for_each_legacy_plane'\n  - 'drm_for_each_plane'\n  - 'drm_for_each_plane_mask'\n  - 'drm_for_each_privobj'\n  - 'drm_mm_for_each_hole'\n  - 'drm_mm_for_each_node'\n  - 'drm_mm_for_each_node_in_range'\n  - 'drm_mm_for_each_node_safe'\n  - 'flow_action_for_each'\n  - 'for_each_active_dev_scope'\n  - 'for_each_active_drhd_unit'\n  - 'for_each_active_iommu'\n  - 'for_each_aggr_pgid'\n  - 'for_each_available_child_of_node'\n  - 'for_each_bio'\n  - 'for_each_board_func_rsrc'\n  - 'for_each_bvec'\n  - 'for_each_card_auxs'\n  - 'for_each_card_auxs_safe'\n  - 'for_each_card_components'\n  - 'for_each_card_dapms'\n  - 'for_each_card_pre_auxs'\n  - 'for_each_card_prelinks'\n  - 'for_each_card_rtds'\n  - 'for_each_card_rtds_safe'\n  - 'for_each_card_widgets'\n  - 'for_each_card_widgets_safe'\n  - 'for_each_cgroup_storage_type'\n  - 'for_each_child_of_node'\n  - 'for_each_clear_bit'\n  - 'for_each_clear_bit_from'\n  - 'for_each_cmsghdr'\n  - 'for_each_compatible_node'\n  - 'for_each_component_dais'\n  - 'for_each_component_dais_safe'\n  - 'for_each_comp_order'\n  - 'for_each_console'\n  - 'for_each_cpu'\n  - 'for_each_cpu_and'\n  - 'for_each_cpu_not'\n  - 'for_each_cpu_wrap'\n  - 'for_each_dapm_widgets'\n  - 'for_each_dev_addr'\n  - 'for_each_dev_scope'\n  - 'for_each_displayid_db'\n  - 'for_each_dma_cap_mask'\n  - 'for_each_dpcm_be'\n  - 'for_each_dpcm_be_rollback'\n  - 'for_each_dpcm_be_safe'\n  - 'for_each_dpcm_fe'\n  - 'for_each_drhd_unit'\n  - 'for_each_dss_dev'\n  - 'for_each_efi_memory_desc'\n  - 'for_each_efi_memory_desc_in_map'\n  - 'for_each_element'\n  - 'for_each_element_extid'\n  - 'for_each_element_id'\n  - 'for_each_endpoint_of_node'\n  - 'for_each_evictable_lru'\n  - 'for_each_fib6_node_rt_rcu'\n  - 'for_each_fib6_walker_rt'\n  - 'for_each_free_mem_pfn_range_in_zone'\n  - 'for_each_free_mem_pfn_range_in_zone_from'\n  - 'for_each_free_mem_range'\n  - 'for_each_free_mem_range_reverse'\n  - 'for_each_func_rsrc'\n  - 'for_each_hstate'\n  - 'for_each_if'\n  - 'for_each_iommu'\n  - 'for_each_ip_tunnel_rcu'\n  - 'for_each_irq_nr'\n  - 'for_each_link_codecs'\n  - 'for_each_link_cpus'\n  - 'for_each_link_platforms'\n  - 'for_each_lru'\n  - 'for_each_matching_node'\n  - 'for_each_matching_node_and_match'\n  - 'for_each_member'\n  - 'for_each_mem_region'\n  - 'for_each_memblock_type'\n  - 'for_each_memcg_cache_index'\n  - 'for_each_mem_pfn_range'\n  - '__for_each_mem_range'\n  - 'for_each_mem_range'\n  - '__for_each_mem_range_rev'\n  - 'for_each_mem_range_rev'\n  - 'for_each_migratetype_order'\n  - 'for_each_msi_entry'\n  - 'for_each_msi_entry_safe'\n  - 'for_each_net'\n  - 'for_each_net_continue_reverse'\n  - 'for_each_netdev'\n  - 'for_each_netdev_continue'\n  - 'for_each_netdev_continue_rcu'\n  - 'for_each_netdev_continue_reverse'\n  - 'for_each_netdev_feature'\n  - 'for_each_netdev_in_bond_rcu'\n  - 'for_each_netdev_rcu'\n  - 'for_each_netdev_reverse'\n  - 'for_each_netdev_safe'\n  - 'for_each_net_rcu'\n  - 'for_each_new_connector_in_state'\n  - 'for_each_new_crtc_in_state'\n  - 'for_each_new_mst_mgr_in_state'\n  - 'for_each_new_plane_in_state'\n  - 'for_each_new_private_obj_in_state'\n  - 'for_each_node'\n  - 'for_each_node_by_name'\n  - 'for_each_node_by_type'\n  - 'for_each_node_mask'\n  - 'for_each_node_state'\n  - 'for_each_node_with_cpus'\n  - 'for_each_node_with_property'\n  - 'for_each_nonreserved_multicast_dest_pgid'\n  - 'for_each_of_allnodes'\n  - 'for_each_of_allnodes_from'\n  - 'for_each_of_cpu_node'\n  - 'for_each_of_pci_range'\n  - 'for_each_old_connector_in_state'\n  - 'for_each_old_crtc_in_state'\n  - 'for_each_old_mst_mgr_in_state'\n  - 'for_each_oldnew_connector_in_state'\n  - 'for_each_oldnew_crtc_in_state'\n  - 'for_each_oldnew_mst_mgr_in_state'\n  - 'for_each_oldnew_plane_in_state'\n  - 'for_each_oldnew_plane_in_state_reverse'\n  - 'for_each_oldnew_private_obj_in_state'\n  - 'for_each_old_plane_in_state'\n  - 'for_each_old_private_obj_in_state'\n  - 'for_each_online_cpu'\n  - 'for_each_online_node'\n  - 'for_each_online_pgdat'\n  - 'for_each_pci_bridge'\n  - 'for_each_pci_dev'\n  - 'for_each_pci_msi_entry'\n  - 'for_each_pcm_streams'\n  - 'for_each_physmem_range'\n  - 'for_each_populated_zone'\n  - 'for_each_possible_cpu'\n  - 'for_each_present_cpu'\n  - 'for_each_prime_number'\n  - 'for_each_prime_number_from'\n  - 'for_each_process'\n  - 'for_each_process_thread'\n  - 'for_each_property_of_node'\n  - 'for_each_registered_fb'\n  - 'for_each_requested_gpio'\n  - 'for_each_requested_gpio_in_range'\n  - 'for_each_reserved_mem_range'\n  - 'for_each_reserved_mem_region'\n  - 'for_each_rtd_codec_dais'\n  - 'for_each_rtd_codec_dais_rollback'\n  - 'for_each_rtd_components'\n  - 'for_each_rtd_cpu_dais'\n  - 'for_each_rtd_cpu_dais_rollback'\n  - 'for_each_rtd_dais'\n  - 'for_each_set_bit'\n  - 'for_each_set_bit_from'\n  - 'for_each_set_clump8'\n  - 'for_each_sg'\n  - 'for_each_sg_dma_page'\n  - 'for_each_sg_page'\n  - 'for_each_sgtable_dma_page'\n  - 'for_each_sgtable_dma_sg'\n  - 'for_each_sgtable_page'\n  - 'for_each_sgtable_sg'\n  - 'for_each_sibling_event'\n  - 'for_each_subelement'\n  - 'for_each_subelement_extid'\n  - 'for_each_subelement_id'\n  - '__for_each_thread'\n  - 'for_each_thread'\n  - 'for_each_unicast_dest_pgid'\n  - 'for_each_wakeup_source'\n  - 'for_each_zone'\n  - 'for_each_zone_zonelist'\n  - 'for_each_zone_zonelist_nodemask'\n  - 'fwnode_for_each_available_child_node'\n  - 'fwnode_for_each_child_node'\n  - 'fwnode_graph_for_each_endpoint'\n  - 'gadget_for_each_ep'\n  - 'genradix_for_each'\n  - 'genradix_for_each_from'\n  - 'hash_for_each'\n  - 'hash_for_each_possible'\n  - 'hash_for_each_possible_rcu'\n  - 'hash_for_each_possible_rcu_notrace'\n  - 'hash_for_each_possible_safe'\n  - 'hash_for_each_rcu'\n  - 'hash_for_each_safe'\n  - 'hctx_for_each_ctx'\n  - 'hlist_bl_for_each_entry'\n  - 'hlist_bl_for_each_entry_rcu'\n  - 'hlist_bl_for_each_entry_safe'\n  - 'hlist_for_each'\n  - 'hlist_for_each_entry'\n  - 'hlist_for_each_entry_continue'\n  - 'hlist_for_each_entry_continue_rcu'\n  - 'hlist_for_each_entry_continue_rcu_bh'\n  - 'hlist_for_each_entry_from'\n  - 'hlist_for_each_entry_from_rcu'\n  - 'hlist_for_each_entry_rcu'\n  - 'hlist_for_each_entry_rcu_bh'\n  - 'hlist_for_each_entry_rcu_notrace'\n  - 'hlist_for_each_entry_safe'\n  - '__hlist_for_each_rcu'\n  - 'hlist_for_each_safe'\n  - 'hlist_nulls_for_each_entry'\n  - 'hlist_nulls_for_each_entry_from'\n  - 'hlist_nulls_for_each_entry_rcu'\n  - 'hlist_nulls_for_each_entry_safe'\n  - 'i3c_bus_for_each_i2cdev'\n  - 'i3c_bus_for_each_i3cdev'\n  - 'ide_host_for_each_port'\n  - 'ide_port_for_each_dev'\n  - 'ide_port_for_each_present_dev'\n  - 'idr_for_each_entry'\n  - 'idr_for_each_entry_continue'\n  - 'idr_for_each_entry_continue_ul'\n  - 'idr_for_each_entry_ul'\n  - 'in_dev_for_each_ifa_rcu'\n  - 'in_dev_for_each_ifa_rtnl'\n  - 'inet_bind_bucket_for_each'\n  - 'inet_lhash2_for_each_icsk_rcu'\n  - 'key_for_each'\n  - 'key_for_each_safe'\n  - 'klp_for_each_func'\n  - 'klp_for_each_func_safe'\n  - 'klp_for_each_func_static'\n  - 'klp_for_each_object'\n  - 'klp_for_each_object_safe'\n  - 'klp_for_each_object_static'\n  - 'kunit_suite_for_each_test_case'\n  - 'kvm_for_each_memslot'\n  - 'kvm_for_each_vcpu'\n  - 'list_for_each'\n  - 'list_for_each_codec'\n  - 'list_for_each_codec_safe'\n  - 'list_for_each_continue'\n  - 'list_for_each_entry'\n  - 'list_for_each_entry_continue'\n  - 'list_for_each_entry_continue_rcu'\n  - 'list_for_each_entry_continue_reverse'\n  - 'list_for_each_entry_from'\n  - 'list_for_each_entry_from_rcu'\n  - 'list_for_each_entry_from_reverse'\n  - 'list_for_each_entry_lockless'\n  - 'list_for_each_entry_rcu'\n  - 'list_for_each_entry_reverse'\n  - 'list_for_each_entry_safe'\n  - 'list_for_each_entry_safe_continue'\n  - 'list_for_each_entry_safe_from'\n  - 'list_for_each_entry_safe_reverse'\n  - 'list_for_each_prev'\n  - 'list_for_each_prev_safe'\n  - 'list_for_each_safe'\n  - 'llist_for_each'\n  - 'llist_for_each_entry'\n  - 'llist_for_each_entry_safe'\n  - 'llist_for_each_safe'\n  - 'mci_for_each_dimm'\n  - 'media_device_for_each_entity'\n  - 'media_device_for_each_intf'\n  - 'media_device_for_each_link'\n  - 'media_device_for_each_pad'\n  - 'nanddev_io_for_each_page'\n  - 'netdev_for_each_lower_dev'\n  - 'netdev_for_each_lower_private'\n  - 'netdev_for_each_lower_private_rcu'\n  - 'netdev_for_each_mc_addr'\n  - 'netdev_for_each_uc_addr'\n  - 'netdev_for_each_upper_dev_rcu'\n  - 'netdev_hw_addr_list_for_each'\n  - 'nft_rule_for_each_expr'\n  - 'nla_for_each_attr'\n  - 'nla_for_each_nested'\n  - 'nlmsg_for_each_attr'\n  - 'nlmsg_for_each_msg'\n  - 'nr_neigh_for_each'\n  - 'nr_neigh_for_each_safe'\n  - 'nr_node_for_each'\n  - 'nr_node_for_each_safe'\n  - 'of_for_each_phandle'\n  - 'of_property_for_each_string'\n  - 'of_property_for_each_u32'\n  - 'pci_bus_for_each_resource'\n  - 'pcm_for_each_format'\n  - 'ping_portaddr_for_each_entry'\n  - 'plist_for_each'\n  - 'plist_for_each_continue'\n  - 'plist_for_each_entry'\n  - 'plist_for_each_entry_continue'\n  - 'plist_for_each_entry_safe'\n  - 'plist_for_each_safe'\n  - 'pnp_for_each_card'\n  - 'pnp_for_each_dev'\n  - 'protocol_for_each_card'\n  - 'protocol_for_each_dev'\n  - 'queue_for_each_hw_ctx'\n  - 'radix_tree_for_each_slot'\n  - 'radix_tree_for_each_tagged'\n  - 'rbtree_postorder_for_each_entry_safe'\n  - 'rdma_for_each_block'\n  - 'rdma_for_each_port'\n  - 'rdma_umem_for_each_dma_block'\n  - 'resource_list_for_each_entry'\n  - 'resource_list_for_each_entry_safe'\n  - 'rhl_for_each_entry_rcu'\n  - 'rhl_for_each_rcu'\n  - 'rht_for_each'\n  - 'rht_for_each_entry'\n  - 'rht_for_each_entry_from'\n  - 'rht_for_each_entry_rcu'\n  - 'rht_for_each_entry_rcu_from'\n  - 'rht_for_each_entry_safe'\n  - 'rht_for_each_from'\n  - 'rht_for_each_rcu'\n  - 'rht_for_each_rcu_from'\n  - '__rq_for_each_bio'\n  - 'rq_for_each_bvec'\n  - 'rq_for_each_segment'\n  - 'scsi_for_each_prot_sg'\n  - 'scsi_for_each_sg'\n  - 'sctp_for_each_hentry'\n  - 'sctp_skb_for_each'\n  - 'shdma_for_each_chan'\n  - '__shost_for_each_device'\n  - 'shost_for_each_device'\n  - 'sk_for_each'\n  - 'sk_for_each_bound'\n  - 'sk_for_each_entry_offset_rcu'\n  - 'sk_for_each_from'\n  - 'sk_for_each_rcu'\n  - 'sk_for_each_safe'\n  - 'sk_nulls_for_each'\n  - 'sk_nulls_for_each_from'\n  - 'sk_nulls_for_each_rcu'\n  - 'snd_array_for_each'\n  - 'snd_pcm_group_for_each_entry'\n  - 'snd_soc_dapm_widget_for_each_path'\n  - 'snd_soc_dapm_widget_for_each_path_safe'\n  - 'snd_soc_dapm_widget_for_each_sink_path'\n  - 'snd_soc_dapm_widget_for_each_source_path'\n  - 'tb_property_for_each'\n  - 'tcf_exts_for_each_action'\n  - 'udp_portaddr_for_each_entry'\n  - 'udp_portaddr_for_each_entry_rcu'\n  - 'usb_hub_for_each_child'\n  - 'v4l2_device_for_each_subdev'\n  - 'v4l2_m2m_for_each_dst_buf'\n  - 'v4l2_m2m_for_each_dst_buf_safe'\n  - 'v4l2_m2m_for_each_src_buf'\n  - 'v4l2_m2m_for_each_src_buf_safe'\n  - 'virtio_device_for_each_vq'\n  - 'while_for_each_ftrace_op'\n  - 'xa_for_each'\n  - 'xa_for_each_marked'\n  - 'xa_for_each_range'\n  - 'xa_for_each_start'\n  - 'xas_for_each'\n  - 'xas_for_each_conflict'\n  - 'xas_for_each_marked'\n  - 'xbc_array_for_each_value'\n  - 'xbc_for_each_key_value'\n  - 'xbc_node_for_each_array_value'\n  - 'xbc_node_for_each_child'\n  - 'xbc_node_for_each_key_value'\n  - 'zorro_for_each_dev'\n\n#IncludeBlocks: Preserve # Unknown to clang-format-5.0\nIncludeCategories:\n  - Regex: '.*'\n    Priority: 1\nIncludeIsMainRegex: '(Test)?$'\nIndentCaseLabels: false\n#IndentPPDirectives: None # Unknown to clang-format-5.0\nIndentWidth: 4\nIndentWrappedFunctionNames: false\nJavaScriptQuotes: Leave\nJavaScriptWrapImports: true\nKeepEmptyLinesAtTheStartOfBlocks: false\nMacroBlockBegin: ''\nMacroBlockEnd: ''\nMaxEmptyLinesToKeep: 1\nNamespaceIndentation: None\n#ObjCBinPackProtocolList: Auto # Unknown to clang-format-5.0\nObjCBlockIndentWidth: 4\nObjCSpaceAfterProperty: true\nObjCSpaceBeforeProtocolList: true\n\n# Taken from git's rules\n#PenaltyBreakAssignment: 10 # Unknown to clang-format-4.0\nPenaltyBreakBeforeFirstCallParameter: 30\nPenaltyBreakComment: 10\nPenaltyBreakFirstLessLess: 0\nPenaltyBreakString: 10\nPenaltyExcessCharacter: 100\nPenaltyReturnTypeOnItsOwnLine: 60\n\nPointerAlignment: Right\nReflowComments: false\nSortIncludes: false\n#SortUsingDeclarations: false # Unknown to clang-format-4.0\nSpaceAfterCStyleCast: false\nSpaceAfterTemplateKeyword: true\nSpaceBeforeAssignmentOperators: true\n#SpaceBeforeCtorInitializerColon: true # Unknown to clang-format-5.0\n#SpaceBeforeInheritanceColon: true # Unknown to clang-format-5.0\nSpaceBeforeParens: ControlStatements\n#SpaceBeforeRangeBasedForLoopColon: true # Unknown to clang-format-5.0\nSpaceInEmptyParentheses: false\nSpacesBeforeTrailingComments: 1\nSpacesInAngles: false\nSpacesInContainerLiterals: false\nSpacesInCStyleCastParentheses: false\nSpacesInParentheses: false\nSpacesInSquareBrackets: false\nStandard: Cpp03\nTabWidth: 4\nUseTab: Never\n...\n"
  },
  {
    "path": "kernel/.clangd",
    "content": "Diagnostics:\n  UnusedIncludes: Strict\n  ClangTidy:\n    Remove: bugprone-sizeof-expression\n"
  },
  {
    "path": "kernel/.gitignore",
    "content": ".cache/\n.thinlto-cache/\ncompile_commands.json\n*.ko\n*.o\n*.mod\n*.lds\n*.mod.o\n.*.o*\n.*.mod*\n*.ko*\n*.mod.c\n*.symvers*\n*.order\n.*.ko.cmd\n.tmp_versions/\nlibs/\nobj/\n\nCLAUDE.md\n.ddk-version\n.vscode/settings.json\ncheck_symbol\n"
  },
  {
    "path": "kernel/Kbuild",
    "content": "kernelsu-objs := ksu.o\nkernelsu-objs += allowlist.o\nkernelsu-objs += app_profile.o\nkernelsu-objs += apk_sign.o\nkernelsu-objs += sucompat.o\nkernelsu-objs += syscall_hook_manager.o\nkernelsu-objs += throne_tracker.o\nkernelsu-objs += pkg_observer.o\nkernelsu-objs += setuid_hook.o\nkernelsu-objs += kernel_umount.o\nkernelsu-objs += supercalls.o\nkernelsu-objs += su_mount_ns.o\nkernelsu-objs += feature.o\nkernelsu-objs += ksud.o\nkernelsu-objs += seccomp_cache.o\nkernelsu-objs += file_wrapper.o\nkernelsu-objs += util.o\n\nkernelsu-objs += selinux/selinux.o\nkernelsu-objs += selinux/sepolicy.o\nkernelsu-objs += selinux/rules.o\nccflags-y += -I$(srctree)/security/selinux -I$(srctree)/security/selinux/include\nccflags-y += -I$(objtree)/security/selinux -include $(srctree)/include/uapi/asm-generic/errno.h\n\nobj-$(CONFIG_KSU) += kernelsu.o\n\nMDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))\n\n# Workaround bazel\n# https://github.com/tiann/KernelSU/pull/316#issuecomment-1479043219\nGIT := $(shell PATH=\"$$PATH\":/usr/bin:/usr/local/bin which git)\nifneq ($(GIT),)\n# Check if this is a git repository\n# Try to detect Git repo intelligently\nGIT_ROOT := $(shell cd $(MDIR) && $(GIT) rev-parse --show-toplevel 2>/dev/null)\nifneq ($(GIT_ROOT),)\nKERNEL_GIT_ROOT := $(shell cd $(srctree) && $(GIT) rev-parse --show-toplevel 2>/dev/null)\nifneq ($(GIT_ROOT),$(KERNEL_GIT_ROOT))\n# Only set version if it's a different repo from kernel\n$(shell cd $(GIT_ROOT) && [ -f .git/shallow ] && $(GIT) fetch --unshallow 2>/dev/null || true)\nKSU_GIT_VERSION := $(shell cd $(GIT_ROOT) && $(GIT) rev-list --count HEAD 2>/dev/null)\nKSU_GIT_VERSION_VALID := 1\n$(info -- KernelSU: Git repo detected at $(GIT_ROOT))\nendif\nendif\nelse\n$(warning -- KernelSU: Git not detected, make sure git is installed in your PATH!)\nendif\n\n\n# Calculate version if git version is available\nifdef KSU_GIT_VERSION_VALID\n# ksu_version: major * 10000 + git version + 200 for historical reasons\n$(eval KSU_VERSION=$(shell expr 30000 + $(KSU_GIT_VERSION)))\n$(info -- KernelSU version: $(KSU_VERSION))\nccflags-y += -DKSU_VERSION=$(KSU_VERSION)\nelse\n# If there is no .git directory, use default version\n$(warning \"KSU_GIT_VERSION not defined! It is better to make KernelSU a git repository!\")\nccflags-y += -DKSU_VERSION=16\nendif\n\nifndef KSU_EXPECTED_SIZE\nKSU_EXPECTED_SIZE := 0x033b\nendif\n\nifndef KSU_EXPECTED_HASH\nKSU_EXPECTED_HASH := c371061b19d8c7d7d6133c6a9bafe198fa944e50c1b31c9d8daa8d7f1fc2d2d6\nendif\n\nifdef KSU_MANAGER_PACKAGE\nccflags-y += -DKSU_MANAGER_PACKAGE=\\\"$(KSU_MANAGER_PACKAGE)\\\"\n$(info -- KernelSU Manager package name: $(KSU_MANAGER_PACKAGE))\nendif\n\n$(info -- KernelSU Manager signature size: $(KSU_EXPECTED_SIZE))\n$(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH))\n\nccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE)\nccflags-y += -DEXPECTED_HASH=\\\"$(KSU_EXPECTED_HASH)\\\"\n\nifdef KSU_EXPECTED_SIZE2\nifndef KSU_EXPECTED_HASH2\n$(error KSU_EXPECTED_HASH2 must be set when KSU_EXPECTED_SIZE2 is set)\nendif\nccflags-y += -DEXPECTED_SIZE2=$(KSU_EXPECTED_SIZE2)\nccflags-y += -DEXPECTED_HASH2=\\\"$(KSU_EXPECTED_HASH2)\\\"\n$(info -- KernelSU Manager signature size2: $(KSU_EXPECTED_SIZE2))\n$(info -- KernelSU Manager signature hash2: $(KSU_EXPECTED_HASH2))\nendif\n\nccflags-y += -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat -Wno-missing-prototypes\nccflags-y += -Wno-declaration-after-statement -Wno-unused-function\n\n# Keep a new line here!! Because someone may append config\n"
  },
  {
    "path": "kernel/Kconfig",
    "content": "menu \"KernelSU\"\n\nconfig KSU\n\ttristate \"KernelSU function support\"\n\tdepends on KPROBES && EXT4_FS\n\tdefault y\n\thelp\n\t  Enable kernel-level root privileges on Android System.\n\t  Requires CONFIG_KPROBES for kernel hooking support.\n\t  Requires CONFIG_EXT4_FS for `ext4_unregister_sysfs`.\n\t  To compile as a module, choose M here: the\n\t  module will be called kernelsu.\n\nconfig KSU_DEBUG\n\tbool \"KernelSU debug mode\"\n\tdepends on KSU\n\tdefault n\n\thelp\n\t  Enable KernelSU debug mode.\n\nendmenu\n"
  },
  {
    "path": "kernel/LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 2, June 1991\n\n Copyright (C) 1989, 1991 Free Software Foundation, Inc.,\n 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The licenses for most software are designed to take away your\nfreedom to share and change it.  By contrast, the GNU General Public\nLicense is intended to guarantee your freedom to share and change free\nsoftware--to make sure the software is free for all its users.  This\nGeneral Public License applies to most of the Free Software\nFoundation's software and to any other program whose authors commit to\nusing it.  (Some other Free Software Foundation software is covered by\nthe GNU Lesser General Public License instead.)  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthis service if you wish), that you receive source code or can get it\nif you want it, that you can change the software or use pieces of it\nin new free programs; and that you know you can do these things.\n\n  To protect your rights, we need to make restrictions that forbid\nanyone to deny you these rights or to ask you to surrender the rights.\nThese restrictions translate to certain responsibilities for you if you\ndistribute copies of the software, or if you modify it.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must give the recipients all the rights that\nyou have.  You must make sure that they, too, receive or can get the\nsource code.  And you must show them these terms so they know their\nrights.\n\n  We protect your rights with two steps: (1) copyright the software, and\n(2) offer you this license which gives you legal permission to copy,\ndistribute and/or modify the software.\n\n  Also, for each author's protection and ours, we want to make certain\nthat everyone understands that there is no warranty for this free\nsoftware.  If the software is modified by someone else and passed on, we\nwant its recipients to know that what they have is not the original, so\nthat any problems introduced by others will not reflect on the original\nauthors' reputations.\n\n  Finally, any free program is threatened constantly by software\npatents.  We wish to avoid the danger that redistributors of a free\nprogram will individually obtain patent licenses, in effect making the\nprogram proprietary.  To prevent this, we have made it clear that any\npatent must be licensed for everyone's free use or not licensed at all.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                    GNU GENERAL PUBLIC LICENSE\n   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION\n\n  0. This License applies to any program or other work which contains\na notice placed by the copyright holder saying it may be distributed\nunder the terms of this General Public License.  The \"Program\", below,\nrefers to any such program or work, and a \"work based on the Program\"\nmeans either the Program or any derivative work under copyright law:\nthat is to say, a work containing the Program or a portion of it,\neither verbatim or with modifications and/or translated into another\nlanguage.  (Hereinafter, translation is included without limitation in\nthe term \"modification\".)  Each licensee is addressed as \"you\".\n\nActivities other than copying, distribution and modification are not\ncovered by this License; they are outside its scope.  The act of\nrunning the Program is not restricted, and the output from the Program\nis covered only if its contents constitute a work based on the\nProgram (independent of having been made by running the Program).\nWhether that is true depends on what the Program does.\n\n  1. You may copy and distribute verbatim copies of the Program's\nsource code as you receive it, in any medium, provided that you\nconspicuously and appropriately publish on each copy an appropriate\ncopyright notice and disclaimer of warranty; keep intact all the\nnotices that refer to this License and to the absence of any warranty;\nand give any other recipients of the Program a copy of this License\nalong with the Program.\n\nYou may charge a fee for the physical act of transferring a copy, and\nyou may at your option offer warranty protection in exchange for a fee.\n\n  2. You may modify your copy or copies of the Program or any portion\nof it, thus forming a work based on the Program, and copy and\ndistribute such modifications or work under the terms of Section 1\nabove, provided that you also meet all of these conditions:\n\n    a) You must cause the modified files to carry prominent notices\n    stating that you changed the files and the date of any change.\n\n    b) You must cause any work that you distribute or publish, that in\n    whole or in part contains or is derived from the Program or any\n    part thereof, to be licensed as a whole at no charge to all third\n    parties under the terms of this License.\n\n    c) If the modified program normally reads commands interactively\n    when run, you must cause it, when started running for such\n    interactive use in the most ordinary way, to print or display an\n    announcement including an appropriate copyright notice and a\n    notice that there is no warranty (or else, saying that you provide\n    a warranty) and that users may redistribute the program under\n    these conditions, and telling the user how to view a copy of this\n    License.  (Exception: if the Program itself is interactive but\n    does not normally print such an announcement, your work based on\n    the Program is not required to print an announcement.)\n\nThese requirements apply to the modified work as a whole.  If\nidentifiable sections of that work are not derived from the Program,\nand can be reasonably considered independent and separate works in\nthemselves, then this License, and its terms, do not apply to those\nsections when you distribute them as separate works.  But when you\ndistribute the same sections as part of a whole which is a work based\non the Program, the distribution of the whole must be on the terms of\nthis License, whose permissions for other licensees extend to the\nentire whole, and thus to each and every part regardless of who wrote it.\n\nThus, it is not the intent of this section to claim rights or contest\nyour rights to work written entirely by you; rather, the intent is to\nexercise the right to control the distribution of derivative or\ncollective works based on the Program.\n\nIn addition, mere aggregation of another work not based on the Program\nwith the Program (or with a work based on the Program) on a volume of\na storage or distribution medium does not bring the other work under\nthe scope of this License.\n\n  3. You may copy and distribute the Program (or a work based on it,\nunder Section 2) in object code or executable form under the terms of\nSections 1 and 2 above provided that you also do one of the following:\n\n    a) Accompany it with the complete corresponding machine-readable\n    source code, which must be distributed under the terms of Sections\n    1 and 2 above on a medium customarily used for software interchange; or,\n\n    b) Accompany it with a written offer, valid for at least three\n    years, to give any third party, for a charge no more than your\n    cost of physically performing source distribution, a complete\n    machine-readable copy of the corresponding source code, to be\n    distributed under the terms of Sections 1 and 2 above on a medium\n    customarily used for software interchange; or,\n\n    c) Accompany it with the information you received as to the offer\n    to distribute corresponding source code.  (This alternative is\n    allowed only for noncommercial distribution and only if you\n    received the program in object code or executable form with such\n    an offer, in accord with Subsection b above.)\n\nThe source code for a work means the preferred form of the work for\nmaking modifications to it.  For an executable work, complete source\ncode means all the source code for all modules it contains, plus any\nassociated interface definition files, plus the scripts used to\ncontrol compilation and installation of the executable.  However, as a\nspecial exception, the source code distributed need not include\nanything that is normally distributed (in either source or binary\nform) with the major components (compiler, kernel, and so on) of the\noperating system on which the executable runs, unless that component\nitself accompanies the executable.\n\nIf distribution of executable or object code is made by offering\naccess to copy from a designated place, then offering equivalent\naccess to copy the source code from the same place counts as\ndistribution of the source code, even though third parties are not\ncompelled to copy the source along with the object code.\n\n  4. You may not copy, modify, sublicense, or distribute the Program\nexcept as expressly provided under this License.  Any attempt\notherwise to copy, modify, sublicense or distribute the Program is\nvoid, and will automatically terminate your rights under this License.\nHowever, parties who have received copies, or rights, from you under\nthis License will not have their licenses terminated so long as such\nparties remain in full compliance.\n\n  5. You are not required to accept this License, since you have not\nsigned it.  However, nothing else grants you permission to modify or\ndistribute the Program or its derivative works.  These actions are\nprohibited by law if you do not accept this License.  Therefore, by\nmodifying or distributing the Program (or any work based on the\nProgram), you indicate your acceptance of this License to do so, and\nall its terms and conditions for copying, distributing or modifying\nthe Program or works based on it.\n\n  6. Each time you redistribute the Program (or any work based on the\nProgram), the recipient automatically receives a license from the\noriginal licensor to copy, distribute or modify the Program subject to\nthese terms and conditions.  You may not impose any further\nrestrictions on the recipients' exercise of the rights granted herein.\nYou are not responsible for enforcing compliance by third parties to\nthis License.\n\n  7. If, as a consequence of a court judgment or allegation of patent\ninfringement or for any other reason (not limited to patent issues),\nconditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot\ndistribute so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you\nmay not distribute the Program at all.  For example, if a patent\nlicense would not permit royalty-free redistribution of the Program by\nall those who receive copies directly or indirectly through you, then\nthe only way you could satisfy both it and this License would be to\nrefrain entirely from distribution of the Program.\n\nIf any portion of this section is held invalid or unenforceable under\nany particular circumstance, the balance of the section is intended to\napply and the section as a whole is intended to apply in other\ncircumstances.\n\nIt is not the purpose of this section to induce you to infringe any\npatents or other property right claims or to contest validity of any\nsuch claims; this section has the sole purpose of protecting the\nintegrity of the free software distribution system, which is\nimplemented by public license practices.  Many people have made\ngenerous contributions to the wide range of software distributed\nthrough that system in reliance on consistent application of that\nsystem; it is up to the author/donor to decide if he or she is willing\nto distribute software through any other system and a licensee cannot\nimpose that choice.\n\nThis section is intended to make thoroughly clear what is believed to\nbe a consequence of the rest of this License.\n\n  8. If the distribution and/or use of the Program is restricted in\ncertain countries either by patents or by copyrighted interfaces, the\noriginal copyright holder who places the Program under this License\nmay add an explicit geographical distribution limitation excluding\nthose countries, so that distribution is permitted only in or among\ncountries not thus excluded.  In such case, this License incorporates\nthe limitation as if written in the body of this License.\n\n  9. The Free Software Foundation may publish revised and/or new versions\nof the General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\nEach version is given a distinguishing version number.  If the Program\nspecifies a version number of this License which applies to it and \"any\nlater version\", you have the option of following the terms and conditions\neither of that version or of any later version published by the Free\nSoftware Foundation.  If the Program does not specify a version number of\nthis License, you may choose any version ever published by the Free Software\nFoundation.\n\n  10. If you wish to incorporate parts of the Program into other free\nprograms whose distribution conditions are different, write to the author\nto ask for permission.  For software which is copyrighted by the Free\nSoftware Foundation, write to the Free Software Foundation; we sometimes\nmake exceptions for this.  Our decision will be guided by the two goals\nof preserving the free status of all derivatives of our free software and\nof promoting the sharing and reuse of software generally.\n\n                            NO WARRANTY\n\n  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY\nFOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN\nOTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES\nPROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED\nOR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\nMERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS\nTO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE\nPROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,\nREPAIR OR CORRECTION.\n\n  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR\nREDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,\nINCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING\nOUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED\nTO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY\nYOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER\nPROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGES.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nconvey the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software; you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation; either version 2 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License along\n    with this program; if not, write to the Free Software Foundation, Inc.,\n    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n\nAlso add information on how to contact you by electronic and paper mail.\n\nIf the program is interactive, make it output a short notice like this\nwhen it starts in an interactive mode:\n\n    Gnomovision version 69, Copyright (C) year name of author\n    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, the commands you use may\nbe called something other than `show w' and `show c'; they could even be\nmouse-clicks or menu items--whatever suits your program.\n\nYou should also get your employer (if you work as a programmer) or your\nschool, if any, to sign a \"copyright disclaimer\" for the program, if\nnecessary.  Here is a sample; alter the names:\n\n  Yoyodyne, Inc., hereby disclaims all copyright interest in the program\n  `Gnomovision' (which makes passes at compilers) written by James Hacker.\n\n  <signature of Ty Coon>, 1 April 1989\n  Ty Coon, President of Vice\n\nThis General Public License does not permit incorporating your program into\nproprietary programs.  If your program is a subroutine library, you may\nconsider it more useful to permit linking proprietary applications with the\nlibrary.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.\n"
  },
  {
    "path": "kernel/Makefile",
    "content": "KDIR := $(KDIR)\nMDIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))\n\n$(info -- KDIR: $(KDIR))\n$(info -- MDIR: $(MDIR))\n\n.PHONY: all compdb clean format check-format\n\nall: check_symbol\n\tmake -C $(KDIR) M=$(MDIR) modules\n\t./check_symbol kernelsu.ko $(KDIR)/vmlinux\ncompdb:\n\tpython3 $(MDIR)/.vscode/generate_compdb.py -O $(KDIR) $(MDIR)\nclean:\n\tmake -C $(KDIR) M=$(MDIR) clean\n\trm check_symbol\ncheck_symbol: tools/check_symbol.c\n\t$(CC) tools/check_symbol.c -o check_symbol\nformat:\n\tfind . \\( -name \"*.c\" -o -name \"*.h\" \\) -print0 | xargs -0 clang-format -i\ncheck-format:\n\tfind . \\( -name \"*.c\" -o -name \"*.h\" \\) -print0 | xargs -0 clang-format --dry-run --Werror\n\n# Keep a new line here!! Because someone may append config\n"
  },
  {
    "path": "kernel/allowlist.c",
    "content": "#include <linux/rcupdate.h>\n#include <linux/limits.h>\n#include <linux/rculist.h>\n#include <linux/mutex.h>\n#include <linux/task_work.h>\n#include <linux/capability.h>\n#include <linux/compiler.h>\n#include <linux/fs.h>\n#include <linux/gfp.h>\n#include <linux/kernel.h>\n#include <linux/list.h>\n#include <linux/printk.h>\n#include <linux/slab.h>\n#include <linux/types.h>\n#include <linux/version.h>\n#include <linux/compiler_types.h>\n\n#include \"klog.h\" // IWYU pragma: keep\n#include \"ksu.h\"\n#include \"ksud.h\"\n#include \"selinux/selinux.h\"\n#include \"allowlist.h\"\n#include \"manager.h\"\n#include \"su_mount_ns.h\"\n\n#define FILE_MAGIC 0x7f4b5355 // ' KSU', u32\n#define FILE_FORMAT_VERSION 3 // u32\n\n#define KSU_APP_PROFILE_PRESERVE_UID 9999 // NOBODY_UID\n#define KSU_DEFAULT_SELINUX_DOMAIN \"u:r:\" KERNEL_SU_DOMAIN \":s0\"\n\nstatic DEFINE_MUTEX(allowlist_mutex);\n\n// default profiles, these may be used frequently, so we cache it\nstatic struct root_profile default_root_profile;\nstatic struct non_root_profile default_non_root_profile;\n\nstatic int allow_list_arr[PAGE_SIZE / sizeof(int)] __read_mostly\n    __aligned(PAGE_SIZE);\nstatic int allow_list_pointer __read_mostly = 0;\n\nstatic void remove_uid_from_arr(uid_t uid)\n{\n    int i;\n    for (i = 0; i < allow_list_pointer; i++) {\n        if (allow_list_arr[i] == uid) {\n            int remaining = allow_list_pointer - 1 - i;\n            if (remaining > 0) {\n                memmove(&allow_list_arr[i], &allow_list_arr[i + 1],\n                        remaining * sizeof(allow_list_arr[0]));\n            }\n            allow_list_pointer--;\n            allow_list_arr[allow_list_pointer] = -1;\n            return;\n        }\n    }\n}\n\nstatic void init_default_profiles()\n{\n    kernel_cap_t full_cap = CAP_FULL_SET;\n\n    default_root_profile.uid = 0;\n    default_root_profile.gid = 0;\n    default_root_profile.groups_count = 1;\n    default_root_profile.groups[0] = 0;\n    memcpy(&default_root_profile.capabilities.effective, &full_cap,\n           sizeof(default_root_profile.capabilities.effective));\n    default_root_profile.namespaces = KSU_NS_INHERITED;\n    strcpy(default_root_profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN);\n\n    // This means that we will umount modules by default!\n    default_non_root_profile.umount_modules = true;\n}\n\nstruct perm_data {\n    struct list_head list;\n    struct rcu_head rcu;\n    struct app_profile profile;\n};\n\nstatic struct list_head allow_list;\n\nstatic uint8_t allow_list_bitmap[PAGE_SIZE] __read_mostly __aligned(PAGE_SIZE);\n#define BITMAP_UID_MAX ((sizeof(allow_list_bitmap) * BITS_PER_BYTE) - 1)\n\n#define KERNEL_SU_ALLOWLIST \"/data/adb/ksu/.allowlist\"\n\nvoid ksu_persistent_allow_list(void);\n\nvoid ksu_show_allow_list(void)\n{\n    struct perm_data *p = NULL;\n    pr_info(\"ksu_show_allow_list\\n\");\n    rcu_read_lock();\n    list_for_each_entry_rcu (p, &allow_list, list) {\n        pr_info(\"uid :%d, allow: %d\\n\", p->profile.current_uid,\n                p->profile.allow_su);\n    }\n    rcu_read_unlock();\n}\n\nbool ksu_get_app_profile(struct app_profile *profile)\n{\n    struct perm_data *p = NULL;\n    bool found = false;\n\n    rcu_read_lock();\n    list_for_each_entry_rcu (p, &allow_list, list) {\n        bool uid_match = profile->current_uid == p->profile.current_uid;\n        if (uid_match) {\n            // found it, override it with ours\n            memcpy(profile, &p->profile, sizeof(*profile));\n            found = true;\n            goto exit;\n        }\n    }\n\nexit:\n    rcu_read_unlock();\n    return found;\n}\n\nstatic inline bool forbid_system_uid(uid_t uid)\n{\n#define SHELL_UID 2000\n#define SYSTEM_UID 1000\n    return uid < SHELL_UID && uid != SYSTEM_UID;\n}\n\nstatic bool profile_valid(struct app_profile *profile)\n{\n    if (!profile) {\n        return false;\n    }\n\n    if (profile->version < KSU_APP_PROFILE_VER) {\n        pr_info(\"Unsupported profile version: %d\\n\", profile->version);\n        return false;\n    }\n\n    if (profile->allow_su) {\n        if (profile->rp_config.profile.groups_count > KSU_MAX_GROUPS) {\n            return false;\n        }\n\n        if (strlen(profile->rp_config.profile.selinux_domain) == 0) {\n            return false;\n        }\n    }\n\n    return true;\n}\n\nint ksu_set_app_profile(struct app_profile *profile)\n{\n    struct perm_data *p = NULL, *np;\n    int result = 0;\n    u16 count = 0;\n\n    if (!profile_valid(profile)) {\n        pr_err(\"Failed to set app profile: invalid profile!\\n\");\n        return -EINVAL;\n    }\n\n    mutex_lock(&allowlist_mutex);\n\n    list_for_each_entry (p, &allow_list, list) {\n        ++count;\n        // both uid and package must match, otherwise it will break multiple package with different user id\n        if (profile->current_uid == p->profile.current_uid &&\n            !strcmp(profile->key, p->profile.key)) {\n            // found it, just override it all!\n            np = (struct perm_data *)kzalloc(sizeof(struct perm_data),\n                                             GFP_KERNEL);\n            if (!np) {\n                result = -ENOMEM;\n                goto out_unlock;\n            }\n            memcpy(&np->profile, profile, sizeof(*profile));\n            list_replace_rcu(&p->list, &np->list);\n            kfree_rcu(p, rcu);\n            goto out;\n        }\n    }\n\n    if (unlikely(count == U16_MAX)) {\n        pr_err(\"too many app profile\\n\");\n        result = -E2BIG;\n        goto out_unlock;\n    }\n\n    // not found, alloc a new node!\n    p = (struct perm_data *)kzalloc(sizeof(struct perm_data), GFP_KERNEL);\n    if (!p) {\n        pr_err(\"ksu_set_app_profile alloc failed\\n\");\n        result = -ENOMEM;\n        goto out_unlock;\n    }\n\n    memcpy(&p->profile, profile, sizeof(*profile));\n    if (profile->allow_su) {\n        pr_info(\"set root profile, key: %s, uid: %d, gid: %d, context: %s\\n\",\n                profile->key, profile->current_uid,\n                profile->rp_config.profile.gid,\n                profile->rp_config.profile.selinux_domain);\n    } else {\n        pr_info(\"set app profile, key: %s, uid: %d, umount modules: %d\\n\",\n                profile->key, profile->current_uid,\n                profile->nrp_config.profile.umount_modules);\n    }\n\n    list_add_tail_rcu(&p->list, &allow_list);\n\nout:\n    result = 0;\n\n    // check if the default profiles is changed, cache it to a single struct to accelerate access.\n    if (unlikely(!strcmp(profile->key, \"$\"))) {\n        // set default non root profile\n        memcpy(&default_non_root_profile, &profile->nrp_config.profile,\n               sizeof(default_non_root_profile));\n    } else if (unlikely(!strcmp(profile->key, \"#\"))) {\n        // set default root profile\n        // TODO: Do we really need this?\n        memcpy(&default_root_profile, &profile->rp_config.profile,\n               sizeof(default_root_profile));\n    } else if (profile->current_uid <= BITMAP_UID_MAX) {\n        if (profile->allow_su)\n            allow_list_bitmap[profile->current_uid / BITS_PER_BYTE] |=\n                1 << (profile->current_uid % BITS_PER_BYTE);\n        else\n            allow_list_bitmap[profile->current_uid / BITS_PER_BYTE] &=\n                ~(1 << (profile->current_uid % BITS_PER_BYTE));\n    } else {\n        if (profile->allow_su) {\n            /*\n             * 1024 apps with uid higher than BITMAP_UID_MAX\n             * registered to request superuser?\n             */\n            if (allow_list_pointer >= ARRAY_SIZE(allow_list_arr)) {\n                pr_err(\"too many apps registered\\n\");\n                WARN_ON(1);\n            } else {\n                allow_list_arr[allow_list_pointer++] = profile->current_uid;\n            }\n        } else {\n            remove_uid_from_arr(profile->current_uid);\n        }\n    }\n\nout_unlock:\n    mutex_unlock(&allowlist_mutex);\n    return result;\n}\n\nbool __ksu_is_allow_uid(uid_t uid)\n{\n    int i;\n\n    if (forbid_system_uid(uid)) {\n        // do not bother going through the list if it's system\n        return false;\n    }\n\n    if (likely(ksu_is_manager_appid_valid()) &&\n        unlikely(ksu_get_manager_appid() == uid % PER_USER_RANGE)) {\n        // manager is always allowed!\n        return true;\n    }\n\n    if (unlikely(allow_shell) && uid == SHELL_UID) {\n        return true;\n    }\n\n    if (likely(uid <= BITMAP_UID_MAX)) {\n        return !!(allow_list_bitmap[uid / BITS_PER_BYTE] &\n                  (1 << (uid % BITS_PER_BYTE)));\n    } else {\n        for (i = 0; i < allow_list_pointer; i++) {\n            if (allow_list_arr[i] == uid)\n                return true;\n        }\n    }\n\n    return false;\n}\n\nbool __ksu_is_allow_uid_for_current(uid_t uid)\n{\n    if (unlikely(uid == 0)) {\n        // already root, but only allow our domain.\n        return is_ksu_domain();\n    }\n    return __ksu_is_allow_uid(uid);\n}\n\nbool ksu_uid_should_umount(uid_t uid)\n{\n    struct app_profile profile = { .current_uid = uid };\n    if (likely(ksu_is_manager_appid_valid()) &&\n        unlikely(ksu_get_manager_appid() == uid % PER_USER_RANGE)) {\n        // we should not umount on manager!\n        return false;\n    }\n    bool found = ksu_get_app_profile(&profile);\n    if (!found) {\n        // no app profile found, it must be non root app\n        return default_non_root_profile.umount_modules;\n    }\n    if (profile.allow_su) {\n        // if found and it is granted to su, we shouldn't umount for it\n        return false;\n    } else {\n        // found an app profile\n        if (profile.nrp_config.use_default) {\n            return default_non_root_profile.umount_modules;\n        } else {\n            return profile.nrp_config.profile.umount_modules;\n        }\n    }\n}\n\nvoid ksu_get_root_profile(uid_t uid, struct root_profile *profile)\n{\n    struct perm_data *p = NULL;\n\n    if (is_uid_manager(uid)) {\n        goto use_default;\n    }\n\n    if (unlikely(allow_shell && uid == SHELL_UID)) {\n        goto use_default;\n    }\n\n    rcu_read_lock();\n    list_for_each_entry_rcu (p, &allow_list, list) {\n        if (uid == p->profile.current_uid && p->profile.allow_su) {\n            if (!p->profile.rp_config.use_default) {\n                memcpy(profile, &p->profile.rp_config.profile,\n                       sizeof(*profile));\n                rcu_read_unlock();\n                return;\n            }\n        }\n    }\n    rcu_read_unlock();\n\nuse_default:\n    // use default profile\n    memcpy(profile, &default_root_profile, sizeof(*profile));\n}\n\nbool ksu_get_allow_list(int *array, u16 length, u16 *out_length, u16 *out_total,\n                        bool allow)\n{\n    struct perm_data *p = NULL;\n    u16 i = 0, j = 0;\n    rcu_read_lock();\n    list_for_each_entry_rcu (p, &allow_list, list) {\n        // pr_info(\"get_allow_list uid: %d allow: %d\\n\", p->uid, p->allow);\n        if (p->profile.allow_su == allow &&\n            !is_uid_manager(p->profile.current_uid)) {\n            if (j < length) {\n                array[j++] = p->profile.current_uid;\n            }\n            ++i;\n        }\n    }\n    rcu_read_unlock();\n    if (out_length) {\n        *out_length = j;\n    }\n    if (out_total) {\n        *out_total = i;\n    }\n\n    return true;\n}\n\n// TODO: move to kernel thread or work queue\nstatic void do_persistent_allow_list(struct callback_head *_cb)\n{\n    u32 magic = FILE_MAGIC;\n    u32 version = FILE_FORMAT_VERSION;\n    struct perm_data *p = NULL;\n    loff_t off = 0;\n\n    const struct cred *saved = override_creds(ksu_cred);\n    struct file *fp =\n        filp_open(KERNEL_SU_ALLOWLIST, O_WRONLY | O_CREAT | O_TRUNC, 0644);\n    if (IS_ERR(fp)) {\n        pr_err(\"save_allow_list create file failed: %ld\\n\", PTR_ERR(fp));\n        goto out;\n    }\n\n    // store magic and version\n    if (kernel_write(fp, &magic, sizeof(magic), &off) != sizeof(magic)) {\n        pr_err(\"save_allow_list write magic failed.\\n\");\n        goto close_file;\n    }\n\n    if (kernel_write(fp, &version, sizeof(version), &off) != sizeof(version)) {\n        pr_err(\"save_allow_list write version failed.\\n\");\n        goto close_file;\n    }\n\n    mutex_lock(&allowlist_mutex);\n    list_for_each_entry (p, &allow_list, list) {\n        pr_info(\"save allow list, name: %s uid :%d, allow: %d\\n\",\n                p->profile.key, p->profile.current_uid, p->profile.allow_su);\n\n        kernel_write(fp, &p->profile, sizeof(p->profile), &off);\n    }\n    mutex_unlock(&allowlist_mutex);\n\nclose_file:\n    filp_close(fp, 0);\nout:\n    revert_creds(saved);\n    kfree(_cb);\n}\n\nvoid ksu_persistent_allow_list()\n{\n    struct task_struct *tsk;\n\n    tsk = get_pid_task(find_vpid(1), PIDTYPE_PID);\n    if (!tsk) {\n        pr_err(\"save_allow_list find init task err\\n\");\n        return;\n    }\n\n    struct callback_head *cb =\n        kzalloc(sizeof(struct callback_head), GFP_KERNEL);\n    if (!cb) {\n        pr_err(\"save_allow_list alloc cb err\\b\");\n        goto put_task;\n    }\n    cb->func = do_persistent_allow_list;\n    if (task_work_add(tsk, cb, TWA_RESUME)) {\n        kfree(cb);\n        pr_warn(\"save_allow_list add task_work failed\\n\");\n    }\n\nput_task:\n    put_task_struct(tsk);\n}\n\nvoid ksu_load_allow_list()\n{\n    loff_t off = 0;\n    ssize_t ret = 0;\n    struct file *fp = NULL;\n    u32 magic;\n    u32 version;\n\n    // load allowlist now!\n    fp = filp_open(KERNEL_SU_ALLOWLIST, O_RDONLY, 0);\n    if (IS_ERR(fp)) {\n        pr_err(\"load_allow_list open file failed: %ld\\n\", PTR_ERR(fp));\n        return;\n    }\n\n    // verify magic\n    if (kernel_read(fp, &magic, sizeof(magic), &off) != sizeof(magic) ||\n        magic != FILE_MAGIC) {\n        pr_err(\"allowlist file invalid: %d!\\n\", magic);\n        goto exit;\n    }\n\n    if (kernel_read(fp, &version, sizeof(version), &off) != sizeof(version)) {\n        pr_err(\"allowlist read version: %d failed\\n\", version);\n        goto exit;\n    }\n\n    pr_info(\"allowlist version: %d\\n\", version);\n\n    while (true) {\n        struct app_profile profile;\n\n        ret = kernel_read(fp, &profile, sizeof(profile), &off);\n\n        if (ret <= 0) {\n            pr_info(\"load_allow_list read err: %zd\\n\", ret);\n            break;\n        }\n\n        pr_info(\"load_allow_uid, name: %s, uid: %d, allow: %d\\n\", profile.key,\n                profile.current_uid, profile.allow_su);\n        ksu_set_app_profile(&profile);\n    }\n\nexit:\n    ksu_show_allow_list();\n    filp_close(fp, 0);\n}\n\nvoid ksu_prune_allowlist(bool (*is_uid_valid)(uid_t, char *, void *),\n                         void *data)\n{\n    struct perm_data *np = NULL;\n    struct perm_data *n = NULL;\n\n    if (!ksu_boot_completed) {\n        pr_info(\"boot not completed, skip prune\\n\");\n        return;\n    }\n\n    bool modified = false;\n    mutex_lock(&allowlist_mutex);\n    list_for_each_entry_safe (np, n, &allow_list, list) {\n        uid_t uid = np->profile.current_uid;\n        char *package = np->profile.key;\n        // we use this uid for special cases, don't prune it!\n        bool is_preserved_uid = uid == KSU_APP_PROFILE_PRESERVE_UID;\n        if (!is_preserved_uid && !is_uid_valid(uid, package, data)) {\n            modified = true;\n            pr_info(\"prune uid: %d, package: %s\\n\", uid, package);\n            list_del_rcu(&np->list);\n            kfree_rcu(np, rcu);\n            if (likely(uid <= BITMAP_UID_MAX)) {\n                allow_list_bitmap[uid / BITS_PER_BYTE] &=\n                    ~(1 << (uid % BITS_PER_BYTE));\n            }\n            remove_uid_from_arr(uid);\n        }\n    }\n    mutex_unlock(&allowlist_mutex);\n\n    if (modified) {\n        smp_mb();\n        ksu_persistent_allow_list();\n    }\n}\n\nvoid ksu_allowlist_init(void)\n{\n    int i;\n\n    BUILD_BUG_ON(sizeof(allow_list_bitmap) != PAGE_SIZE);\n    BUILD_BUG_ON(sizeof(allow_list_arr) != PAGE_SIZE);\n\n    for (i = 0; i < ARRAY_SIZE(allow_list_arr); i++)\n        allow_list_arr[i] = -1;\n\n    INIT_LIST_HEAD(&allow_list);\n\n    init_default_profiles();\n}\n\nvoid ksu_allowlist_exit(void)\n{\n    struct perm_data *np = NULL;\n    struct perm_data *n = NULL;\n\n    // free allowlist\n    mutex_lock(&allowlist_mutex);\n    list_for_each_entry_safe (np, n, &allow_list, list) {\n        list_del(&np->list);\n        kfree(np);\n    }\n    mutex_unlock(&allowlist_mutex);\n}\n"
  },
  {
    "path": "kernel/allowlist.h",
    "content": "#ifndef __KSU_H_ALLOWLIST\n#define __KSU_H_ALLOWLIST\n\n#include <linux/types.h>\n#include <linux/uidgid.h>\n#include \"app_profile.h\"\n\n#define PER_USER_RANGE 100000\n#define FIRST_APPLICATION_UID 10000\n#define LAST_APPLICATION_UID 19999\n#define FIRST_ISOLATED_UID 99000\n#define LAST_ISOLATED_UID 99999\n\nvoid ksu_allowlist_init(void);\n\nvoid ksu_allowlist_exit(void);\n\nvoid ksu_load_allow_list(void);\n\nvoid ksu_show_allow_list(void);\n\n// Check if the uid is in allow list\nbool __ksu_is_allow_uid(uid_t uid);\n#define ksu_is_allow_uid(uid) unlikely(__ksu_is_allow_uid(uid))\n\n// Check if the uid is in allow list, or current is ksu domain root\nbool __ksu_is_allow_uid_for_current(uid_t uid);\n#define ksu_is_allow_uid_for_current(uid)                                      \\\n    unlikely(__ksu_is_allow_uid_for_current(uid))\n\nbool ksu_get_allow_list(int *array, u16 length, u16 *out_length, u16 *out_total,\n                        bool allow);\n\nvoid ksu_prune_allowlist(bool (*is_uid_exist)(uid_t, char *, void *),\n                         void *data);\nvoid ksu_persistent_allow_list();\n\nbool ksu_get_app_profile(struct app_profile *);\nint ksu_set_app_profile(struct app_profile *);\n\nbool ksu_uid_should_umount(uid_t uid);\nvoid ksu_get_root_profile(uid_t uid, struct root_profile *);\n\nstatic inline bool is_appuid(uid_t uid)\n{\n    uid_t appid = uid % PER_USER_RANGE;\n    return appid >= FIRST_APPLICATION_UID && appid <= LAST_APPLICATION_UID;\n}\n\nstatic inline bool is_isolated_process(uid_t uid)\n{\n    uid_t appid = uid % PER_USER_RANGE;\n    return appid >= FIRST_ISOLATED_UID && appid <= LAST_ISOLATED_UID;\n}\n#endif\n\nextern bool allow_shell;\n"
  },
  {
    "path": "kernel/apk_sign.c",
    "content": "#include <linux/err.h>\n#include <linux/fs.h>\n#include <linux/gfp.h>\n#include <linux/kernel.h>\n#include <linux/slab.h>\n#include <linux/version.h>\n#ifdef CONFIG_KSU_DEBUG\n#include <linux/moduleparam.h>\n#endif\n#include <crypto/hash.h>\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)\n#include <crypto/sha2.h>\n#else\n#include <crypto/sha.h>\n#endif\n\n#include \"apk_sign.h\"\n#include \"app_profile.h\"\n#include \"klog.h\" // IWYU pragma: keep\n\nstruct sdesc {\n    struct shash_desc shash;\n    char ctx[];\n};\n\nstatic struct sdesc *init_sdesc(struct crypto_shash *alg)\n{\n    struct sdesc *sdesc;\n    int size;\n\n    size = sizeof(struct shash_desc) + crypto_shash_descsize(alg);\n    sdesc = kzalloc(size, GFP_KERNEL);\n    if (!sdesc)\n        return ERR_PTR(-ENOMEM);\n    sdesc->shash.tfm = alg;\n    return sdesc;\n}\n\nstatic int calc_hash(struct crypto_shash *alg, const unsigned char *data,\n                     unsigned int datalen, unsigned char *digest)\n{\n    struct sdesc *sdesc;\n    int ret;\n\n    sdesc = init_sdesc(alg);\n    if (IS_ERR(sdesc)) {\n        pr_info(\"can't alloc sdesc\\n\");\n        return PTR_ERR(sdesc);\n    }\n\n    ret = crypto_shash_digest(&sdesc->shash, data, datalen, digest);\n    kfree(sdesc);\n    return ret;\n}\n\nstatic int ksu_sha256(const unsigned char *data, unsigned int datalen,\n                      unsigned char *digest)\n{\n    struct crypto_shash *alg;\n    char *hash_alg_name = \"sha256\";\n    int ret;\n\n    alg = crypto_alloc_shash(hash_alg_name, 0, 0);\n    if (IS_ERR(alg)) {\n        pr_info(\"can't alloc alg %s\\n\", hash_alg_name);\n        return PTR_ERR(alg);\n    }\n    ret = calc_hash(alg, data, datalen, digest);\n    crypto_free_shash(alg);\n    return ret;\n}\n\nstatic bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset,\n                        unsigned expected_size, const char *expected_sha256)\n{\n    kernel_read(fp, size4, 0x4, pos); // signer-sequence length\n    kernel_read(fp, size4, 0x4, pos); // signer length\n    kernel_read(fp, size4, 0x4, pos); // signed data length\n\n    *offset += 0x4 * 3;\n\n    kernel_read(fp, size4, 0x4, pos); // digests-sequence length\n\n    *pos += *size4;\n    *offset += 0x4 + *size4;\n\n    kernel_read(fp, size4, 0x4, pos); // certificates length\n    kernel_read(fp, size4, 0x4, pos); // certificate length\n    *offset += 0x4 * 2;\n\n    if (*size4 == expected_size) {\n        *offset += *size4;\n\n#define CERT_MAX_LENGTH 1024\n        char cert[CERT_MAX_LENGTH];\n        if (*size4 > CERT_MAX_LENGTH) {\n            pr_info(\"cert length overlimit\\n\");\n            return false;\n        }\n        kernel_read(fp, cert, *size4, pos);\n        unsigned char digest[SHA256_DIGEST_SIZE];\n        if (IS_ERR(ksu_sha256(cert, *size4, digest))) {\n            pr_info(\"sha256 error\\n\");\n            return false;\n        }\n\n        char hash_str[SHA256_DIGEST_SIZE * 2 + 1];\n        hash_str[SHA256_DIGEST_SIZE * 2] = '\\0';\n\n        bin2hex(hash_str, digest, SHA256_DIGEST_SIZE);\n        pr_info(\"sha256: %s, expected: %s\\n\", hash_str, expected_sha256);\n        if (strcmp(expected_sha256, hash_str) == 0) {\n            return true;\n        }\n    }\n    return false;\n}\n\nstruct zip_entry_header {\n    uint32_t signature;\n    uint16_t version;\n    uint16_t flags;\n    uint16_t compression;\n    uint16_t mod_time;\n    uint16_t mod_date;\n    uint32_t crc32;\n    uint32_t compressed_size;\n    uint32_t uncompressed_size;\n    uint16_t file_name_length;\n    uint16_t extra_field_length;\n} __attribute__((packed));\n\n// This is a necessary but not sufficient condition, but it is enough for us\nstatic bool has_v1_signature_file(struct file *fp)\n{\n    struct zip_entry_header header;\n    const char MANIFEST[] = \"META-INF/MANIFEST.MF\";\n\n    loff_t pos = 0;\n\n    while (kernel_read(fp, &header, sizeof(struct zip_entry_header), &pos) ==\n           sizeof(struct zip_entry_header)) {\n        if (header.signature != 0x04034b50) {\n            // ZIP magic: 'PK'\n            return false;\n        }\n        // Read the entry file name\n        if (header.file_name_length == sizeof(MANIFEST) - 1) {\n            char fileName[sizeof(MANIFEST)];\n            kernel_read(fp, fileName, header.file_name_length, &pos);\n            fileName[header.file_name_length] = '\\0';\n\n            // Check if the entry matches META-INF/MANIFEST.MF\n            if (strncmp(MANIFEST, fileName, sizeof(MANIFEST) - 1) == 0) {\n                return true;\n            }\n        } else {\n            // Skip the entry file name\n            pos += header.file_name_length;\n        }\n\n        // Skip to the next entry\n        pos += header.extra_field_length + header.compressed_size;\n    }\n\n    return false;\n}\n\nstatic __always_inline bool check_v2_signature(char *path,\n                                               unsigned expected_size,\n                                               const char *expected_sha256)\n{\n    unsigned char buffer[0x11] = { 0 };\n    u32 size4;\n    u64 size8, size_of_block;\n\n    loff_t pos;\n\n    bool v2_signing_valid = false;\n    int v2_signing_blocks = 0;\n    bool v3_signing_exist = false;\n    bool v3_1_signing_exist = false;\n\n    int i;\n    struct file *fp = filp_open(path, O_RDONLY, 0);\n    if (IS_ERR(fp)) {\n        pr_err(\"open %s error.\\n\", path);\n        return false;\n    }\n\n    // disable inotify for this file\n    fp->f_mode |= FMODE_NONOTIFY;\n\n    // https://en.wikipedia.org/wiki/Zip_(file_format)#End_of_central_directory_record_(EOCD)\n    for (i = 0;; ++i) {\n        unsigned short n;\n        pos = generic_file_llseek(fp, -i - 2, SEEK_END);\n        kernel_read(fp, &n, 2, &pos);\n        if (n == i) {\n            pos -= 22;\n            kernel_read(fp, &size4, 4, &pos);\n            if ((size4 ^ 0xcafebabeu) == 0xccfbf1eeu) {\n                break;\n            }\n        }\n        if (i == 0xffff) {\n            pr_info(\"error: cannot find eocd\\n\");\n            goto clean;\n        }\n    }\n\n    pos += 12;\n    // offset\n    kernel_read(fp, &size4, 0x4, &pos);\n    pos = size4 - 0x18;\n\n    kernel_read(fp, &size8, 0x8, &pos);\n    kernel_read(fp, buffer, 0x10, &pos);\n    if (strcmp((char *)buffer, \"APK Sig Block 42\")) {\n        goto clean;\n    }\n\n    pos = size4 - (size8 + 0x8);\n    kernel_read(fp, &size_of_block, 0x8, &pos);\n    if (size_of_block != size8) {\n        goto clean;\n    }\n\n    int loop_count = 0;\n    while (loop_count++ < 10) {\n        uint32_t id;\n        uint32_t offset;\n        kernel_read(fp, &size8, 0x8,\n                    &pos); // sequence length\n        if (size8 == size_of_block) {\n            break;\n        }\n        kernel_read(fp, &id, 0x4, &pos); // id\n        offset = 4;\n        if (id == 0x7109871au) {\n            v2_signing_blocks++;\n            v2_signing_valid = check_block(fp, &size4, &pos, &offset,\n                                           expected_size, expected_sha256);\n        } else if (id == 0xf05368c0u) {\n            // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73\n            v3_signing_exist = true;\n        } else if (id == 0x1b93ad61u) {\n            // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#74\n            v3_1_signing_exist = true;\n        } else {\n#ifdef CONFIG_KSU_DEBUG\n            pr_info(\"Unknown id: 0x%08x\\n\", id);\n#endif\n        }\n        pos += (size8 - offset);\n    }\n\n    if (v2_signing_blocks != 1) {\n#ifdef CONFIG_KSU_DEBUG\n        pr_err(\"Unexpected v2 signature count: %d\\n\", v2_signing_blocks);\n#endif\n        v2_signing_valid = false;\n    }\n\n    if (v2_signing_valid) {\n        int has_v1_signing = has_v1_signature_file(fp);\n        if (has_v1_signing) {\n            pr_err(\"Unexpected v1 signature scheme found!\\n\");\n            filp_close(fp, 0);\n            return false;\n        }\n    }\nclean:\n    filp_close(fp, 0);\n\n    if (v3_signing_exist || v3_1_signing_exist) {\n#ifdef CONFIG_KSU_DEBUG\n        pr_err(\"Unexpected v3 signature scheme found!\\n\");\n#endif\n        return false;\n    }\n\n    return v2_signing_valid;\n}\n\n#ifdef CONFIG_KSU_DEBUG\n\nint ksu_debug_manager_appid = -1;\n\n#include \"manager.h\"\n\nstatic int set_expected_size(const char *val, const struct kernel_param *kp)\n{\n    int rv = param_set_uint(val, kp);\n    ksu_set_manager_appid(ksu_debug_manager_appid);\n    pr_info(\"ksu_manager_appid set to %d\\n\", ksu_debug_manager_appid);\n    return rv;\n}\n\nstatic struct kernel_param_ops expected_size_ops = {\n    .set = set_expected_size,\n    .get = param_get_uint,\n};\n\nmodule_param_cb(ksu_debug_manager_appid, &expected_size_ops,\n                &ksu_debug_manager_appid, S_IRUSR | S_IWUSR);\n\n#endif\n\nint get_pkg_from_apk_path(char *pkg, const char *path)\n{\n    int len = strlen(path);\n    if (len >= KSU_MAX_PACKAGE_NAME || len < 1)\n        return -1;\n\n    const char *last_slash = NULL;\n    const char *second_last_slash = NULL;\n\n    int i;\n    for (i = len - 1; i >= 0; i--) {\n        if (path[i] == '/') {\n            if (!last_slash) {\n                last_slash = &path[i];\n            } else {\n                second_last_slash = &path[i];\n                break;\n            }\n        }\n    }\n\n    if (!last_slash || !second_last_slash)\n        return -1;\n\n    const char *last_hyphen = strchr(second_last_slash, '-');\n    if (!last_hyphen || last_hyphen > last_slash)\n        return -1;\n\n    int pkg_len = last_hyphen - second_last_slash - 1;\n    if (pkg_len >= KSU_MAX_PACKAGE_NAME || pkg_len <= 0)\n        return -1;\n\n    // Copying the package name\n    strncpy(pkg, second_last_slash + 1, pkg_len);\n    pkg[pkg_len] = '\\0';\n\n    return 0;\n}\n\nbool is_manager_apk(char *path)\n{\n#ifdef KSU_MANAGER_PACKAGE\n    char pkg[KSU_MAX_PACKAGE_NAME];\n    if (get_pkg_from_apk_path(pkg, path) < 0) {\n        pr_err(\"Failed to get package name from apk path: %s\\n\", path);\n        return false;\n    }\n\n    // pkg is `<real package>`\n    if (strncmp(pkg, KSU_MANAGER_PACKAGE, sizeof(KSU_MANAGER_PACKAGE))) {\n        return false;\n    }\n#endif\n    if (check_v2_signature(path, EXPECTED_SIZE, EXPECTED_HASH)) {\n        return true;\n    }\n#ifdef EXPECTED_SIZE2\n    return check_v2_signature(path, EXPECTED_SIZE2, EXPECTED_HASH2);\n#else\n    return false;\n#endif\n}\n"
  },
  {
    "path": "kernel/apk_sign.h",
    "content": "#ifndef __KSU_H_APK_V2_SIGN\n#define __KSU_H_APK_V2_SIGN\n\n#include <linux/types.h>\n\nbool is_manager_apk(char *path);\nint get_pkg_from_apk_path(char *pkg, const char *path);\n\n#endif\n"
  },
  {
    "path": "kernel/app_profile.c",
    "content": "#include <linux/capability.h>\n#include <linux/cred.h>\n#include <linux/sched.h>\n#include <linux/sched/user.h>\n#include <linux/sched/signal.h>\n#include <linux/seccomp.h>\n#include <linux/slab.h>\n#include <linux/thread_info.h>\n#include <linux/uidgid.h>\n#include <linux/version.h>\n\n#include \"allowlist.h\"\n#include \"app_profile.h\"\n#include \"klog.h\" // IWYU pragma: keep\n#include \"selinux/selinux.h\"\n#include \"su_mount_ns.h\"\n#include \"syscall_hook_manager.h\"\n\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 7, 0)\nstatic struct group_info root_groups = { .usage = REFCOUNT_INIT(2) };\n#else\nstatic struct group_info root_groups = { .usage = ATOMIC_INIT(2) };\n#endif\n\nvoid setup_groups(struct root_profile *profile, struct cred *cred)\n{\n    if (profile->groups_count > KSU_MAX_GROUPS) {\n        pr_warn(\"Failed to setgroups, too large group: %d!\\n\", profile->uid);\n        return;\n    }\n\n    if (profile->groups_count == 1 && profile->groups[0] == 0) {\n        // setgroup to root and return early.\n        if (cred->group_info)\n            put_group_info(cred->group_info);\n        cred->group_info = get_group_info(&root_groups);\n        return;\n    }\n\n    u32 ngroups = profile->groups_count;\n    struct group_info *group_info = groups_alloc(ngroups);\n    if (!group_info) {\n        pr_warn(\"Failed to setgroups, ENOMEM for: %d\\n\", profile->uid);\n        return;\n    }\n\n    int i;\n    for (i = 0; i < ngroups; i++) {\n        gid_t gid = profile->groups[i];\n        kgid_t kgid = make_kgid(current_user_ns(), gid);\n        if (!gid_valid(kgid)) {\n            pr_warn(\"Failed to setgroups, invalid gid: %d\\n\", gid);\n            put_group_info(group_info);\n            return;\n        }\n        group_info->gid[i] = kgid;\n    }\n\n    groups_sort(group_info);\n    set_groups(cred, group_info);\n    put_group_info(group_info);\n}\n\nvoid seccomp_filter_release(struct task_struct *tsk);\n\nstatic void disable_seccomp(void)\n{\n    struct task_struct *fake;\n\n    fake = kmalloc(sizeof(*fake), GFP_ATOMIC);\n    if (!fake) {\n        pr_warn(\"failed to alloc fake task_struct\\n\");\n        return;\n    }\n\n    // Refer to kernel/seccomp.c: seccomp_set_mode_strict\n    // When disabling Seccomp, ensure that current->sighand->siglock is held during the operation.\n    spin_lock_irq(&current->sighand->siglock);\n    // disable seccomp\n#if defined(CONFIG_GENERIC_ENTRY) &&                                           \\\n    LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)\n    clear_syscall_work(SECCOMP);\n#else\n    clear_thread_flag(TIF_SECCOMP);\n#endif\n\n    memcpy(fake, current, sizeof(*fake));\n\n    current->seccomp.mode = 0;\n    current->seccomp.filter = NULL;\n    atomic_set(&current->seccomp.filter_count, 0);\n    spin_unlock_irq(&current->sighand->siglock);\n\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 11, 0)\n    // https://github.com/torvalds/linux/commit/bfafe5efa9754ebc991750da0bcca2a6694f3ed3#diff-45eb79a57536d8eccfc1436932f093eb5c0b60d9361c39edb46581ad313e8987R576-R577\n    fake->flags |= PF_EXITING;\n#elif LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)\n    // https://github.com/torvalds/linux/commit/0d8315dddd2899f519fe1ca3d4d5cdaf44ea421e#diff-45eb79a57536d8eccfc1436932f093eb5c0b60d9361c39edb46581ad313e8987R556-R558\n    fake->sighand = NULL;\n#endif\n\n    seccomp_filter_release(fake);\n    kfree(fake);\n}\n\nvoid escape_with_root_profile(void)\n{\n    struct cred *cred;\n    struct task_struct *p = current;\n    struct task_struct *t;\n    struct root_profile profile;\n    struct user_struct *new_user;\n\n    cred = prepare_creds();\n    if (!cred) {\n        pr_warn(\"prepare_creds failed!\\n\");\n        return;\n    }\n\n    if (cred->euid.val == 0) {\n        pr_warn(\"Already root, don't escape!\\n\");\n        goto out_abort_creds;\n    }\n\n    ksu_get_root_profile(cred->uid.val, &profile);\n\n    cred->uid.val = profile.uid;\n    cred->suid.val = profile.uid;\n    cred->euid.val = profile.uid;\n    cred->fsuid.val = profile.uid;\n\n    cred->gid.val = profile.gid;\n    cred->fsgid.val = profile.gid;\n    cred->sgid.val = profile.gid;\n    cred->egid.val = profile.gid;\n    cred->securebits = 0;\n\n    BUILD_BUG_ON(sizeof(profile.capabilities.effective) !=\n                 sizeof(kernel_cap_t));\n\n    /*\n     * Mirror the kernel set*uid path: update cred->user first, then\n     * cred->ucounts, before commit_creds(). commit_creds() moves\n     * RLIMIT_NPROC accounting based on cred->user; if uid changes while\n     * user/ucounts stay stale, the old charge can remain pinned to the\n     * previous UID.\n     * See kernel/sys.c:set_user() and kernel/cred.c:set_cred_ucounts() /\n     * commit_creds():\n     * https://github.com/torvalds/linux/blob/v5.14/kernel/sys.c\n     * https://github.com/torvalds/linux/blob/v5.14/kernel/cred.c\n     */\n    new_user = alloc_uid(cred->uid);\n    if (!new_user) {\n        goto out_abort_creds;\n    }\n\n    free_uid(cred->user);\n    cred->user = new_user;\n\n    // v5.14+ added cred->ucounts, so we must refresh it after changing uid/user:\n    // https://github.com/torvalds/linux/commit/905ae01c4ae2ae3df05bb141801b1db4b7d83c61#diff-ff6060da281bd9ef3f24e17b77a9b0b5b2ed2d7208bb69b29107bee69732bd31\n    // on older kernels, per-UID process accounting lives in user_struct.\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 14, 0)\n    if (set_cred_ucounts(cred)) {\n        goto out_abort_creds;\n    }\n#endif\n\n    // setup capabilities\n    // we need CAP_DAC_READ_SEARCH becuase `/data/adb/ksud` is not accessible for non root process\n    // we add it here but don't add it to cap_inhertiable, it would be dropped automaticly after exec!\n    u64 cap_for_ksud = profile.capabilities.effective | CAP_DAC_READ_SEARCH;\n    memcpy(&cred->cap_effective, &cap_for_ksud, sizeof(cred->cap_effective));\n    memcpy(&cred->cap_permitted, &profile.capabilities.effective,\n           sizeof(cred->cap_permitted));\n    memcpy(&cred->cap_bset, &profile.capabilities.effective,\n           sizeof(cred->cap_bset));\n\n    setup_groups(&profile, cred);\n    setup_selinux(profile.selinux_domain, cred);\n\n    commit_creds(cred);\n\n    disable_seccomp();\n\n    for_each_thread (p, t) {\n        ksu_set_task_tracepoint_flag(t);\n    }\n\n    setup_mount_ns(profile.namespaces);\n    return;\n\nout_abort_creds:\n    abort_creds(cred);\n}\n\nvoid escape_to_root_for_init(void)\n{\n    struct cred *cred = prepare_creds();\n    if (!cred) {\n        pr_err(\"Failed to prepare init's creds!\\n\");\n        return;\n    }\n\n    setup_selinux(KERNEL_SU_CONTEXT, cred);\n    commit_creds(cred);\n}\n"
  },
  {
    "path": "kernel/app_profile.h",
    "content": "#ifndef __KSU_H_APP_PROFILE\n#define __KSU_H_APP_PROFILE\n\n#include <linux/types.h>\n\n// Forward declarations\nstruct cred;\n\n#define KSU_APP_PROFILE_VER 2\n#define KSU_MAX_PACKAGE_NAME 256\n// NGROUPS_MAX for Linux is 65535 generally, but we only supports 32 groups.\n#define KSU_MAX_GROUPS 32\n#define KSU_SELINUX_DOMAIN 64\n\nstruct root_profile {\n    int32_t uid;\n    int32_t gid;\n\n    int32_t groups_count;\n    int32_t groups[KSU_MAX_GROUPS];\n\n    // kernel_cap_t is u32[2] for capabilities v3\n    struct {\n        u64 effective;\n        u64 permitted;\n        u64 inheritable;\n    } capabilities;\n\n    char selinux_domain[KSU_SELINUX_DOMAIN];\n\n    int32_t namespaces;\n};\n\nstruct non_root_profile {\n    bool umount_modules;\n};\n\nstruct app_profile {\n    // It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this.\n    u32 version;\n\n    // this is usually the package of the app, but can be other value for special apps\n    char key[KSU_MAX_PACKAGE_NAME];\n    int32_t current_uid;\n    bool allow_su;\n\n    union {\n        struct {\n            bool use_default;\n            char template_name[KSU_MAX_PACKAGE_NAME];\n\n            struct root_profile profile;\n        } rp_config;\n\n        struct {\n            bool use_default;\n\n            struct non_root_profile profile;\n        } nrp_config;\n    };\n};\n\n// Escalate current process to root with the appropriate profile\nvoid escape_with_root_profile(void);\n\nvoid escape_to_root_for_init(void);\n\n#endif\n"
  },
  {
    "path": "kernel/arch.h",
    "content": "#ifndef __KSU_H_ARCH\n#define __KSU_H_ARCH\n\n#include <linux/version.h>\n\n#if defined(__aarch64__)\n\n#define __PT_PARM1_REG regs[0]\n#define __PT_PARM2_REG regs[1]\n#define __PT_PARM3_REG regs[2]\n#define __PT_SYSCALL_PARM4_REG regs[3]\n#define __PT_CCALL_PARM4_REG regs[3]\n#define __PT_PARM5_REG regs[4]\n#define __PT_PARM6_REG regs[5]\n#define __PT_RET_REG regs[30]\n#define __PT_FP_REG regs[29] /* Works only with CONFIG_FRAME_POINTER */\n#define __PT_RC_REG regs[0]\n#define __PT_SP_REG sp\n#define __PT_IP_REG pc\n\n#define REBOOT_SYMBOL \"__arm64_sys_reboot\"\n#define SYS_READ_SYMBOL \"__arm64_sys_read\"\n#define SYS_EXECVE_SYMBOL \"__arm64_sys_execve\"\n// https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/scripts/syscalltbl.sh;l=57;drc=9142be9e6443fd641ca37f820efe00d9cd890eb1\n// https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/scripts/syscall.tbl;l=104;drc=b36d4b6aa88ef039647228b98c59a875e92f8c8e\n#define SYS_FSTAT_SYMBOL \"__arm64_sys_newfstat\"\n\n#elif defined(__x86_64__)\n\n#define __PT_PARM1_REG di\n#define __PT_PARM2_REG si\n#define __PT_PARM3_REG dx\n/* syscall uses r10 for PARM4 */\n#define __PT_SYSCALL_PARM4_REG r10\n#define __PT_CCALL_PARM4_REG cx\n#define __PT_PARM5_REG r8\n#define __PT_PARM6_REG r9\n#define __PT_RET_REG sp\n#define __PT_FP_REG bp\n#define __PT_RC_REG ax\n#define __PT_SP_REG sp\n#define __PT_IP_REG ip\n#define REBOOT_SYMBOL \"__x64_sys_reboot\"\n#define SYS_READ_SYMBOL \"__x64_sys_read\"\n#define SYS_EXECVE_SYMBOL \"__x64_sys_execve\"\n#define SYS_FSTAT_SYMBOL \"__x64_sys_newfstat\"\n\n#else\n#error \"Unsupported arch\"\n#endif\n\n/* allow some architecutres to override `struct pt_regs` */\n#ifndef __PT_REGS_CAST\n#define __PT_REGS_CAST(x) (x)\n#endif\n\n#define PT_REGS_PARM1(x) (__PT_REGS_CAST(x)->__PT_PARM1_REG)\n#define PT_REGS_PARM2(x) (__PT_REGS_CAST(x)->__PT_PARM2_REG)\n#define PT_REGS_PARM3(x) (__PT_REGS_CAST(x)->__PT_PARM3_REG)\n#define PT_REGS_SYSCALL_PARM4(x) (__PT_REGS_CAST(x)->__PT_SYSCALL_PARM4_REG)\n#define PT_REGS_CCALL_PARM4(x) (__PT_REGS_CAST(x)->__PT_CCALL_PARM4_REG)\n#define PT_REGS_PARM5(x) (__PT_REGS_CAST(x)->__PT_PARM5_REG)\n#define PT_REGS_PARM6(x) (__PT_REGS_CAST(x)->__PT_PARM6_REG)\n#define PT_REGS_RET(x) (__PT_REGS_CAST(x)->__PT_RET_REG)\n#define PT_REGS_FP(x) (__PT_REGS_CAST(x)->__PT_FP_REG)\n#define PT_REGS_RC(x) (__PT_REGS_CAST(x)->__PT_RC_REG)\n#define PT_REGS_SP(x) (__PT_REGS_CAST(x)->__PT_SP_REG)\n#define PT_REGS_IP(x) (__PT_REGS_CAST(x)->__PT_IP_REG)\n\n#define PT_REAL_REGS(regs) ((struct pt_regs *)PT_REGS_PARM1(regs))\n\n#endif\n"
  },
  {
    "path": "kernel/feature.c",
    "content": "#include \"feature.h\"\n#include \"klog.h\" // IWYU pragma: keep\n\n#include <linux/mutex.h>\n\nstatic const struct ksu_feature_handler *feature_handlers[KSU_FEATURE_MAX];\n\nstatic DEFINE_MUTEX(feature_mutex);\n\nint ksu_register_feature_handler(const struct ksu_feature_handler *handler)\n{\n    if (!handler) {\n        pr_err(\"feature: register handler is NULL\\n\");\n        return -EINVAL;\n    }\n\n    if (handler->feature_id >= KSU_FEATURE_MAX) {\n        pr_err(\"feature: invalid feature_id %u\\n\", handler->feature_id);\n        return -EINVAL;\n    }\n\n    if (!handler->get_handler && !handler->set_handler) {\n        pr_err(\"feature: no handler provided for feature %u\\n\",\n               handler->feature_id);\n        return -EINVAL;\n    }\n\n    mutex_lock(&feature_mutex);\n\n    if (feature_handlers[handler->feature_id]) {\n        pr_warn(\"feature: handler for %u already registered, overwriting\\n\",\n                handler->feature_id);\n    }\n\n    feature_handlers[handler->feature_id] = handler;\n\n    pr_info(\"feature: registered handler for %s (id=%u)\\n\",\n            handler->name ? handler->name : \"unknown\", handler->feature_id);\n\n    mutex_unlock(&feature_mutex);\n    return 0;\n}\n\nint ksu_unregister_feature_handler(u32 feature_id)\n{\n    int ret = 0;\n\n    if (feature_id >= KSU_FEATURE_MAX) {\n        pr_err(\"feature: invalid feature_id %u\\n\", feature_id);\n        return -EINVAL;\n    }\n\n    mutex_lock(&feature_mutex);\n\n    if (!feature_handlers[feature_id]) {\n        pr_warn(\"feature: no handler registered for %u\\n\", feature_id);\n        ret = -ENOENT;\n        goto out;\n    }\n\n    feature_handlers[feature_id] = NULL;\n\n    pr_info(\"feature: unregistered handler for id=%u\\n\", feature_id);\n\nout:\n    mutex_unlock(&feature_mutex);\n    return ret;\n}\n\nint ksu_get_feature(u32 feature_id, u64 *value, bool *supported)\n{\n    int ret = 0;\n    const struct ksu_feature_handler *handler;\n\n    if (feature_id >= KSU_FEATURE_MAX) {\n        pr_err(\"feature: invalid feature_id %u\\n\", feature_id);\n        return -EINVAL;\n    }\n\n    if (!value || !supported) {\n        pr_err(\"feature: invalid parameters\\n\");\n        return -EINVAL;\n    }\n\n    mutex_lock(&feature_mutex);\n\n    handler = feature_handlers[feature_id];\n\n    if (!handler) {\n        *supported = false;\n        *value = 0;\n        pr_debug(\"feature: feature %u not supported\\n\", feature_id);\n        goto out;\n    }\n\n    *supported = true;\n\n    if (!handler->get_handler) {\n        pr_warn(\"feature: no get_handler for feature %u\\n\", feature_id);\n        ret = -EOPNOTSUPP;\n        goto out;\n    }\n\n    ret = handler->get_handler(value);\n    if (ret) {\n        pr_err(\"feature: get_handler for %u failed: %d\\n\", feature_id, ret);\n    }\n\nout:\n    mutex_unlock(&feature_mutex);\n    return ret;\n}\n\nint ksu_set_feature(u32 feature_id, u64 value)\n{\n    int ret = 0;\n    const struct ksu_feature_handler *handler;\n\n    if (feature_id >= KSU_FEATURE_MAX) {\n        pr_err(\"feature: invalid feature_id %u\\n\", feature_id);\n        return -EINVAL;\n    }\n\n    mutex_lock(&feature_mutex);\n\n    handler = feature_handlers[feature_id];\n\n    if (!handler) {\n        pr_err(\"feature: feature %u not registered\\n\", feature_id);\n        ret = -EOPNOTSUPP;\n        goto out;\n    }\n\n    if (!handler->set_handler) {\n        pr_warn(\"feature: no set_handler for feature %u\\n\", feature_id);\n        ret = -EOPNOTSUPP;\n        goto out;\n    }\n\n    ret = handler->set_handler(value);\n    if (ret) {\n        pr_err(\"feature: set_handler for %u failed: %d\\n\", feature_id, ret);\n    }\n\nout:\n    mutex_unlock(&feature_mutex);\n    return ret;\n}\n\nvoid ksu_feature_init(void)\n{\n    int i;\n\n    for (i = 0; i < KSU_FEATURE_MAX; i++) {\n        feature_handlers[i] = NULL;\n    }\n\n    pr_info(\"feature: feature management initialized\\n\");\n}\n\nvoid ksu_feature_exit(void)\n{\n    int i;\n\n    mutex_lock(&feature_mutex);\n\n    for (i = 0; i < KSU_FEATURE_MAX; i++) {\n        feature_handlers[i] = NULL;\n    }\n\n    mutex_unlock(&feature_mutex);\n\n    pr_info(\"feature: feature management cleaned up\\n\");\n}\n"
  },
  {
    "path": "kernel/feature.h",
    "content": "#ifndef __KSU_H_FEATURE\n#define __KSU_H_FEATURE\n\n#include <linux/types.h>\n\nenum ksu_feature_id {\n    KSU_FEATURE_SU_COMPAT = 0,\n    KSU_FEATURE_KERNEL_UMOUNT = 1,\n\n    KSU_FEATURE_MAX\n};\n\ntypedef int (*ksu_feature_get_t)(u64 *value);\ntypedef int (*ksu_feature_set_t)(u64 value);\n\nstruct ksu_feature_handler {\n    u32 feature_id;\n    const char *name;\n    ksu_feature_get_t get_handler;\n    ksu_feature_set_t set_handler;\n};\n\nint ksu_register_feature_handler(const struct ksu_feature_handler *handler);\n\nint ksu_unregister_feature_handler(u32 feature_id);\n\nint ksu_get_feature(u32 feature_id, u64 *value, bool *supported);\n\nint ksu_set_feature(u32 feature_id, u64 value);\n\nvoid ksu_feature_init(void);\n\nvoid ksu_feature_exit(void);\n\n#endif // __KSU_H_FEATURE\n"
  },
  {
    "path": "kernel/file_wrapper.c",
    "content": "#include <linux/gfp.h>\n#include <linux/fdtable.h>\n#include <linux/export.h>\n#include <linux/anon_inodes.h>\n#include <linux/capability.h>\n#include <linux/cred.h>\n#include <linux/err.h>\n#include <linux/file.h>\n#include <linux/fs.h>\n#include <linux/seq_file.h>\n#include <linux/slab.h>\n#include <linux/uaccess.h>\n#include <linux/version.h>\n#include <linux/mount.h>\n\n#include \"objsec.h\"\n\n#include \"klog.h\" // IWYU pragma: keep\n#include \"selinux/selinux.h\"\n#include \"ksud.h\"\n\n#include \"file_wrapper.h\"\n\nstruct ksu_file_wrapper {\n    struct file *orig;\n    struct file_operations ops;\n};\n\nstatic struct ksu_file_wrapper *ksu_create_file_wrapper(struct file *fp);\n\nstatic int ksu_wrapper_open(struct inode *ino, struct file *fp)\n{\n    struct path *orig_path = fp->f_path.dentry->d_fsdata;\n    struct file *orig_file =\n        dentry_open(orig_path, fp->f_flags, current_cred());\n    if (IS_ERR(orig_file)) {\n        return PTR_ERR(orig_file);\n    }\n    struct ksu_file_wrapper *wrapper = ksu_create_file_wrapper(orig_file);\n    if (IS_ERR(wrapper)) {\n        filp_close(orig_file, current->files);\n        return PTR_ERR(wrapper);\n    }\n    fp->private_data = wrapper;\n    const struct file_operations *new_fops = fops_get(&wrapper->ops);\n    replace_fops(fp, new_fops);\n    return 0;\n}\n\nstatic const struct file_operations ksu_file_wrapper_inode_fops = {\n    .owner = THIS_MODULE,\n    .open = ksu_wrapper_open\n};\n\nstatic loff_t ksu_wrapper_llseek(struct file *fp, loff_t off, int flags)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->llseek(data->orig, off, flags);\n}\n\nstatic ssize_t ksu_wrapper_read(struct file *fp, char __user *ptr, size_t sz,\n                                loff_t *off)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->read(orig, ptr, sz, off);\n}\n\nstatic ssize_t ksu_wrapper_write(struct file *fp, const char __user *ptr,\n                                 size_t sz, loff_t *off)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->write(orig, ptr, sz, off);\n}\n\nstatic ssize_t ksu_wrapper_read_iter(struct kiocb *iocb, struct iov_iter *iovi)\n{\n    struct ksu_file_wrapper *data = iocb->ki_filp->private_data;\n    struct file *orig = data->orig;\n    iocb->ki_filp = orig;\n    return orig->f_op->read_iter(iocb, iovi);\n}\n\nstatic ssize_t ksu_wrapper_write_iter(struct kiocb *iocb, struct iov_iter *iovi)\n{\n    struct ksu_file_wrapper *data = iocb->ki_filp->private_data;\n    struct file *orig = data->orig;\n    iocb->ki_filp = orig;\n    return orig->f_op->write_iter(iocb, iovi);\n}\n\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 1, 0)\nstatic int ksu_wrapper_iopoll(struct kiocb *kiocb, struct io_comp_batch *icb,\n                              unsigned int v)\n{\n    struct ksu_file_wrapper *data = kiocb->ki_filp->private_data;\n    struct file *orig = data->orig;\n    kiocb->ki_filp = orig;\n    return orig->f_op->iopoll(kiocb, icb, v);\n}\n#else\nstatic int ksu_wrapper_iopoll(struct kiocb *kiocb, bool spin)\n{\n    struct ksu_file_wrapper *data = kiocb->ki_filp->private_data;\n    struct file *orig = data->orig;\n    kiocb->ki_filp = orig;\n    return orig->f_op->iopoll(kiocb, spin);\n}\n#endif\n\n#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0)\nstatic int ksu_wrapper_iterate(struct file *fp, struct dir_context *dc)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->iterate(orig, dc);\n}\n#endif\n\nstatic int ksu_wrapper_iterate_shared(struct file *fp, struct dir_context *dc)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->iterate_shared(orig, dc);\n}\n\nstatic __poll_t ksu_wrapper_poll(struct file *fp, struct poll_table_struct *pts)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->poll(orig, pts);\n}\n\nstatic long ksu_wrapper_unlocked_ioctl(struct file *fp, unsigned int cmd,\n                                       unsigned long arg)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->unlocked_ioctl(orig, cmd, arg);\n}\n\nstatic long ksu_wrapper_compat_ioctl(struct file *fp, unsigned int cmd,\n                                     unsigned long arg)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->compat_ioctl(orig, cmd, arg);\n}\n\nstatic int ksu_wrapper_mmap(struct file *fp, struct vm_area_struct *vma)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->mmap(orig, vma);\n}\n\nstatic int ksu_wrapper_flush(struct file *fp, fl_owner_t id)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->flush(orig, id);\n}\n\nstatic int ksu_wrapper_fsync(struct file *fp, loff_t off1, loff_t off2,\n                             int datasync)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->fsync(orig, off1, off2, datasync);\n}\n\nstatic int ksu_wrapper_fasync(int arg, struct file *fp, int arg2)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->fasync(arg, orig, arg2);\n}\n\nstatic int ksu_wrapper_lock(struct file *fp, int arg1, struct file_lock *fl)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->lock(orig, arg1, fl);\n}\n\n#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0)\nstatic ssize_t ksu_wrapper_sendpage(struct file *fp, struct page *pg, int arg1,\n                                    size_t sz, loff_t *off, int arg2)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->sendpage) {\n        return orig->f_op->sendpage(orig, pg, arg1, sz, off, arg2);\n    }\n    return -EINVAL;\n}\n#endif\n\nstatic unsigned long ksu_wrapper_get_unmapped_area(struct file *fp,\n                                                   unsigned long arg1,\n                                                   unsigned long arg2,\n                                                   unsigned long arg3,\n                                                   unsigned long arg4)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->get_unmapped_area) {\n        return orig->f_op->get_unmapped_area(orig, arg1, arg2, arg3, arg4);\n    }\n    return -EINVAL;\n}\n\n// static int ksu_wrapper_check_flags(int arg) {}\n\nstatic int ksu_wrapper_flock(struct file *fp, int arg1, struct file_lock *fl)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->flock) {\n        return orig->f_op->flock(orig, arg1, fl);\n    }\n    return -EINVAL;\n}\n\nstatic ssize_t ksu_wrapper_splice_write(struct pipe_inode_info *pii,\n                                        struct file *fp, loff_t *off, size_t sz,\n                                        unsigned int arg1)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->splice_write) {\n        return orig->f_op->splice_write(pii, orig, off, sz, arg1);\n    }\n    return -EINVAL;\n}\n\nstatic ssize_t ksu_wrapper_splice_read(struct file *fp, loff_t *off,\n                                       struct pipe_inode_info *pii, size_t sz,\n                                       unsigned int arg1)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->splice_read) {\n        return orig->f_op->splice_read(orig, off, pii, sz, arg1);\n    }\n    return -EINVAL;\n}\n\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0)\nvoid ksu_wrapper_splice_eof(struct file *fp)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->splice_eof) {\n        return orig->f_op->splice_eof(orig);\n    }\n}\n#endif\n\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0)\nstatic int ksu_wrapper_setlease(struct file *fp, int arg1,\n                                struct file_lease **fl, void **p)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->setlease) {\n        return orig->f_op->setlease(orig, arg1, fl, p);\n    }\n    return -EINVAL;\n}\n#elif LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0)\nstatic int ksu_wrapper_setlease(struct file *fp, int arg1,\n                                struct file_lock **fl, void **p)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->setlease) {\n        return orig->f_op->setlease(orig, arg1, fl, p);\n    }\n    return -EINVAL;\n}\n#else\nstatic int ksu_wrapper_setlease(struct file *fp, long arg1,\n                                struct file_lock **fl, void **p)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->setlease) {\n        return orig->f_op->setlease(orig, arg1, fl, p);\n    }\n    return -EINVAL;\n}\n#endif\n\nstatic long ksu_wrapper_fallocate(struct file *fp, int mode, loff_t offset,\n                                  loff_t len)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->fallocate) {\n        return orig->f_op->fallocate(orig, mode, offset, len);\n    }\n    return -EINVAL;\n}\n\nstatic void ksu_wrapper_show_fdinfo(struct seq_file *m, struct file *f)\n{\n    struct ksu_file_wrapper *data = f->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->show_fdinfo) {\n        orig->f_op->show_fdinfo(m, orig);\n    }\n}\n\n// https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/read_write.c;l=1593-1606;drc=398da7defe218d3e51b0f3bdff75147e28125b60\nstatic ssize_t ksu_wrapper_copy_file_range(struct file *file_in, loff_t pos_in,\n                                           struct file *file_out,\n                                           loff_t pos_out, size_t len,\n                                           unsigned int flags)\n{\n    struct ksu_file_wrapper *data = file_out->private_data;\n    struct file *orig = data->orig;\n    return orig->f_op->copy_file_range(file_in, pos_in, orig, pos_out, len,\n                                       flags);\n}\n\n// no REMAP_FILE_DEDUP: use file_in\n// https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/read_write.c;l=1598-1599;drc=398da7defe218d3e51b0f3bdff75147e28125b60\n// https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/remap_range.c;l=403-404;drc=398da7defe218d3e51b0f3bdff75147e28125b60\n// REMAP_FILE_DEDUP: use file_out\n// https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/remap_range.c;l=483-484;drc=398da7defe218d3e51b0f3bdff75147e28125b60\nstatic loff_t ksu_wrapper_remap_file_range(struct file *file_in, loff_t pos_in,\n                                           struct file *file_out,\n                                           loff_t pos_out, loff_t len,\n                                           unsigned int remap_flags)\n{\n    if (remap_flags & REMAP_FILE_DEDUP) {\n        struct ksu_file_wrapper *data = file_out->private_data;\n        struct file *orig = data->orig;\n        return orig->f_op->remap_file_range(file_in, pos_in, orig, pos_out, len,\n                                            remap_flags);\n    } else {\n        struct ksu_file_wrapper *data = file_in->private_data;\n        struct file *orig = data->orig;\n        return orig->f_op->remap_file_range(orig, pos_in, file_out, pos_out,\n                                            len, remap_flags);\n    }\n}\n\nstatic int ksu_wrapper_fadvise(struct file *fp, loff_t off1, loff_t off2,\n                               int flags)\n{\n    struct ksu_file_wrapper *data = fp->private_data;\n    struct file *orig = data->orig;\n    if (orig->f_op->fadvise) {\n        return orig->f_op->fadvise(orig, off1, off2, flags);\n    }\n    return -EINVAL;\n}\n\nstatic void ksu_release_file_wrapper(struct ksu_file_wrapper *data);\n\nstatic int ksu_wrapper_release(struct inode *inode, struct file *filp)\n{\n    // https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/file_table.c;l=467-473;drc=3be0b283b562eabbc2b1f3bb534dc8903079bbaa\n    // f_op->release is called before fops_put(f_op), so we put it manually.\n    fops_put(filp->f_op);\n    // prevent it from being put again\n    filp->f_op = NULL;\n    ksu_release_file_wrapper(filp->private_data);\n    return 0;\n}\n\nstatic struct ksu_file_wrapper *ksu_create_file_wrapper(struct file *fp)\n{\n    struct ksu_file_wrapper *p =\n        kcalloc(1, sizeof(struct ksu_file_wrapper), GFP_KERNEL);\n    if (!p) {\n        return ERR_PTR(-ENOMEM);\n    }\n\n    get_file(fp);\n\n    p->orig = fp;\n    p->ops.owner = THIS_MODULE;\n    p->ops.llseek = fp->f_op->llseek ? ksu_wrapper_llseek : NULL;\n    p->ops.read = fp->f_op->read ? ksu_wrapper_read : NULL;\n    p->ops.write = fp->f_op->write ? ksu_wrapper_write : NULL;\n    p->ops.read_iter = fp->f_op->read_iter ? ksu_wrapper_read_iter : NULL;\n    p->ops.write_iter = fp->f_op->write_iter ? ksu_wrapper_write_iter : NULL;\n    p->ops.iopoll = fp->f_op->iopoll ? ksu_wrapper_iopoll : NULL;\n#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0)\n    p->ops.iterate = fp->f_op->iterate ? ksu_wrapper_iterate : NULL;\n#endif\n    p->ops.iterate_shared =\n        fp->f_op->iterate_shared ? ksu_wrapper_iterate_shared : NULL;\n    p->ops.poll = fp->f_op->poll ? ksu_wrapper_poll : NULL;\n    p->ops.unlocked_ioctl =\n        fp->f_op->unlocked_ioctl ? ksu_wrapper_unlocked_ioctl : NULL;\n    p->ops.compat_ioctl =\n        fp->f_op->compat_ioctl ? ksu_wrapper_compat_ioctl : NULL;\n    p->ops.mmap = fp->f_op->mmap ? ksu_wrapper_mmap : NULL;\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0)\n    p->ops.fop_flags = fp->f_op->fop_flags;\n#else\n    p->ops.mmap_supported_flags = fp->f_op->mmap_supported_flags;\n#endif\n    p->ops.flush = fp->f_op->flush ? ksu_wrapper_flush : NULL;\n    p->ops.release = ksu_wrapper_release;\n    p->ops.fsync = fp->f_op->fsync ? ksu_wrapper_fsync : NULL;\n    p->ops.fasync = fp->f_op->fasync ? ksu_wrapper_fasync : NULL;\n    p->ops.lock = fp->f_op->lock ? ksu_wrapper_lock : NULL;\n#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0)\n    p->ops.sendpage = fp->f_op->sendpage ? ksu_wrapper_sendpage : NULL;\n#endif\n    p->ops.get_unmapped_area =\n        fp->f_op->get_unmapped_area ? ksu_wrapper_get_unmapped_area : NULL;\n    p->ops.check_flags = fp->f_op->check_flags;\n    p->ops.flock = fp->f_op->flock ? ksu_wrapper_flock : NULL;\n    p->ops.splice_write =\n        fp->f_op->splice_write ? ksu_wrapper_splice_write : NULL;\n    p->ops.splice_read = fp->f_op->splice_read ? ksu_wrapper_splice_read : NULL;\n    p->ops.setlease = fp->f_op->setlease ? ksu_wrapper_setlease : NULL;\n    p->ops.fallocate = fp->f_op->fallocate ? ksu_wrapper_fallocate : NULL;\n    p->ops.show_fdinfo = fp->f_op->show_fdinfo ? ksu_wrapper_show_fdinfo : NULL;\n    p->ops.copy_file_range =\n        fp->f_op->copy_file_range ? ksu_wrapper_copy_file_range : NULL;\n    p->ops.remap_file_range =\n        fp->f_op->remap_file_range ? ksu_wrapper_remap_file_range : NULL;\n    p->ops.fadvise = fp->f_op->fadvise ? ksu_wrapper_fadvise : NULL;\n\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0)\n    p->ops.splice_eof = fp->f_op->splice_eof ? ksu_wrapper_splice_eof : NULL;\n#endif\n\n    return p;\n}\n\nstatic void ksu_release_file_wrapper(struct ksu_file_wrapper *data)\n{\n    fput((struct file *)data->orig);\n    kfree(data);\n}\n\nstatic char *ksu_wrapper_d_dname(struct dentry *dentry, char *buffer,\n                                 int buflen)\n{\n    struct path *orig_path = dentry->d_fsdata;\n    return d_path(orig_path, buffer, buflen);\n}\n\nstatic void ksu_wrapper_d_release(struct dentry *dentry)\n{\n    struct path *orig_path = dentry->d_fsdata;\n    path_put(orig_path);\n    kfree(orig_path);\n}\n\nstatic const struct dentry_operations ksu_file_wrapper_d_ops = {\n    .d_dname = ksu_wrapper_d_dname,\n    .d_release = ksu_wrapper_d_release\n};\n\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 8, 0)\n#define ksu_anon_inode_create_getfile_compat anon_inode_create_getfile\n#elif LINUX_VERSION_CODE >= KERNEL_VERSION(5, 16, 0)\n#define ksu_anon_inode_create_getfile_compat anon_inode_getfile_secure\n#else\n// There is no anon_inode_create_getfile before 5.16, but it's not difficult to implement it.\n// https://cs.android.com/android/kernel/superproject/+/common-android12-5.10:common/fs/anon_inodes.c;l=58-125;drc=0d34ce8aa78e38affbb501690bcabec4df88620e\n\n// Borrow kernel's anon_inode_mnt, so that we don't need to mount one by ourselves.\nstatic struct vfsmount *anon_inode_mnt __read_mostly;\n\nstatic struct inode *\nksu_anon_inode_make_secure_inode(const char *name,\n                                 const struct inode *context_inode)\n{\n    struct inode *inode;\n    const struct qstr qname = QSTR_INIT(name, strlen(name));\n    int error;\n\n    if (unlikely(!anon_inode_mnt)) {\n        return ERR_PTR(-ENODEV);\n    }\n\n    inode = alloc_anon_inode(anon_inode_mnt->mnt_sb);\n    if (IS_ERR(inode))\n        return inode;\n    inode->i_flags &= ~S_PRIVATE;\n    error = security_inode_init_security_anon(inode, &qname, context_inode);\n    if (error) {\n        iput(inode);\n        return ERR_PTR(error);\n    }\n    return inode;\n}\n\nstatic struct file *ksu_anon_inode_create_getfile_compat(\n    const char *name, const struct file_operations *fops, void *priv, int flags,\n    const struct inode *context_inode)\n{\n    struct inode *inode;\n    struct file *file;\n\n    if (fops->owner && !try_module_get(fops->owner))\n        return ERR_PTR(-ENOENT);\n\n    inode = ksu_anon_inode_make_secure_inode(name, context_inode);\n    if (IS_ERR(inode)) {\n        file = ERR_CAST(inode);\n        goto err;\n    }\n\n    file = alloc_file_pseudo(inode, anon_inode_mnt, name,\n                             flags & (O_ACCMODE | O_NONBLOCK), fops);\n    if (IS_ERR(file))\n        goto err_iput;\n\n    file->f_mapping = inode->i_mapping;\n\n    file->private_data = priv;\n\n    return file;\n\nerr_iput:\n    iput(inode);\nerr:\n    module_put(fops->owner);\n    return file;\n}\n#endif\n\nint ksu_install_file_wrapper(int fd)\n{\n    int out_fd, ret;\n    struct file *orig_file = fget(fd);\n    if (!orig_file) {\n        return -EBADF;\n    }\n\n    out_fd = get_unused_fd_flags(O_CLOEXEC);\n    if (out_fd < 0) {\n        ret = out_fd;\n        goto done;\n    }\n\n    struct ksu_file_wrapper *file_wrapper_data =\n        ksu_create_file_wrapper(orig_file);\n    if (IS_ERR(file_wrapper_data)) {\n        ret = PTR_ERR(file_wrapper_data);\n        goto out_put_fd;\n    }\n\n    struct file *wrapper_file = ksu_anon_inode_create_getfile_compat(\n        \"[ksu_fdwrapper]\", &file_wrapper_data->ops, file_wrapper_data,\n        orig_file->f_flags, NULL);\n    if (IS_ERR(wrapper_file)) {\n        pr_err(\"ksu_fdwrapper: getfile failed: %ld\\n\", PTR_ERR(wrapper_file));\n        ret = PTR_ERR(wrapper_file);\n        goto out_release_wrapper;\n    }\n\n    // Now do magic on inode and dentry.\n    // It should be safe to modify them since the file hasn't been published.\n\n    struct inode *wrapper_inode = file_inode(wrapper_file);\n    // libc's stdio relies on the fstat() result of the fd to determine its buffer type.\n    wrapper_inode->i_mode = file_inode(orig_file)->i_mode;\n    struct inode_security_struct *wrapper_sec = selinux_inode(wrapper_inode);\n    // Use ksu_file_sid to bypass SELinux check.\n    // When we call `su` from terminal app, this is useful.\n    if (wrapper_sec) {\n        wrapper_sec->sid = ksu_file_sid;\n    }\n    // Install open file operation for inode.\n    wrapper_inode->i_fop = &ksu_file_wrapper_inode_fops;\n\n    struct path *orig_path = kmalloc(sizeof(struct path), GFP_KERNEL);\n    if (!orig_path) {\n        ret = -ENOMEM;\n        goto out_put_wrapper_file;\n    }\n    *orig_path = orig_file->f_path;\n    path_get(orig_path);\n    // Some applications (such as screen) won't work if the tty's path is weird,\n    // Therefore, we use d_dname to spoof it to return the path to the original file.\n    wrapper_file->f_path.dentry->d_fsdata = orig_path;\n    wrapper_file->f_path.dentry->d_op = &ksu_file_wrapper_d_ops;\n\n    fd_install(out_fd, wrapper_file);\n    ret = out_fd;\n    goto done;\n\nout_put_wrapper_file:\n    fput(wrapper_file);\n    // file_wrapper will be released by fput\n    goto out_put_fd;\nout_release_wrapper:\n    ksu_release_file_wrapper(file_wrapper_data);\nout_put_fd:\n    put_unused_fd(out_fd);\ndone:\n    fput(orig_file);\n\n    return ret;\n}\n\nvoid ksu_file_wrapper_init(void)\n{\n#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 16, 0)\n    static const struct file_operations tmp = { .owner = THIS_MODULE };\n    struct file *dummy = anon_inode_getfile(\"dummy\", &tmp, NULL, 0);\n    if (IS_ERR(dummy)) {\n        pr_err(\n            \"file_wrapper: initialize anon_inode_mnt failed, can't get file: %ld\\n\",\n            PTR_ERR(dummy));\n        return;\n    }\n    anon_inode_mnt = dummy->f_path.mnt;\n    if (unlikely(!anon_inode_mnt)) {\n        pr_err(\"file_wrapper: initialize anon_inode_mnt failed, got NULL\\n\");\n    }\n    fput(dummy);\n#endif\n}\n"
  },
  {
    "path": "kernel/file_wrapper.h",
    "content": "#ifndef KSU_FILE_WRAPPER_H\n#define KSU_FILE_WRAPPER_H\n\n#include <linux/file.h>\n#include <linux/fs.h>\n\nint ksu_install_file_wrapper(int fd);\nvoid ksu_file_wrapper_init(void);\n\n#endif // KSU_FILE_WRAPPER_H\n"
  },
  {
    "path": "kernel/kernel_umount.c",
    "content": "#include <linux/sched.h>\n#include <linux/slab.h>\n#include <linux/task_work.h>\n#include <linux/cred.h>\n#include <linux/fs.h>\n#include <linux/mount.h>\n#include <linux/namei.h>\n#include <linux/nsproxy.h>\n#include <linux/path.h>\n#include <linux/printk.h>\n#include <linux/types.h>\n\n#include \"kernel_umount.h\"\n#include \"klog.h\" // IWYU pragma: keep\n#include \"allowlist.h\"\n#include \"selinux/selinux.h\"\n#include \"feature.h\"\n#include \"ksud.h\"\n#include \"ksu.h\"\n\nstatic bool ksu_kernel_umount_enabled = true;\n\nstatic int kernel_umount_feature_get(u64 *value)\n{\n    *value = ksu_kernel_umount_enabled ? 1 : 0;\n    return 0;\n}\n\nstatic int kernel_umount_feature_set(u64 value)\n{\n    bool enable = value != 0;\n    ksu_kernel_umount_enabled = enable;\n    pr_info(\"kernel_umount: set to %d\\n\", enable);\n    return 0;\n}\n\nstatic const struct ksu_feature_handler kernel_umount_handler = {\n    .feature_id = KSU_FEATURE_KERNEL_UMOUNT,\n    .name = \"kernel_umount\",\n    .get_handler = kernel_umount_feature_get,\n    .set_handler = kernel_umount_feature_set,\n};\n\nextern int path_umount(struct path *path, int flags);\n\nstatic void ksu_umount_mnt(struct path *path, int flags)\n{\n    int err = path_umount(path, flags);\n    if (err) {\n        pr_info(\"umount %s failed: %d\\n\", path->dentry->d_iname, err);\n    }\n}\n\nstatic void try_umount(const char *mnt, int flags)\n{\n    struct path path;\n    int err = kern_path(mnt, 0, &path);\n    if (err) {\n        return;\n    }\n\n    if (path.dentry != path.mnt->mnt_root) {\n        // it is not root mountpoint, maybe umounted by others already.\n        path_put(&path);\n        return;\n    }\n\n    ksu_umount_mnt(&path, flags);\n}\n\nstruct umount_tw {\n    struct callback_head cb;\n};\n\nstatic void umount_tw_func(struct callback_head *cb)\n{\n    struct umount_tw *tw = container_of(cb, struct umount_tw, cb);\n    const struct cred *saved = override_creds(ksu_cred);\n\n    struct mount_entry *entry;\n    down_read(&mount_list_lock);\n    list_for_each_entry (entry, &mount_list, list) {\n        pr_info(\"%s: unmounting: %s flags 0x%x\\n\", __func__, entry->umountable,\n                entry->flags);\n        try_umount(entry->umountable, entry->flags);\n    }\n    up_read(&mount_list_lock);\n\n    revert_creds(saved);\n\n    kfree(tw);\n}\n\nint ksu_handle_umount(uid_t old_uid, uid_t new_uid)\n{\n    struct umount_tw *tw;\n\n    // if there isn't any module mounted, just ignore it!\n    if (!ksu_module_mounted) {\n        return 0;\n    }\n\n    if (!ksu_kernel_umount_enabled) {\n        return 0;\n    }\n\n    if (!ksu_cred) {\n        return 0;\n    }\n\n    // There are 5 scenarios:\n    // 1. Normal app: zygote -> appuid\n    // 2. Isolated process forked from zygote: zygote -> isolated_process\n    // 3. App zygote forked from zygote: zygote -> appuid\n    // 4. Isolated process froked from app zygote: appuid -> isolated_process (already handled by 3)\n    // 5. Isolated process froked from webview zygote (no need to handle, app cannot run custom code)\n    if (!is_appuid(new_uid) && !is_isolated_process(new_uid)) {\n        return 0;\n    }\n\n    if (!ksu_uid_should_umount(new_uid) && !is_isolated_process(new_uid)) {\n        return 0;\n    }\n\n    // check old process's selinux context, if it is not zygote, ignore it!\n    // because some su apps may setuid to untrusted_app but they are in global mount namespace\n    // when we umount for such process, that is a disaster!\n    // also handle case 4 and 5\n    bool is_zygote_child = is_zygote(get_current_cred());\n    if (!is_zygote_child) {\n        pr_info(\"handle umount ignore non zygote child: %d\\n\", current->pid);\n        return 0;\n    }\n    // umount the target mnt\n    pr_info(\"handle umount for uid: %d, pid: %d\\n\", new_uid, current->pid);\n\n    tw = kzalloc(sizeof(*tw), GFP_ATOMIC);\n    if (!tw)\n        return 0;\n\n    tw->cb.func = umount_tw_func;\n\n    int err = task_work_add(current, &tw->cb, TWA_RESUME);\n    if (err) {\n        kfree(tw);\n        pr_warn(\"unmount add task_work failed\\n\");\n    }\n\n    return 0;\n}\n\nvoid ksu_kernel_umount_init(void)\n{\n    if (ksu_register_feature_handler(&kernel_umount_handler)) {\n        pr_err(\"Failed to register kernel_umount feature handler\\n\");\n    }\n}\n\nvoid ksu_kernel_umount_exit(void)\n{\n    ksu_unregister_feature_handler(KSU_FEATURE_KERNEL_UMOUNT);\n}\n"
  },
  {
    "path": "kernel/kernel_umount.h",
    "content": "#ifndef __KSU_H_KERNEL_UMOUNT\n#define __KSU_H_KERNEL_UMOUNT\n\n#include <linux/types.h>\n#include <linux/list.h>\n#include <linux/rwsem.h>\n\nvoid ksu_kernel_umount_init(void);\nvoid ksu_kernel_umount_exit(void);\n\n// Handler function to be called from setresuid hook\nint ksu_handle_umount(uid_t old_uid, uid_t new_uid);\n\n// for the umount list\nstruct mount_entry {\n    char *umountable;\n    unsigned int flags;\n    struct list_head list;\n};\nextern struct list_head mount_list;\nextern struct rw_semaphore mount_list_lock;\n\n#endif\n"
  },
  {
    "path": "kernel/klog.h",
    "content": "#ifndef __KSU_H_KLOG\n#define __KSU_H_KLOG\n\n#include <linux/printk.h>\n\n#ifdef pr_fmt\n#undef pr_fmt\n#define pr_fmt(fmt) \"KernelSU: \" fmt\n#endif\n\n#endif\n"
  },
  {
    "path": "kernel/ksu.c",
    "content": "#include <linux/export.h>\n#include <linux/fs.h>\n#include <linux/kobject.h>\n#include <linux/module.h>\n#include <linux/sched.h>\n#include <linux/workqueue.h>\n#include <linux/moduleparam.h>\n\n#include \"allowlist.h\"\n#include \"app_profile.h\"\n#include \"feature.h\"\n#include \"klog.h\" // IWYU pragma: keep\n#include \"manager.h\"\n#include \"throne_tracker.h\"\n#include \"syscall_hook_manager.h\"\n#include \"ksud.h\"\n#include \"supercalls.h\"\n#include \"ksu.h\"\n#include \"file_wrapper.h\"\n#include \"selinux/selinux.h\"\n\n// workaround for A12-5.10 kernel\n// Some third-party kernel (e.g. linegaeOS) uses wrong toolchain, which supports\n// CC_HAVE_STACKPROTECTOR_SYSREG while gki's toolchain doesn't.\n// Therefore, ksu lkm, which uses gki toolchain, requires this __stack_chk_guard,\n// while those third-party kernel can't provide.\n// Thus, we manually provide it instead of using kernel's\n#if defined(CONFIG_STACKPROTECTOR) &&                                          \\\n    (defined(CONFIG_ARM64) && defined(MODULE) &&                               \\\n     !defined(CONFIG_STACKPROTECTOR_PER_TASK))\n#include <linux/stackprotector.h>\n#include <linux/random.h>\nunsigned long __stack_chk_guard __ro_after_init\n    __attribute__((visibility(\"hidden\")));\n\n__attribute__((no_stack_protector)) void ksu_setup_stack_chk_guard()\n{\n    unsigned long canary;\n\n    /* Try to get a semi random initial value. */\n    get_random_bytes(&canary, sizeof(canary));\n    canary ^= LINUX_VERSION_CODE;\n    canary &= CANARY_MASK;\n    __stack_chk_guard = canary;\n}\n\n__attribute__((naked)) int __init kernelsu_init_early(void)\n{\n    asm(\"mov x19, x30;\\n\"\n        \"bl ksu_setup_stack_chk_guard;\\n\"\n        \"mov x30, x19;\\n\"\n        \"b kernelsu_init;\\n\");\n}\n#define NEED_OWN_STACKPROTECTOR 1\n#else\n#define NEED_OWN_STACKPROTECTOR 0\n#endif\n\nstruct cred *ksu_cred;\nbool ksu_late_loaded;\n\n#ifdef CONFIG_KSU_DEBUG\nbool allow_shell = true;\n#else\nbool allow_shell = false;\n#endif\nmodule_param(allow_shell, bool, 0);\n\nint __init kernelsu_init(void)\n{\n#ifdef MODULE\n    ksu_late_loaded = (current->pid != 1);\n#else\n    ksu_late_loaded = false;\n#endif\n\n#ifdef CONFIG_KSU_DEBUG\n    pr_alert(\"*************************************************************\");\n    pr_alert(\"**     NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE    **\");\n    pr_alert(\"**                                                         **\");\n    pr_alert(\"**         You are running KernelSU in DEBUG mode          **\");\n    pr_alert(\"**                                                         **\");\n    pr_alert(\"**     NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE    **\");\n    pr_alert(\"*************************************************************\");\n#endif\n    if (allow_shell) {\n        pr_alert(\"shell is allowed at init!\");\n    }\n\n    ksu_cred = prepare_creds();\n    if (!ksu_cred) {\n        pr_err(\"prepare cred failed!\\n\");\n    }\n\n    ksu_feature_init();\n\n    ksu_supercalls_init();\n\n    if (ksu_late_loaded) {\n        pr_info(\"late load mode, skipping kprobe hooks\\n\");\n\n        apply_kernelsu_rules();\n        cache_sid();\n        setup_ksu_cred();\n\n        // Grant current process (ksud late-load) root\n        // with KSU SELinux domain before enforcing SELinux, so it\n        // can continue to access /data/app etc. after enforcement.\n        escape_to_root_for_init();\n\n        ksu_allowlist_init();\n        ksu_load_allow_list();\n\n        ksu_syscall_hook_manager_init();\n\n        ksu_throne_tracker_init();\n        ksu_observer_init();\n        ksu_file_wrapper_init();\n\n        ksu_boot_completed = true;\n        track_throne(false);\n\n        if (!getenforce()) {\n            pr_info(\"Permissive SELinux, enforcing\\n\");\n            setenforce(true);\n        }\n\n    } else {\n        ksu_syscall_hook_manager_init();\n\n        ksu_allowlist_init();\n\n        ksu_throne_tracker_init();\n\n        ksu_ksud_init();\n\n        ksu_file_wrapper_init();\n    }\n\n#ifdef MODULE\n#ifndef CONFIG_KSU_DEBUG\n    kobject_del(&THIS_MODULE->mkobj.kobj);\n#endif\n#endif\n    return 0;\n}\n\nextern void ksu_observer_exit(void);\nvoid kernelsu_exit(void)\n{\n    ksu_allowlist_exit();\n\n    ksu_throne_tracker_exit();\n\n    ksu_observer_exit();\n\n    if (!ksu_late_loaded)\n        ksu_ksud_exit();\n\n    ksu_syscall_hook_manager_exit();\n\n    ksu_supercalls_exit();\n\n    ksu_feature_exit();\n\n    if (ksu_cred) {\n        put_cred(ksu_cred);\n    }\n}\n\n#if NEED_OWN_STACKPROTECTOR\nmodule_init(kernelsu_init_early);\n#else\nmodule_init(kernelsu_init);\n#endif\nmodule_exit(kernelsu_exit);\n\nMODULE_LICENSE(\"GPL\");\nMODULE_AUTHOR(\"weishu\");\nMODULE_DESCRIPTION(\"Android KernelSU\");\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0)\nMODULE_IMPORT_NS(\"VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver\");\n#else\nMODULE_IMPORT_NS(VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver);\n#endif\n"
  },
  {
    "path": "kernel/ksu.h",
    "content": "#ifndef __KSU_H_KSU\n#define __KSU_H_KSU\n\n#include <linux/types.h>\n#include <linux/workqueue.h>\n#include <linux/cred.h>\n\n#define KERNEL_SU_VERSION KSU_VERSION\n\n#define EVENT_POST_FS_DATA 1\n#define EVENT_BOOT_COMPLETED 2\n#define EVENT_MODULE_MOUNTED 3\n\nstatic inline int startswith(char *s, char *prefix)\n{\n    return strncmp(s, prefix, strlen(prefix));\n}\n\nstatic inline int endswith(const char *s, const char *t)\n{\n    size_t slen = strlen(s);\n    size_t tlen = strlen(t);\n    if (tlen > slen)\n        return 1;\n    return strcmp(s + slen - tlen, t);\n}\n\nextern struct cred *ksu_cred;\nextern bool ksu_late_loaded;\n\n#endif\n"
  },
  {
    "path": "kernel/ksud.c",
    "content": "#include <linux/rcupdate.h>\n#include <linux/slab.h>\n#include <linux/task_work.h>\n#include <asm/current.h>\n#include <linux/compat.h>\n#include <linux/cred.h>\n#include <linux/dcache.h>\n#include <linux/err.h>\n#include <linux/file.h>\n#include <linux/fs.h>\n#include <linux/version.h>\n#include <linux/input-event-codes.h>\n#include <linux/kprobes.h>\n#include <linux/printk.h>\n#include <linux/types.h>\n#include <linux/uaccess.h>\n#include <linux/namei.h>\n#include <linux/workqueue.h>\n#include <linux/uio.h>\n\n#include \"manager.h\"\n#include \"allowlist.h\"\n#include \"arch.h\"\n#include \"klog.h\" // IWYU pragma: keep\n#include \"ksu.h\"\n#include \"ksud.h\"\n#include \"util.h\"\n#include \"selinux/selinux.h\"\n#include \"throne_tracker.h\"\n\nbool ksu_module_mounted __read_mostly = false;\nbool ksu_boot_completed __read_mostly = false;\n\nstatic const char KERNEL_SU_RC[] =\n    \"\\n\"\n\n    \"on post-fs-data\\n\"\n    \"    start logd\\n\"\n    // We should wait for the post-fs-data finish\n    \"    exec u:r:\" KERNEL_SU_DOMAIN \":s0 root -- \" KSUD_PATH \" post-fs-data\\n\"\n    \"\\n\"\n\n    \"on nonencrypted\\n\"\n    \"    exec u:r:\" KERNEL_SU_DOMAIN \":s0 root -- \" KSUD_PATH \" services\\n\"\n    \"\\n\"\n\n    \"on property:vold.decrypt=trigger_restart_framework\\n\"\n    \"    exec u:r:\" KERNEL_SU_DOMAIN \":s0 root -- \" KSUD_PATH \" services\\n\"\n    \"\\n\"\n\n    \"on property:sys.boot_completed=1\\n\"\n    \"    exec u:r:\" KERNEL_SU_DOMAIN \":s0 root -- \" KSUD_PATH\n    \" boot-completed\\n\"\n    \"\\n\"\n\n    \"\\n\";\n\nstatic void stop_init_rc_hook();\nstatic void stop_execve_hook();\nstatic void stop_input_hook();\n\nstatic struct work_struct stop_init_rc_hook_work;\nstatic struct work_struct stop_execve_hook_work;\nstatic struct work_struct stop_input_hook_work;\n\nvoid on_post_fs_data(void)\n{\n    static bool done = false;\n    if (done) {\n        pr_info(\"on_post_fs_data already done\\n\");\n        return;\n    }\n    done = true;\n    pr_info(\"on_post_fs_data!\\n\");\n\n    ksu_load_allow_list();\n    ksu_observer_init();\n    // sanity check, this may influence the performance\n    stop_input_hook();\n}\n\nextern void ext4_unregister_sysfs(struct super_block *sb);\nint nuke_ext4_sysfs(const char *mnt)\n{\n    struct path path;\n    int err = kern_path(mnt, 0, &path);\n    if (err) {\n        pr_err(\"nuke path err: %d\\n\", err);\n        return err;\n    }\n\n    struct super_block *sb = path.dentry->d_inode->i_sb;\n    const char *name = sb->s_type->name;\n    if (strcmp(name, \"ext4\") != 0) {\n        pr_info(\"nuke but module aren't mounted\\n\");\n        path_put(&path);\n        return -EINVAL;\n    }\n\n    ext4_unregister_sysfs(sb);\n    path_put(&path);\n    return 0;\n}\n\nvoid on_module_mounted(void)\n{\n    pr_info(\"on_module_mounted!\\n\");\n    ksu_module_mounted = true;\n}\n\nvoid on_boot_completed(void)\n{\n    ksu_boot_completed = true;\n    pr_info(\"on_boot_completed!\\n\");\n    track_throne(true);\n}\n\n#define MAX_ARG_STRINGS 0x7FFFFFFF\nstruct user_arg_ptr {\n#ifdef CONFIG_COMPAT\n    bool is_compat;\n#endif\n    union {\n        const char __user *const __user *native;\n#ifdef CONFIG_COMPAT\n        const compat_uptr_t __user *compat;\n#endif\n    } ptr;\n};\n\nstatic const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr)\n{\n    const char __user *native;\n\n#ifdef CONFIG_COMPAT\n    if (unlikely(argv.is_compat)) {\n        compat_uptr_t compat;\n\n        if (get_user(compat, argv.ptr.compat + nr))\n            return ERR_PTR(-EFAULT);\n\n        return compat_ptr(compat);\n    }\n#endif\n\n    if (get_user(native, argv.ptr.native + nr))\n        return ERR_PTR(-EFAULT);\n\n    return native;\n}\n\n/*\n * count() counts the number of strings in array ARGV.\n */\n\n/*\n * Make sure old GCC compiler can use __maybe_unused,\n * Test passed in 4.4.x ~ 4.9.x when use GCC.\n */\n\nstatic int __maybe_unused count(struct user_arg_ptr argv, int max)\n{\n    int i = 0;\n\n    if (argv.ptr.native != NULL) {\n        for (;;) {\n            const char __user *p = get_user_arg_ptr(argv, i);\n\n            if (!p)\n                break;\n\n            if (IS_ERR(p))\n                return -EFAULT;\n\n            if (i >= max)\n                return -E2BIG;\n            ++i;\n\n            if (fatal_signal_pending(current))\n                return -ERESTARTNOHAND;\n        }\n    }\n    return i;\n}\n\nstatic void on_post_fs_data_cbfun(struct callback_head *cb)\n{\n    on_post_fs_data();\n}\n\nstatic struct callback_head on_post_fs_data_cb = { .func =\n                                                       on_post_fs_data_cbfun };\n\nstatic bool check_argv(struct user_arg_ptr argv, int index,\n                       const char *expected, char *buf, size_t buf_len)\n{\n    const char __user *p;\n    int argc;\n\n    argc = count(argv, MAX_ARG_STRINGS);\n    if (argc <= index)\n        return false;\n\n    p = get_user_arg_ptr(argv, index);\n    if (!p || IS_ERR(p))\n        goto fail;\n\n    if (strncpy_from_user_nofault(buf, p, buf_len) <= 0)\n        goto fail;\n\n    buf[buf_len - 1] = '\\0';\n    return !strcmp(buf, expected);\n\nfail:\n    pr_err(\"check_argv failed\\n\");\n    return false;\n}\n\nstatic void ksu_initialize_selinux_tw_func(struct callback_head *cb)\n{\n    apply_kernelsu_rules();\n    cache_sid();\n    setup_ksu_cred();\n    kfree(cb);\n}\n\n// IMPORTANT NOTE: the call from execve_handler_pre WON'T provided correct value for envp and flags in GKI version\nint ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr,\n                             struct user_arg_ptr *argv,\n                             struct user_arg_ptr *envp, int *flags)\n{\n    struct filename *filename;\n\n    static const char app_process[] = \"/system/bin/app_process\";\n    static bool first_zygote = true;\n\n    /* This applies to versions Android 10+ */\n    static const char system_bin_init[] = \"/system/bin/init\";\n    static bool init_second_stage_executed = false;\n\n    if (!filename_ptr)\n        return 0;\n\n    filename = *filename_ptr;\n    if (IS_ERR(filename)) {\n        return 0;\n    }\n\n    // https://cs.android.com/android/platform/superproject/+/android-16.0.0_r2:system/core/init/main.cpp;l=77\n    if (unlikely(!memcmp(filename->name, system_bin_init,\n                         sizeof(system_bin_init) - 1) &&\n                 argv)) {\n        char buf[16];\n        if (!init_second_stage_executed &&\n            check_argv(*argv, 1, \"second_stage\", buf, sizeof(buf))) {\n            pr_info(\"/system/bin/init second_stage executed\\n\");\n            struct callback_head *cb = kzalloc(sizeof(*cb), GFP_ATOMIC);\n            if (cb) {\n                cb->func = ksu_initialize_selinux_tw_func;\n                if (task_work_add(current, cb, TWA_RESUME)) {\n                    kfree(cb);\n                    pr_warn(\"ksu_initialize_selinux failed to add task work\\n\");\n                }\n            } else {\n                pr_warn(\n                    \"ksu_initialize_selinux failed to allocate task work\\n\");\n            }\n            init_second_stage_executed = true;\n        }\n    }\n\n    if (unlikely(\n            first_zygote &&\n            !memcmp(filename->name, app_process, sizeof(app_process) - 1) &&\n            argv)) {\n        char buf[16];\n        if (check_argv(*argv, 1, \"-Xzygote\", buf, sizeof(buf))) {\n            pr_info(\"exec zygote, /data prepared, second_stage: %d\\n\",\n                    init_second_stage_executed);\n            rcu_read_lock();\n            struct task_struct *init_task =\n                rcu_dereference(current->real_parent);\n            if (init_task)\n                task_work_add(init_task, &on_post_fs_data_cb, TWA_RESUME);\n            rcu_read_unlock();\n            first_zygote = false;\n            stop_execve_hook();\n        }\n    }\n\n    return 0;\n}\n\nstatic ssize_t (*orig_read)(struct file *, char __user *, size_t, loff_t *);\nstatic ssize_t (*orig_read_iter)(struct kiocb *, struct iov_iter *);\nstatic struct file_operations fops_proxy;\nstatic ssize_t ksu_rc_pos = 0;\nconst size_t ksu_rc_len = sizeof(KERNEL_SU_RC) - 1;\n\n// https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/parser.cpp;l=144;drc=61197364367c9e404c7da6900658f1b16c42d0da\n// https://cs.android.com/android/platform/superproject/main/+/main:system/libbase/file.cpp;l=241-243;drc=61197364367c9e404c7da6900658f1b16c42d0da\n// The system will read init.rc file until EOF, whenever read() returns 0,\n// so we begin append ksu rc when we meet EOF.\n\nstatic ssize_t read_proxy(struct file *file, char __user *buf, size_t count,\n                          loff_t *pos)\n{\n    ssize_t ret = 0;\n    size_t append_count;\n    if (ksu_rc_pos && ksu_rc_pos < ksu_rc_len)\n        goto append_ksu_rc;\n\n    ret = orig_read(file, buf, count, pos);\n    if (ret != 0 || ksu_rc_pos >= ksu_rc_len) {\n        return ret;\n    } else {\n        pr_info(\"read_proxy: orig read finished, start append rc\\n\");\n    }\nappend_ksu_rc:\n    append_count = ksu_rc_len - ksu_rc_pos;\n    if (append_count > count - ret)\n        append_count = count - ret;\n    // copy_to_user returns the number of not copied\n    if (copy_to_user(buf + ret, KERNEL_SU_RC + ksu_rc_pos, append_count)) {\n        pr_info(\"read_proxy: append error, totally appended %ld\\n\", ksu_rc_pos);\n    } else {\n        pr_info(\"read_proxy: append %ld\\n\", append_count);\n\n        ksu_rc_pos += append_count;\n        if (ksu_rc_pos == ksu_rc_len) {\n            pr_info(\"read_proxy: append done\\n\");\n        }\n        ret += append_count;\n    }\n\n    return ret;\n}\n\nstatic ssize_t read_iter_proxy(struct kiocb *iocb, struct iov_iter *to)\n{\n    ssize_t ret = 0;\n    size_t append_count;\n    if (ksu_rc_pos && ksu_rc_pos < ksu_rc_len)\n        goto append_ksu_rc;\n\n    ret = orig_read_iter(iocb, to);\n    if (ret != 0 || ksu_rc_pos >= ksu_rc_len) {\n        return ret;\n    } else {\n        pr_info(\"read_iter_proxy: orig read finished, start append rc\\n\");\n    }\nappend_ksu_rc:\n    // copy_to_iter returns the number of copied bytes\n    append_count =\n        copy_to_iter(KERNEL_SU_RC + ksu_rc_pos, ksu_rc_len - ksu_rc_pos, to);\n    if (!append_count) {\n        pr_info(\"read_iter_proxy: append error, totally appended %ld\\n\",\n                ksu_rc_pos);\n    } else {\n        pr_info(\"read_iter_proxy: append %ld\\n\", append_count);\n\n        ksu_rc_pos += append_count;\n        if (ksu_rc_pos == ksu_rc_len) {\n            pr_info(\"read_iter_proxy: append done\\n\");\n        }\n        ret += append_count;\n    }\n    return ret;\n}\n\nstatic bool is_init_rc(struct file *fp)\n{\n    if (strcmp(current->comm, \"init\")) {\n        // we are only interest in `init` process\n        return false;\n    }\n\n    if (!d_is_reg(fp->f_path.dentry)) {\n        return false;\n    }\n\n    const char *short_name = fp->f_path.dentry->d_name.name;\n    if (strcmp(short_name, \"init.rc\")) {\n        // we are only interest `init.rc` file name file\n        return false;\n    }\n    char path[256];\n    char *dpath = d_path(&fp->f_path, path, sizeof(path));\n\n    if (IS_ERR(dpath)) {\n        return false;\n    }\n\n    if (strcmp(dpath, \"/system/etc/init/hw/init.rc\")) {\n        return false;\n    }\n\n    return true;\n}\n\nstatic void ksu_handle_sys_read(unsigned int fd)\n{\n    struct file *file = fget(fd);\n    if (!file) {\n        return;\n    }\n\n    if (!is_init_rc(file)) {\n        goto skip;\n    }\n\n    // we only process the first read\n    static bool rc_hooked = false;\n    if (rc_hooked) {\n        // we don't need these kprobe, unregister it!\n        stop_init_rc_hook();\n        goto skip;\n    }\n    rc_hooked = true;\n\n    // now we can sure that the init process is reading\n    // `/system/etc/init/init.rc`\n\n    pr_info(\"read init.rc, comm: %s, rc_count: %zu\\n\", current->comm,\n            ksu_rc_len);\n\n    // Now we need to proxy the read and modify the result!\n    // But, we can not modify the file_operations directly, because it's in read-only memory.\n    // We just replace the whole file_operations with a proxy one.\n    memcpy(&fops_proxy, file->f_op, sizeof(struct file_operations));\n    orig_read = file->f_op->read;\n    if (orig_read) {\n        fops_proxy.read = read_proxy;\n    }\n    orig_read_iter = file->f_op->read_iter;\n    if (orig_read_iter) {\n        fops_proxy.read_iter = read_iter_proxy;\n    }\n    // replace the file_operations\n    file->f_op = &fops_proxy;\n\nskip:\n    fput(file);\n}\n\nstatic unsigned int volumedown_pressed_count = 0;\n\nstatic bool is_volumedown_enough(unsigned int count)\n{\n    return count >= 3;\n}\n\nint ksu_handle_input_handle_event(unsigned int *type, unsigned int *code,\n                                  int *value)\n{\n    if (*type == EV_KEY && *code == KEY_VOLUMEDOWN) {\n        int val = *value;\n        pr_info(\"KEY_VOLUMEDOWN val: %d\\n\", val);\n        if (val) {\n            // key pressed, count it\n            volumedown_pressed_count += 1;\n            if (is_volumedown_enough(volumedown_pressed_count)) {\n                stop_input_hook();\n            }\n        }\n    }\n\n    return 0;\n}\n\nbool ksu_is_safe_mode()\n{\n    static bool safe_mode = false;\n    if (safe_mode) {\n        // don't need to check again, userspace may call multiple times\n        return true;\n    }\n\n    if (ksu_late_loaded) {\n        return false;\n    }\n\n    // stop hook first!\n    stop_input_hook();\n\n    pr_info(\"volumedown_pressed_count: %d\\n\", volumedown_pressed_count);\n    if (is_volumedown_enough(volumedown_pressed_count)) {\n        // pressed over 3 times\n        pr_info(\"KEY_VOLUMEDOWN pressed max times, safe mode detected!\\n\");\n        safe_mode = true;\n        return true;\n    }\n\n    return false;\n}\n\nstatic int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs)\n{\n    struct pt_regs *real_regs = PT_REAL_REGS(regs);\n    const char __user **filename_user =\n        (const char **)&PT_REGS_PARM1(real_regs);\n    const char __user *const __user *__argv =\n        (const char __user *const __user *)PT_REGS_PARM2(real_regs);\n    struct user_arg_ptr argv = { .ptr.native = __argv };\n    struct filename filename_in, *filename_p;\n    char path[32];\n    long ret;\n    unsigned long addr;\n    const char __user *fn;\n\n    if (!filename_user)\n        return 0;\n\n    addr = untagged_addr((unsigned long)*filename_user);\n    fn = (const char __user *)addr;\n\n    memset(path, 0, sizeof(path));\n    ret = strncpy_from_user_nofault(path, fn, 32);\n    if (ret < 0 && try_set_access_flag(addr)) {\n        ret = strncpy_from_user_nofault(path, fn, 32);\n    }\n    if (ret < 0) {\n        pr_err(\"Access filename failed for execve_handler_pre\\n\");\n        return 0;\n    }\n    filename_in.name = path;\n\n    filename_p = &filename_in;\n    return ksu_handle_execveat_ksud(AT_FDCWD, &filename_p, &argv, NULL, NULL);\n}\n\nstatic int sys_read_handler_pre(struct kprobe *p, struct pt_regs *regs)\n{\n    struct pt_regs *real_regs = PT_REAL_REGS(regs);\n    unsigned int fd = PT_REGS_PARM1(real_regs);\n\n    ksu_handle_sys_read(fd);\n    return 0;\n}\n\nstatic int sys_fstat_handler_pre(struct kretprobe_instance *p,\n                                 struct pt_regs *regs)\n{\n    struct pt_regs *real_regs = PT_REAL_REGS(regs);\n    unsigned int fd = PT_REGS_PARM1(real_regs);\n    void *statbuf = PT_REGS_PARM2(real_regs);\n    *(void **)&p->data = NULL;\n\n    struct file *file = fget(fd);\n    if (!file)\n        return 1;\n    if (is_init_rc(file)) {\n        pr_info(\"stat init.rc\");\n        fput(file);\n        *(void **)&p->data = statbuf;\n        return 0;\n    }\n    fput(file);\n    return 1;\n}\n\nstatic int sys_fstat_handler_post(struct kretprobe_instance *p,\n                                  struct pt_regs *regs)\n{\n    void __user *statbuf = *(void **)&p->data;\n    if (statbuf) {\n        void __user *st_size_ptr = statbuf + offsetof(struct stat, st_size);\n        long size, new_size;\n        if (!copy_from_user_nofault(&size, st_size_ptr, sizeof(long))) {\n            new_size = size + ksu_rc_len;\n            pr_info(\"adding ksu_rc_len: %ld -> %ld\", size, new_size);\n            if (!copy_to_user_nofault(st_size_ptr, &new_size, sizeof(long))) {\n                pr_info(\"added ksu_rc_len\");\n            } else {\n                pr_err(\"add ksu_rc_len failed: statbuf 0x%lx\",\n                       (unsigned long)st_size_ptr);\n            }\n        } else {\n            pr_err(\"read statbuf 0x%lx failed\", (unsigned long)st_size_ptr);\n        }\n    }\n\n    return 0;\n}\n\nstatic int input_handle_event_handler_pre(struct kprobe *p,\n                                          struct pt_regs *regs)\n{\n    unsigned int *type = (unsigned int *)&PT_REGS_PARM2(regs);\n    unsigned int *code = (unsigned int *)&PT_REGS_PARM3(regs);\n    int *value = (int *)&PT_REGS_CCALL_PARM4(regs);\n    return ksu_handle_input_handle_event(type, code, value);\n}\n\nstatic struct kprobe execve_kp = {\n    .symbol_name = SYS_EXECVE_SYMBOL,\n    .pre_handler = sys_execve_handler_pre,\n};\n\nstatic struct kprobe sys_read_kp = {\n    .symbol_name = SYS_READ_SYMBOL,\n    .pre_handler = sys_read_handler_pre,\n};\n\nstatic struct kretprobe sys_fstat_kp = {\n    .kp.symbol_name = SYS_FSTAT_SYMBOL,\n    .entry_handler = sys_fstat_handler_pre,\n    .handler = sys_fstat_handler_post,\n    .data_size = sizeof(void *),\n};\n\nstatic struct kprobe input_event_kp = {\n    .symbol_name = \"input_event\",\n    .pre_handler = input_handle_event_handler_pre,\n};\n\nstatic void do_stop_init_rc_hook(struct work_struct *work)\n{\n    unregister_kprobe(&sys_read_kp);\n    unregister_kretprobe(&sys_fstat_kp);\n}\n\nstatic void do_stop_execve_hook(struct work_struct *work)\n{\n    unregister_kprobe(&execve_kp);\n}\n\nstatic void do_stop_input_hook(struct work_struct *work)\n{\n    unregister_kprobe(&input_event_kp);\n}\n\nstatic void stop_init_rc_hook()\n{\n    bool ret = schedule_work(&stop_init_rc_hook_work);\n    pr_info(\"unregister init_rc_hook kprobe: %d!\\n\", ret);\n}\n\nstatic void stop_execve_hook()\n{\n    bool ret = schedule_work(&stop_execve_hook_work);\n    pr_info(\"unregister execve kprobe: %d!\\n\", ret);\n}\n\nstatic void stop_input_hook()\n{\n    static bool input_hook_stopped = false;\n    if (input_hook_stopped) {\n        return;\n    }\n    input_hook_stopped = true;\n    bool ret = schedule_work(&stop_input_hook_work);\n    pr_info(\"unregister input kprobe: %d!\\n\", ret);\n}\n\n// ksud: module support\nvoid ksu_ksud_init()\n{\n    int ret;\n\n    ret = register_kprobe(&execve_kp);\n    pr_info(\"ksud: execve_kp: %d\\n\", ret);\n\n    ret = register_kprobe(&sys_read_kp);\n    pr_info(\"ksud: sys_read_kp: %d\\n\", ret);\n\n    ret = register_kretprobe(&sys_fstat_kp);\n    pr_info(\"ksud: sys_fstat_kp: %d\\n\", ret);\n\n    ret = register_kprobe(&input_event_kp);\n    pr_info(\"ksud: input_event_kp: %d\\n\", ret);\n\n    INIT_WORK(&stop_init_rc_hook_work, do_stop_init_rc_hook);\n    INIT_WORK(&stop_execve_hook_work, do_stop_execve_hook);\n    INIT_WORK(&stop_input_hook_work, do_stop_input_hook);\n}\n\nvoid ksu_ksud_exit()\n{\n    unregister_kprobe(&execve_kp);\n    // this should be done before unregister sys_read_kp\n    // unregister_kprobe(&sys_read_kp);\n    unregister_kprobe(&input_event_kp);\n}\n"
  },
  {
    "path": "kernel/ksud.h",
    "content": "#ifndef __KSU_H_KSUD\n#define __KSU_H_KSUD\n\n#include <linux/types.h>\n\n#define KSUD_PATH \"/data/adb/ksud\"\n\nvoid ksu_ksud_init();\nvoid ksu_ksud_exit();\n\nvoid on_post_fs_data(void);\nvoid on_module_mounted(void);\nvoid on_boot_completed(void);\n\nbool ksu_is_safe_mode(void);\n\nint nuke_ext4_sysfs(const char *mnt);\n\nextern u32 ksu_file_sid;\nextern bool ksu_module_mounted;\nextern bool ksu_boot_completed;\n\n#endif\n"
  },
  {
    "path": "kernel/manager.h",
    "content": "#ifndef __KSU_H_KSU_MANAGER\n#define __KSU_H_KSU_MANAGER\n\n#include <linux/cred.h>\n#include <linux/types.h>\n#include \"allowlist.h\"\n\n#define KSU_INVALID_APPID -1\n\nextern uid_t ksu_manager_appid; // DO NOT DIRECT USE\n\nstatic inline bool ksu_is_manager_appid_valid()\n{\n    return ksu_manager_appid != KSU_INVALID_APPID;\n}\n\nstatic inline bool is_manager()\n{\n    return unlikely(ksu_manager_appid == current_uid().val % PER_USER_RANGE);\n}\n\nstatic inline bool is_uid_manager(uid_t uid)\n{\n    return unlikely(ksu_manager_appid == uid % PER_USER_RANGE);\n}\n\nstatic inline uid_t ksu_get_manager_appid()\n{\n    return ksu_manager_appid;\n}\n\nstatic inline void ksu_set_manager_appid(uid_t appid)\n{\n    ksu_manager_appid = appid;\n}\n\nstatic inline void ksu_invalidate_manager_uid()\n{\n    ksu_manager_appid = KSU_INVALID_APPID;\n}\n\nint ksu_observer_init(void);\n#endif\n"
  },
  {
    "path": "kernel/pkg_observer.c",
    "content": "// SPDX-License-Identifier: GPL-2.0\n#include <linux/module.h>\n#include <linux/fs.h>\n#include <linux/namei.h>\n#include <linux/fsnotify_backend.h>\n#include <linux/slab.h>\n#include <linux/rculist.h>\n#include <linux/version.h>\n#include \"klog.h\" // IWYU pragma: keep\n#include \"throne_tracker.h\"\n\n#define MASK_SYSTEM (FS_CREATE | FS_MOVE | FS_EVENT_ON_CHILD)\n\nstruct watch_dir {\n    const char *path;\n    u32 mask;\n    struct path kpath;\n    struct inode *inode;\n    struct fsnotify_mark *mark;\n};\n\nstatic struct fsnotify_group *g;\n\nstatic int ksu_handle_inode_event(struct fsnotify_mark *mark, u32 mask,\n                                  struct inode *inode, struct inode *dir,\n                                  const struct qstr *file_name, u32 cookie)\n{\n    if (!file_name)\n        return 0;\n    if (mask & FS_ISDIR)\n        return 0;\n    if (file_name->len == 13 && !memcmp(file_name->name, \"packages.list\", 13)) {\n        pr_info(\"packages.list detected: %d\\n\", mask);\n        track_throne(false);\n    }\n    return 0;\n}\n\nstatic const struct fsnotify_ops ksu_ops = {\n    .handle_inode_event = ksu_handle_inode_event,\n};\n\nstatic int add_mark_on_inode(struct inode *inode, u32 mask,\n                             struct fsnotify_mark **out)\n{\n    struct fsnotify_mark *m;\n\n    m = kzalloc(sizeof(*m), GFP_KERNEL);\n    if (!m)\n        return -ENOMEM;\n\n    fsnotify_init_mark(m, g);\n    m->mask = mask;\n\n    if (fsnotify_add_inode_mark(m, inode, 0)) {\n        fsnotify_put_mark(m);\n        return -EINVAL;\n    }\n    *out = m;\n    return 0;\n}\n\nstatic int watch_one_dir(struct watch_dir *wd)\n{\n    int ret = kern_path(wd->path, LOOKUP_FOLLOW, &wd->kpath);\n    if (ret) {\n        pr_info(\"path not ready: %s (%d)\\n\", wd->path, ret);\n        return ret;\n    }\n    wd->inode = d_inode(wd->kpath.dentry);\n    ihold(wd->inode);\n\n    ret = add_mark_on_inode(wd->inode, wd->mask, &wd->mark);\n    if (ret) {\n        pr_err(\"Add mark failed for %s (%d)\\n\", wd->path, ret);\n        path_put(&wd->kpath);\n        iput(wd->inode);\n        wd->inode = NULL;\n        return ret;\n    }\n    pr_info(\"watching %s\\n\", wd->path);\n    return 0;\n}\n\nstatic void unwatch_one_dir(struct watch_dir *wd)\n{\n    if (wd->mark) {\n        fsnotify_destroy_mark(wd->mark, g);\n        fsnotify_put_mark(wd->mark);\n        wd->mark = NULL;\n    }\n    if (wd->inode) {\n        iput(wd->inode);\n        wd->inode = NULL;\n    }\n    if (wd->kpath.dentry) {\n        path_put(&wd->kpath);\n        memset(&wd->kpath, 0, sizeof(wd->kpath));\n    }\n}\n\nstatic struct watch_dir g_watch = { .path = \"/data/system\",\n                                    .mask = MASK_SYSTEM };\n\nint ksu_observer_init(void)\n{\n    int ret = 0;\n\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 0, 0)\n    g = fsnotify_alloc_group(&ksu_ops, 0);\n#else\n    g = fsnotify_alloc_group(&ksu_ops);\n#endif\n    if (IS_ERR(g))\n        return PTR_ERR(g);\n\n    ret = watch_one_dir(&g_watch);\n    pr_info(\"observer init done\\n\");\n    return 0;\n}\n\nvoid ksu_observer_exit(void)\n{\n    unwatch_one_dir(&g_watch);\n    fsnotify_put_group(g);\n    pr_info(\"observer exit done\\n\");\n}"
  },
  {
    "path": "kernel/seccomp_cache.c",
    "content": "#include <linux/version.h>\n#include <linux/fs.h>\n#include <linux/nsproxy.h>\n#include <linux/sched/task.h>\n#include <linux/uaccess.h>\n#include <linux/filter.h>\n#include <linux/seccomp.h>\n#include \"klog.h\" // IWYU pragma: keep\n#include \"seccomp_cache.h\"\n\nstruct action_cache {\n    DECLARE_BITMAP(allow_native, SECCOMP_ARCH_NATIVE_NR);\n#ifdef SECCOMP_ARCH_COMPAT\n    DECLARE_BITMAP(allow_compat, SECCOMP_ARCH_COMPAT_NR);\n#endif\n};\n\nstruct seccomp_filter {\n    refcount_t refs;\n    refcount_t users;\n    bool log;\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 1, 0)\n    bool wait_killable_recv;\n#endif\n    struct action_cache cache;\n    struct seccomp_filter *prev;\n    struct bpf_prog *prog;\n    struct notification *notif;\n    struct mutex notify_lock;\n    wait_queue_head_t wqh;\n};\n\nvoid ksu_seccomp_clear_cache(struct seccomp_filter *filter, int nr)\n{\n    if (!filter) {\n        return;\n    }\n\n    if (nr >= 0 && nr < SECCOMP_ARCH_NATIVE_NR) {\n        clear_bit(nr, filter->cache.allow_native);\n    }\n\n#ifdef SECCOMP_ARCH_COMPAT\n    if (nr >= 0 && nr < SECCOMP_ARCH_COMPAT_NR) {\n        clear_bit(nr, filter->cache.allow_compat);\n    }\n#endif\n}\n\nvoid ksu_seccomp_allow_cache(struct seccomp_filter *filter, int nr)\n{\n    if (!filter) {\n        return;\n    }\n\n    if (nr >= 0 && nr < SECCOMP_ARCH_NATIVE_NR) {\n        set_bit(nr, filter->cache.allow_native);\n    }\n\n#ifdef SECCOMP_ARCH_COMPAT\n    if (nr >= 0 && nr < SECCOMP_ARCH_COMPAT_NR) {\n        set_bit(nr, filter->cache.allow_compat);\n    }\n#endif\n}\n"
  },
  {
    "path": "kernel/seccomp_cache.h",
    "content": "#ifndef __KSU_H_KERNEL_COMPAT\n#define __KSU_H_KERNEL_COMPAT\n\n#include <linux/fs.h>\n#include <linux/version.h>\n\nextern void ksu_seccomp_clear_cache(struct seccomp_filter *filter, int nr);\nextern void ksu_seccomp_allow_cache(struct seccomp_filter *filter, int nr);\n\n#endif\n"
  },
  {
    "path": "kernel/selinux/rules.c",
    "content": "#include \"linux/rcupdate.h\"\n#include \"security.h\"\n#include <linux/uaccess.h>\n#include <linux/types.h>\n#include <linux/version.h>\n#include <linux/lockdep.h>\n#include <linux/slab.h>\n#include <linux/string.h>\n\n#include \"../klog.h\" // IWYU pragma: keep\n#include \"selinux.h\"\n#include \"sepolicy.h\"\n#include \"ss/services.h\"\n#include \"linux/lsm_audit.h\" // IWYU pragma: keep\n#include \"xfrm.h\"\n\n#define SELINUX_POLICY_INSTEAD_SELINUX_SS\n\n#define ALL NULL\n\n#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0))\nextern int avc_ss_reset(u32 seqno);\n#else\nextern int avc_ss_reset(struct selinux_avc *avc, u32 seqno);\n#endif\n// reset avc cache table, otherwise the new rules will not take effect if already denied\nstatic void reset_avc_cache()\n{\n#if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0))\n    avc_ss_reset(0);\n    selnl_notify_policyload(0);\n    selinux_status_update_policyload(0);\n#else\n    struct selinux_avc *avc = selinux_state.avc;\n    avc_ss_reset(avc, 0);\n    selnl_notify_policyload(0);\n    selinux_status_update_policyload(&selinux_state, 0);\n#endif\n    selinux_xfrm_notify_policyload();\n}\n\nvoid apply_kernelsu_rules()\n{\n    struct selinux_policy *pol, *old_pol = selinux_state.policy;\n    struct policydb *db;\n\n    if (!getenforce()) {\n        pr_info(\"SELinux permissive or disabled, apply rules!\\n\");\n    }\n\n    mutex_lock(&selinux_state.policy_mutex);\n    pol = ksu_dup_sepolicy(rcu_dereference_protected(\n        old_pol, lockdep_is_held(&selinux_state.policy_mutex)));\n    if (!pol) {\n        pr_err(\"failed to dup selinux_policy\\n\");\n        goto out_unlock;\n    }\n\n    db = &pol->policydb;\n\n    ksu_permissive(db, KERNEL_SU_DOMAIN);\n    ksu_typeattribute(db, KERNEL_SU_DOMAIN, \"mlstrustedsubject\");\n    ksu_typeattribute(db, KERNEL_SU_DOMAIN, \"netdomain\");\n    ksu_typeattribute(db, KERNEL_SU_DOMAIN, \"bluetoothdomain\");\n\n    // Create unconstrained file type\n    ksu_type(db, KERNEL_SU_FILE, \"file_type\");\n    ksu_typeattribute(db, KERNEL_SU_FILE, \"mlstrustedobject\");\n    ksu_allow(db, ALL, KERNEL_SU_FILE, ALL, ALL);\n\n    // allow all!\n    ksu_allow(db, KERNEL_SU_DOMAIN, ALL, ALL, ALL);\n\n    // allow us do any ioctl\n    if (db->policyvers >= POLICYDB_VERSION_XPERMS_IOCTL) {\n        ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, \"blk_file\", ALL);\n        ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, \"fifo_file\", ALL);\n        ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, \"chr_file\", ALL);\n        ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, \"file\", ALL);\n    }\n\n    // our ksud triggered by init\n    ksu_allow(db, \"init\", KERNEL_SU_DOMAIN, ALL, ALL);\n\n    // copied from Magisk rules\n    // suRights\n    ksu_allow(db, \"servicemanager\", KERNEL_SU_DOMAIN, \"dir\", \"search\");\n    ksu_allow(db, \"servicemanager\", KERNEL_SU_DOMAIN, \"dir\", \"read\");\n    ksu_allow(db, \"servicemanager\", KERNEL_SU_DOMAIN, \"file\", \"open\");\n    ksu_allow(db, \"servicemanager\", KERNEL_SU_DOMAIN, \"file\", \"read\");\n    ksu_allow(db, \"servicemanager\", KERNEL_SU_DOMAIN, \"process\", \"getattr\");\n    ksu_allow(db, ALL, KERNEL_SU_DOMAIN, \"process\", \"sigchld\");\n\n    // allowLog\n    ksu_allow(db, \"logd\", KERNEL_SU_DOMAIN, \"dir\", \"search\");\n    ksu_allow(db, \"logd\", KERNEL_SU_DOMAIN, \"file\", \"read\");\n    ksu_allow(db, \"logd\", KERNEL_SU_DOMAIN, \"file\", \"open\");\n    ksu_allow(db, \"logd\", KERNEL_SU_DOMAIN, \"file\", \"getattr\");\n\n    // dumpsys\n    ksu_allow(db, ALL, KERNEL_SU_DOMAIN, \"fd\", \"use\");\n    ksu_allow(db, ALL, KERNEL_SU_DOMAIN, \"fifo_file\", \"write\");\n    ksu_allow(db, ALL, KERNEL_SU_DOMAIN, \"fifo_file\", \"read\");\n    ksu_allow(db, ALL, KERNEL_SU_DOMAIN, \"fifo_file\", \"open\");\n    ksu_allow(db, ALL, KERNEL_SU_DOMAIN, \"fifo_file\", \"getattr\");\n\n    // bootctl\n    ksu_allow(db, \"hwservicemanager\", KERNEL_SU_DOMAIN, \"dir\", \"search\");\n    ksu_allow(db, \"hwservicemanager\", KERNEL_SU_DOMAIN, \"file\", \"read\");\n    ksu_allow(db, \"hwservicemanager\", KERNEL_SU_DOMAIN, \"file\", \"open\");\n    ksu_allow(db, \"hwservicemanager\", KERNEL_SU_DOMAIN, \"process\", \"getattr\");\n\n    // Allow all binder transactions\n    ksu_allow(db, ALL, KERNEL_SU_DOMAIN, \"binder\", ALL);\n\n    // Allow system server kill su process\n    ksu_allow(db, \"system_server\", KERNEL_SU_DOMAIN, \"process\", \"getpgid\");\n    ksu_allow(db, \"system_server\", KERNEL_SU_DOMAIN, \"process\", \"sigkill\");\n\n    rcu_assign_pointer(selinux_state.policy, pol);\n    synchronize_rcu();\n    ksu_destroy_sepolicy(old_pol);\n\n    reset_avc_cache();\nout_unlock:\n    mutex_unlock(&selinux_state.policy_mutex);\n}\n\n#define KSU_SEPOLICY_MAX_BATCH_SIZE (8U * 1024U * 1024U)\n#define KSU_SEPOLICY_MAX_ARGS 5\n\n#define CMD_NORMAL_PERM 1\n#define CMD_XPERM 2\n#define CMD_TYPE_STATE 3\n#define CMD_TYPE 4\n#define CMD_TYPE_ATTR 5\n#define CMD_ATTR 6\n#define CMD_TYPE_TRANSITION 7\n#define CMD_TYPE_CHANGE 8\n#define CMD_GENFSCON 9\n\n#define SUBCMD_NORMAL_PERM_ALLOW 1\n#define SUBCMD_NORMAL_PERM_DENY 2\n#define SUBCMD_NORMAL_PERM_AUDITALLOW 3\n#define SUBCMD_NORMAL_PERM_DONTAUDIT 4\n\n#define SUBCMD_XPERM_ALLOW 1\n#define SUBCMD_XPERM_AUDITALLOW 2\n#define SUBCMD_XPERM_DONTAUDIT 3\n\n#define SUBCMD_TYPE_STATE_PERMISSIVE 1\n#define SUBCMD_TYPE_STATE_ENFORCE 2\n\n#define SUBCMD_TYPE_CHANGE_CHANGE 1\n#define SUBCMD_TYPE_CHANGE_MEMBER 2\n\nstruct sepol_data {\n    u32 cmd;\n    u32 subcmd;\n};\n\nstruct sepol_batch_cursor {\n    const u8 *cur;\n    const u8 *end;\n};\n\nstatic size_t sepol_remaining(const struct sepol_batch_cursor *cursor)\n{\n    return (size_t)(cursor->end - cursor->cur);\n}\n\nstatic int sepol_read_cmd_header(struct sepol_batch_cursor *cursor,\n                                 struct sepol_data *header)\n{\n    if (sepol_remaining(cursor) < sizeof(*header)) {\n        return -EINVAL;\n    }\n\n    memcpy(header, cursor->cur, sizeof(*header));\n    cursor->cur += sizeof(*header);\n\n    return 0;\n}\n\nstatic int sepol_read_string(struct sepol_batch_cursor *cursor,\n                             const char **out)\n{\n    u32 len;\n    const char *str;\n\n    if (sepol_remaining(cursor) < sizeof(len)) {\n        return -EINVAL;\n    }\n\n    memcpy(&len, cursor->cur, sizeof(len));\n    cursor->cur += sizeof(len);\n\n    if (len >= sepol_remaining(cursor)) {\n        return -EINVAL;\n    }\n\n    str = (const char *)cursor->cur;\n    if (memchr(str, '\\0', len) != NULL || str[len] != '\\0') {\n        return -EINVAL;\n    }\n\n    cursor->cur += len + 1;\n    if (len == 0) {\n        *out = ALL;\n        return 0;\n    }\n\n    *out = str;\n    return 0;\n}\n\nstatic int sepol_require_not_all(const char *value, const char *name)\n{\n    if (value != ALL) {\n        return 0;\n    }\n\n    pr_err(\"sepol: %s cannot be ALL.\\n\", name);\n    return -EINVAL;\n}\n\nstatic int sepol_expected_argc(u32 cmd)\n{\n    switch (cmd) {\n    case CMD_NORMAL_PERM:\n        return 4;\n    case CMD_XPERM:\n        return 5;\n    case CMD_TYPE_STATE:\n        return 1;\n    case CMD_TYPE:\n    case CMD_TYPE_ATTR:\n        return 2;\n    case CMD_ATTR:\n        return 1;\n    case CMD_TYPE_TRANSITION:\n        return 5;\n    case CMD_TYPE_CHANGE:\n        return 4;\n    case CMD_GENFSCON:\n        return 3;\n    default:\n        return -EINVAL;\n    }\n}\n\nstatic int apply_one_sepolicy_cmd(struct policydb *db,\n                                  const struct sepol_data *header,\n                                  const char **args)\n{\n    bool success = false;\n    int ret;\n\n    switch (header->cmd) {\n    case CMD_NORMAL_PERM:\n        if (header->subcmd == SUBCMD_NORMAL_PERM_ALLOW) {\n            success = ksu_allow(db, args[0], args[1], args[2], args[3]);\n        } else if (header->subcmd == SUBCMD_NORMAL_PERM_DENY) {\n            success = ksu_deny(db, args[0], args[1], args[2], args[3]);\n        } else if (header->subcmd == SUBCMD_NORMAL_PERM_AUDITALLOW) {\n            success = ksu_auditallow(db, args[0], args[1], args[2], args[3]);\n        } else if (header->subcmd == SUBCMD_NORMAL_PERM_DONTAUDIT) {\n            success = ksu_dontaudit(db, args[0], args[1], args[2], args[3]);\n        } else {\n            pr_err(\"sepol: unknown subcmd: %d\\n\", header->subcmd);\n        }\n        return success ? 0 : -EINVAL;\n\n    case CMD_XPERM:\n        ret = sepol_require_not_all(args[3], \"operation\");\n        if (ret < 0) {\n            return ret;\n        }\n        ret = sepol_require_not_all(args[4], \"perm_set\");\n        if (ret < 0) {\n            return ret;\n        }\n\n        if (header->subcmd == SUBCMD_XPERM_ALLOW) {\n            success = ksu_allowxperm(db, args[0], args[1], args[2], args[4]);\n        } else if (header->subcmd == SUBCMD_XPERM_AUDITALLOW) {\n            success =\n                ksu_auditallowxperm(db, args[0], args[1], args[2], args[4]);\n        } else if (header->subcmd == SUBCMD_XPERM_DONTAUDIT) {\n            success =\n                ksu_dontauditxperm(db, args[0], args[1], args[2], args[4]);\n        } else {\n            pr_err(\"sepol: unknown subcmd: %d\\n\", header->subcmd);\n        }\n        return success ? 0 : -EINVAL;\n\n    case CMD_TYPE_STATE:\n        ret = sepol_require_not_all(args[0], \"type\");\n        if (ret < 0) {\n            return ret;\n        }\n\n        if (header->subcmd == SUBCMD_TYPE_STATE_PERMISSIVE) {\n            success = ksu_permissive(db, args[0]);\n        } else if (header->subcmd == SUBCMD_TYPE_STATE_ENFORCE) {\n            success = ksu_enforce(db, args[0]);\n        } else {\n            pr_err(\"sepol: unknown subcmd: %d\\n\", header->subcmd);\n        }\n        return success ? 0 : -EINVAL;\n\n    case CMD_TYPE:\n    case CMD_TYPE_ATTR:\n        ret = sepol_require_not_all(args[0], \"type\");\n        if (ret < 0) {\n            return ret;\n        }\n        ret = sepol_require_not_all(args[1], \"attribute\");\n        if (ret < 0) {\n            return ret;\n        }\n\n        if (header->cmd == CMD_TYPE) {\n            success = ksu_type(db, args[0], args[1]);\n        } else {\n            success = ksu_typeattribute(db, args[0], args[1]);\n        }\n        if (!success) {\n            pr_err(\"sepol: %d failed.\\n\", header->cmd);\n            return -EINVAL;\n        }\n        return 0;\n\n    case CMD_ATTR:\n        ret = sepol_require_not_all(args[0], \"attribute\");\n        if (ret < 0) {\n            return ret;\n        }\n\n        if (!ksu_attribute(db, args[0])) {\n            pr_err(\"sepol: %d failed.\\n\", header->cmd);\n            return -EINVAL;\n        }\n        return 0;\n\n    case CMD_TYPE_TRANSITION: {\n        const char *object = ALL;\n\n        ret = sepol_require_not_all(args[0], \"src\");\n        if (ret < 0) {\n            return ret;\n        }\n        ret = sepol_require_not_all(args[1], \"tgt\");\n        if (ret < 0) {\n            return ret;\n        }\n        ret = sepol_require_not_all(args[2], \"cls\");\n        if (ret < 0) {\n            return ret;\n        }\n        ret = sepol_require_not_all(args[3], \"default_type\");\n        if (ret < 0) {\n            return ret;\n        }\n\n        object = args[4];\n\n        success =\n            ksu_type_transition(db, args[0], args[1], args[2], args[3], object);\n        return success ? 0 : -EINVAL;\n    }\n\n    case CMD_TYPE_CHANGE:\n        ret = sepol_require_not_all(args[0], \"src\");\n        if (ret < 0) {\n            return ret;\n        }\n        ret = sepol_require_not_all(args[1], \"tgt\");\n        if (ret < 0) {\n            return ret;\n        }\n        ret = sepol_require_not_all(args[2], \"cls\");\n        if (ret < 0) {\n            return ret;\n        }\n        ret = sepol_require_not_all(args[3], \"default_type\");\n        if (ret < 0) {\n            return ret;\n        }\n\n        if (header->subcmd == SUBCMD_TYPE_CHANGE_CHANGE) {\n            success = ksu_type_change(db, args[0], args[1], args[2], args[3]);\n        } else if (header->subcmd == SUBCMD_TYPE_CHANGE_MEMBER) {\n            success = ksu_type_member(db, args[0], args[1], args[2], args[3]);\n        } else {\n            pr_err(\"sepol: unknown subcmd: %d\\n\", header->subcmd);\n        }\n        return success ? 0 : -EINVAL;\n\n    case CMD_GENFSCON:\n        ret = sepol_require_not_all(args[0], \"name\");\n        if (ret < 0) {\n            return ret;\n        }\n        ret = sepol_require_not_all(args[1], \"path\");\n        if (ret < 0) {\n            return ret;\n        }\n        ret = sepol_require_not_all(args[2], \"context\");\n        if (ret < 0) {\n            return ret;\n        }\n\n        if (!ksu_genfscon(db, args[0], args[1], args[2])) {\n            pr_err(\"sepol: %d failed.\\n\", header->cmd);\n            return -EINVAL;\n        }\n        return 0;\n\n    default:\n        pr_err(\"sepol: unknown cmd: %d\\n\", header->cmd);\n        return -EINVAL;\n    }\n}\n\nint handle_sepolicy(void __user *user_data, u64 data_len)\n{\n    struct selinux_policy *pol, *old_pol;\n    struct policydb *db;\n    struct sepol_batch_cursor cursor;\n    u8 *payload;\n    int ret;\n    int success_cmd_count;\n    u32 cmd_index;\n\n    if (!user_data || !data_len) {\n        return -EINVAL;\n    }\n\n    if (data_len > KSU_SEPOLICY_MAX_BATCH_SIZE) {\n        return -E2BIG;\n    }\n\n    payload = kvmalloc((size_t)data_len, GFP_KERNEL);\n    if (!payload) {\n        return -ENOMEM;\n    }\n\n    if (copy_from_user(payload, user_data, (size_t)data_len)) {\n        ret = -EFAULT;\n        goto out_free;\n    }\n\n    if (!getenforce()) {\n        pr_info(\"SELinux permissive or disabled when handle policy!\\n\");\n    }\n\n    mutex_lock(&selinux_state.policy_mutex);\n\n    old_pol = selinux_state.policy;\n    pol = ksu_dup_sepolicy(rcu_dereference_protected(\n        old_pol, lockdep_is_held(&selinux_state.policy_mutex)));\n    if (!pol) {\n        ret = -ENOMEM;\n        goto out_unlock;\n    }\n    db = &pol->policydb;\n\n    cursor.cur = payload;\n    cursor.end = payload + (size_t)data_len;\n\n    ret = 0;\n    success_cmd_count = 0;\n    cmd_index = 0;\n    while (cursor.cur < cursor.end) {\n        struct sepol_data header;\n        const char *args[KSU_SEPOLICY_MAX_ARGS] = { 0 };\n        int expected_argc;\n        u32 arg_index;\n\n        ret = sepol_read_cmd_header(&cursor, &header);\n        if (ret < 0) {\n            pr_err(\"sepol: failed to read cmd header #%u.\\n\", cmd_index);\n            goto out_drop_new_policy;\n        }\n\n        expected_argc = sepol_expected_argc(header.cmd);\n        if (expected_argc < 0 || expected_argc > KSU_SEPOLICY_MAX_ARGS) {\n            ret = -EINVAL;\n            pr_err(\"sepol: invalid cmd header #%u.\\n\", cmd_index);\n            goto out_drop_new_policy;\n        }\n\n        for (arg_index = 0; arg_index < (u32)expected_argc; arg_index++) {\n            ret = sepol_read_string(&cursor, &args[arg_index]);\n            if (ret < 0) {\n                pr_err(\"sepol: failed to read cmd #%u arg #%u.\\n\", cmd_index,\n                       arg_index);\n                goto out_drop_new_policy;\n            }\n        }\n\n        ret = apply_one_sepolicy_cmd(db, &header, args);\n        if (ret < 0) {\n            pr_err(\"sepol: cmd #%u failed, cmd=%u subcmd=%u.\\n\", cmd_index,\n                   header.cmd, header.subcmd);\n        } else {\n            success_cmd_count++;\n        }\n        cmd_index++;\n    }\n\n    rcu_assign_pointer(selinux_state.policy, pol);\n    synchronize_rcu();\n    ksu_destroy_sepolicy(old_pol);\n\n    reset_avc_cache();\n    ret = success_cmd_count;\n    goto out_unlock;\n\nout_drop_new_policy:\n    ksu_destroy_sepolicy(pol);\nout_unlock:\n    mutex_unlock(&selinux_state.policy_mutex);\nout_free:\n    kvfree(payload);\n\n    return ret;\n}\n"
  },
  {
    "path": "kernel/selinux/selinux.c",
    "content": "#include \"selinux.h\"\n#include \"linux/cred.h\"\n#include \"linux/sched.h\"\n#include \"objsec.h\"\n#include \"linux/version.h\"\n#include \"../klog.h\" // IWYU pragma: keep\n#include \"../ksu.h\"\n\n/*\n * Cached SID values for frequently checked contexts.\n * These are resolved once at init and used for fast u32 comparison\n * instead of expensive string operations on every check.\n *\n * A value of 0 means \"no cached SID is available\" for that context.\n * This covers both the initial \"not yet cached\" state and any case\n * where resolving the SID (e.g. via security_secctx_to_secid) failed.\n * In all such cases we intentionally fall back to the slower\n * string-based comparison path; this degrades performance only and\n * does not cause a functional failure.\n */\nstatic u32 cached_su_sid __read_mostly = 0;\nstatic u32 cached_zygote_sid __read_mostly = 0;\nstatic u32 cached_init_sid __read_mostly = 0;\nu32 ksu_file_sid __read_mostly = 0;\n\nstatic int transive_to_domain(const char *domain, struct cred *cred)\n{\n    u32 sid;\n    int error;\n#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 18, 0)\n    struct task_security_struct *tsec;\n#else\n    struct cred_security_struct *tsec;\n#endif\n    tsec = selinux_cred(cred);\n    if (!tsec) {\n        pr_err(\"tsec == NULL!\\n\");\n        return -1;\n    }\n    error = security_secctx_to_secid(domain, strlen(domain), &sid);\n    if (error) {\n        pr_info(\"security_secctx_to_secid %s -> sid: %d, error: %d\\n\", domain,\n                sid, error);\n    }\n    if (!error) {\n        tsec->sid = sid;\n        tsec->create_sid = 0;\n        tsec->keycreate_sid = 0;\n        tsec->sockcreate_sid = 0;\n    }\n    return error;\n}\n\nvoid setup_selinux(const char *domain, struct cred *cred)\n{\n    if (transive_to_domain(domain, cred)) {\n        pr_err(\"transive domain failed.\\n\");\n        return;\n    }\n}\n\nvoid setup_ksu_cred(void)\n{\n    if (ksu_cred && transive_to_domain(KERNEL_SU_CONTEXT, ksu_cred)) {\n        pr_err(\"setup ksu cred failed.\\n\");\n    }\n}\n\nvoid setenforce(bool enforce)\n{\n#ifdef CONFIG_SECURITY_SELINUX_DEVELOP\n    selinux_state.enforcing = enforce;\n#endif\n}\n\nbool getenforce(void)\n{\n#ifdef CONFIG_SECURITY_SELINUX_DISABLE\n    if (selinux_state.disabled) {\n        return false;\n    }\n#endif\n\n#ifdef CONFIG_SECURITY_SELINUX_DEVELOP\n    return selinux_state.enforcing;\n#else\n    return true;\n#endif\n}\n\n#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 14, 0)\nstruct lsm_context {\n    char *context;\n    u32 len;\n};\n\nstatic int __security_secid_to_secctx(u32 secid, struct lsm_context *cp)\n{\n    return security_secid_to_secctx(secid, &cp->context, &cp->len);\n}\nstatic void __security_release_secctx(struct lsm_context *cp)\n{\n    security_release_secctx(cp->context, cp->len);\n}\n#else\n#define __security_secid_to_secctx security_secid_to_secctx\n#define __security_release_secctx security_release_secctx\n#endif\n\n/*\n * Initialize cached SID values for frequently checked SELinux contexts.\n * Called once after SELinux policy is loaded (post-fs-data).\n * This eliminates expensive string comparisons in hot paths.\n */\nvoid cache_sid(void)\n{\n    int err;\n\n    err = security_secctx_to_secid(KERNEL_SU_CONTEXT, strlen(KERNEL_SU_CONTEXT),\n                                   &cached_su_sid);\n    if (err) {\n        pr_warn(\"Failed to cache kernel su domain SID: %d\\n\", err);\n        cached_su_sid = 0;\n    } else {\n        pr_info(\"Cached su SID: %u\\n\", cached_su_sid);\n    }\n\n    err = security_secctx_to_secid(ZYGOTE_CONTEXT, strlen(ZYGOTE_CONTEXT),\n                                   &cached_zygote_sid);\n    if (err) {\n        pr_warn(\"Failed to cache zygote SID: %d\\n\", err);\n        cached_zygote_sid = 0;\n    } else {\n        pr_info(\"Cached zygote SID: %u\\n\", cached_zygote_sid);\n    }\n\n    err = security_secctx_to_secid(INIT_CONTEXT, strlen(INIT_CONTEXT),\n                                   &cached_init_sid);\n    if (err) {\n        pr_warn(\"Failed to cache init SID: %d\\n\", err);\n        cached_init_sid = 0;\n    } else {\n        pr_info(\"Cached init SID: %u\\n\", cached_init_sid);\n    }\n\n    err = security_secctx_to_secid(KSU_FILE_CONTEXT, strlen(KSU_FILE_CONTEXT),\n                                   &ksu_file_sid);\n    if (err) {\n        pr_warn(\"Failed to cache ksu_file SID: %d\\n\", err);\n        ksu_file_sid = 0;\n    } else {\n        pr_info(\"Cached ksu_file SID: %u\\n\", ksu_file_sid);\n    }\n}\n\n/*\n * Fast path: compare task's SID directly against cached value.\n * Falls back to string comparison if cache is not initialized.\n */\nstatic bool is_sid_match(const struct cred *cred, u32 cached_sid,\n                         const char *fallback_context)\n{\n    if (!cred) {\n        return false;\n    }\n#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 18, 0)\n    const struct task_security_struct *tsec = selinux_cred(cred);\n#else\n    const struct cred_security_struct *tsec = selinux_cred(cred);\n#endif\n    if (!tsec) {\n        return false;\n    }\n\n    // Fast path: use cached SID if available\n    if (likely(cached_sid != 0)) {\n        return tsec->sid == cached_sid;\n    }\n\n    // Slow path fallback: string comparison (only before cache is initialized)\n    struct lsm_context ctx;\n    bool result;\n    if (__security_secid_to_secctx(tsec->sid, &ctx)) {\n        return false;\n    }\n    result = strncmp(fallback_context, ctx.context, ctx.len) == 0;\n    __security_release_secctx(&ctx);\n    return result;\n}\n\nbool is_task_ksu_domain(const struct cred *cred)\n{\n    return is_sid_match(cred, cached_su_sid, KERNEL_SU_CONTEXT);\n}\n\nbool is_ksu_domain(void)\n{\n    return is_task_ksu_domain(current_cred());\n}\n\nbool is_zygote(const struct cred *cred)\n{\n    return is_sid_match(cred, cached_zygote_sid, ZYGOTE_CONTEXT);\n}\n\nbool is_init(const struct cred *cred)\n{\n    return is_sid_match(cred, cached_init_sid, INIT_CONTEXT);\n}\n"
  },
  {
    "path": "kernel/selinux/selinux.h",
    "content": "#ifndef __KSU_H_SELINUX\n#define __KSU_H_SELINUX\n\n#include \"linux/types.h\"\n#include \"linux/version.h\"\n#include \"linux/cred.h\"\n\n// TODO: rename to \"ksu\"\n#define KERNEL_SU_DOMAIN \"su\"\n#define KERNEL_SU_FILE \"ksu_file\"\n\n#define KERNEL_SU_CONTEXT \"u:r:\" KERNEL_SU_DOMAIN \":s0\"\n#define KSU_FILE_CONTEXT \"u:object_r:\" KERNEL_SU_FILE \":s0\"\n#define ZYGOTE_CONTEXT \"u:r:zygote:s0\"\n#define INIT_CONTEXT \"u:r:init:s0\"\n\nvoid setup_selinux(const char *, struct cred *);\n\nvoid setenforce(bool);\n\nbool getenforce();\n\nvoid cache_sid(void);\n\nbool is_task_ksu_domain(const struct cred *cred);\n\nbool is_ksu_domain();\n\nbool is_zygote(const struct cred *cred);\n\nbool is_init(const struct cred *cred);\n\nvoid apply_kernelsu_rules();\n\nint handle_sepolicy(void __user *user_data, u64 data_len);\n\nvoid setup_ksu_cred();\n\n#endif\n"
  },
  {
    "path": "kernel/selinux/sepolicy.c",
    "content": "#include \"ss/avtab.h\"\n#include \"ss/constraint.h\"\n#include \"ss/ebitmap.h\"\n#include \"ss/hashtab.h\"\n#include \"ss/policydb.h\"\n#include \"ss/services.h\"\n#include <linux/gfp.h>\n#include <linux/printk.h>\n#include <linux/slab.h>\n#include <linux/version.h>\n\n#include \"sepolicy.h\"\n#include \"../klog.h\" // IWYU pragma: keep\n#include \"ss/symtab.h\"\n\n#define KSU_SUPPORT_ADD_TYPE\n\n//////////////////////////////////////////////////////\n// Declaration\n//////////////////////////////////////////////////////\n\nstatic struct avtab_node *get_avtab_node(struct policydb *db,\n                                         struct avtab_key *key,\n                                         struct avtab_extended_perms *xperms);\n\nstatic bool add_rule(struct policydb *db, const char *s, const char *t,\n                     const char *c, const char *p, int effect, bool invert);\n\nstatic void add_rule_raw(struct policydb *db, struct type_datum *src,\n                         struct type_datum *tgt, struct class_datum *cls,\n                         struct perm_datum *perm, int effect, bool invert);\n\nstatic void add_xperm_rule_raw(struct policydb *db, struct type_datum *src,\n                               struct type_datum *tgt, struct class_datum *cls,\n                               uint16_t low, uint16_t high, int effect,\n                               bool invert);\nstatic bool add_xperm_rule(struct policydb *db, const char *s, const char *t,\n                           const char *c, const char *range, int effect,\n                           bool invert);\n\nstatic bool add_type_rule(struct policydb *db, const char *s, const char *t,\n                          const char *c, const char *d, int effect);\n\nstatic bool add_filename_trans(struct policydb *db, const char *s,\n                               const char *t, const char *c, const char *d,\n                               const char *o);\n\nstatic bool add_genfscon(struct policydb *db, const char *fs_name,\n                         const char *path, const char *context);\n\nstatic bool add_type(struct policydb *db, const char *type_name, bool attr);\n\nstatic bool set_type_state(struct policydb *db, const char *type_name,\n                           bool permissive);\n\nstatic void add_typeattribute_raw(struct policydb *db, struct type_datum *type,\n                                  struct type_datum *attr);\n\nstatic bool add_typeattribute(struct policydb *db, const char *type,\n                              const char *attr);\n\n//////////////////////////////////////////////////////\n// Implementation\n//////////////////////////////////////////////////////\n\n// Invert is adding rules for auditdeny; in other cases, invert is removing\n// rules\n#define strip_av(effect, invert) ((effect == AVTAB_AUDITDENY) == !invert)\n\n#define ksu_hash_for_each(node_ptr, n_slot, cur)                               \\\n    int i;                                                                     \\\n    for (i = 0; i < n_slot; ++i)                                               \\\n        for (cur = node_ptr[i]; cur; cur = cur->next)\n\n// htable is a struct instead of pointer above 5.8.0:\n// https://elixir.bootlin.com/linux/v5.8-rc1/source/security/selinux/ss/symtab.h\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 8, 0)\n#define ksu_hashtab_for_each(htab, cur)                                        \\\n    ksu_hash_for_each(htab.htable, htab.size, cur)\n#else\n#define ksu_hashtab_for_each(htab, cur)                                        \\\n    ksu_hash_for_each(htab->htable, htab->size, cur)\n#endif\n\n// symtab_search is introduced on 5.9.0:\n// https://elixir.bootlin.com/linux/v5.9-rc1/source/security/selinux/ss/symtab.h\n#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 9, 0)\n#define symtab_search(s, name) hashtab_search((s)->table, name)\n#define symtab_insert(s, name, datum) hashtab_insert((s)->table, name, datum)\n#endif\n\n#define avtab_for_each(avtab, cur)                                             \\\n    ksu_hash_for_each(avtab.htable, avtab.nslot, cur);\n\nstatic struct avtab_node *get_avtab_node(struct policydb *db,\n                                         struct avtab_key *key,\n                                         struct avtab_extended_perms *xperms)\n{\n    struct avtab_node *node;\n\n    /* AVTAB_XPERMS entries are not necessarily unique */\n    if (key->specified & AVTAB_XPERMS) {\n        bool match = false;\n        node = avtab_search_node(&db->te_avtab, key);\n        while (node) {\n            if ((node->datum.u.xperms->specified == xperms->specified) &&\n                (node->datum.u.xperms->driver == xperms->driver)) {\n                match = true;\n                break;\n            }\n            node = avtab_search_node_next(node, key->specified);\n        }\n        if (!match)\n            node = NULL;\n    } else {\n        node = avtab_search_node(&db->te_avtab, key);\n    }\n\n    if (!node) {\n        struct avtab_datum avdatum = {};\n        /*\n     * AUDITDENY, aka DONTAUDIT, are &= assigned, versus |= for\n     * others. Initialize the data accordingly.\n     */\n        if (key->specified & AVTAB_XPERMS) {\n            avdatum.u.xperms = xperms;\n        } else {\n            avdatum.u.data = key->specified == AVTAB_AUDITDENY ? ~0U : 0U;\n        }\n        /* this is used to get the node - insertion is actually unique */\n        node = avtab_insert_nonunique(&db->te_avtab, key, &avdatum);\n\n        int grow_size = sizeof(struct avtab_key);\n        grow_size += sizeof(struct avtab_datum);\n        if (key->specified & AVTAB_XPERMS) {\n            grow_size += sizeof(u8);\n            grow_size += sizeof(u8);\n            grow_size += sizeof(u32) * ARRAY_SIZE(avdatum.u.xperms->perms.p);\n        }\n        db->len += grow_size;\n    }\n\n    return node;\n}\n\nstatic bool add_rule(struct policydb *db, const char *s, const char *t,\n                     const char *c, const char *p, int effect, bool invert)\n{\n    struct type_datum *src = NULL, *tgt = NULL;\n    struct class_datum *cls = NULL;\n    struct perm_datum *perm = NULL;\n\n    if (s) {\n        src = symtab_search(&db->p_types, s);\n        if (src == NULL) {\n            pr_info(\"source type %s does not exist\\n\", s);\n            return false;\n        }\n    }\n\n    if (t) {\n        tgt = symtab_search(&db->p_types, t);\n        if (tgt == NULL) {\n            pr_info(\"target type %s does not exist\\n\", t);\n            return false;\n        }\n    }\n\n    if (c) {\n        cls = symtab_search(&db->p_classes, c);\n        if (cls == NULL) {\n            pr_info(\"class %s does not exist\\n\", c);\n            return false;\n        }\n    }\n\n    if (p) {\n        if (c == NULL) {\n            pr_info(\"No class is specified, cannot add perm [%s] \\n\", p);\n            return false;\n        }\n\n        perm = symtab_search(&cls->permissions, p);\n        if (perm == NULL && cls->comdatum != NULL) {\n            perm = symtab_search(&cls->comdatum->permissions, p);\n        }\n        if (perm == NULL) {\n            pr_info(\"perm %s does not exist in class %s\\n\", p, c);\n            return false;\n        }\n    }\n    add_rule_raw(db, src, tgt, cls, perm, effect, invert);\n    return true;\n}\n\nstatic void add_rule_raw(struct policydb *db, struct type_datum *src,\n                         struct type_datum *tgt, struct class_datum *cls,\n                         struct perm_datum *perm, int effect, bool invert)\n{\n    if (src == NULL) {\n        struct hashtab_node *node;\n        if (strip_av(effect, invert)) {\n            ksu_hashtab_for_each(db->p_types.table, node)\n            {\n                add_rule_raw(db, (struct type_datum *)node->datum, tgt, cls,\n                             perm, effect, invert);\n            };\n        } else {\n            ksu_hashtab_for_each(db->p_types.table, node)\n            {\n                struct type_datum *type = (struct type_datum *)(node->datum);\n                if (type->attribute) {\n                    add_rule_raw(db, type, tgt, cls, perm, effect, invert);\n                }\n            };\n        }\n    } else if (tgt == NULL) {\n        struct hashtab_node *node;\n        if (strip_av(effect, invert)) {\n            ksu_hashtab_for_each(db->p_types.table, node)\n            {\n                add_rule_raw(db, src, (struct type_datum *)node->datum, cls,\n                             perm, effect, invert);\n            };\n        } else {\n            ksu_hashtab_for_each(db->p_types.table, node)\n            {\n                struct type_datum *type = (struct type_datum *)(node->datum);\n                if (type->attribute) {\n                    add_rule_raw(db, src, type, cls, perm, effect, invert);\n                }\n            };\n        }\n    } else if (cls == NULL) {\n        struct hashtab_node *node;\n        ksu_hashtab_for_each(db->p_classes.table, node)\n        {\n            add_rule_raw(db, src, tgt, (struct class_datum *)node->datum, perm,\n                         effect, invert);\n        }\n    } else {\n        struct avtab_key key;\n        key.source_type = src->value;\n        key.target_type = tgt->value;\n        key.target_class = cls->value;\n        key.specified = effect;\n\n        struct avtab_node *node = get_avtab_node(db, &key, NULL);\n        if (invert) {\n            if (perm)\n                node->datum.u.data &= ~(1U << (perm->value - 1));\n            else\n                node->datum.u.data = 0U;\n        } else {\n            if (perm)\n                node->datum.u.data |= 1U << (perm->value - 1);\n            else\n                node->datum.u.data = ~0U;\n        }\n    }\n}\n\n#define ioctl_driver(x) (x >> 8 & 0xFF)\n#define ioctl_func(x) (x & 0xFF)\n\n#define xperm_test(x, p) (1 & (p[x >> 5] >> (x & 0x1f)))\n#define xperm_set(x, p) (p[x >> 5] |= (1 << (x & 0x1f)))\n#define xperm_clear(x, p) (p[x >> 5] &= ~(1 << (x & 0x1f)))\n\nstatic void add_xperm_rule_raw(struct policydb *db, struct type_datum *src,\n                               struct type_datum *tgt, struct class_datum *cls,\n                               uint16_t low, uint16_t high, int effect,\n                               bool invert)\n{\n    if (src == NULL) {\n        struct hashtab_node *node;\n        ksu_hashtab_for_each(db->p_types.table, node)\n        {\n            struct type_datum *type = (struct type_datum *)(node->datum);\n            if (type->attribute) {\n                add_xperm_rule_raw(db, type, tgt, cls, low, high, effect,\n                                   invert);\n            }\n        };\n    } else if (tgt == NULL) {\n        struct hashtab_node *node;\n        ksu_hashtab_for_each(db->p_types.table, node)\n        {\n            struct type_datum *type = (struct type_datum *)(node->datum);\n            if (type->attribute) {\n                add_xperm_rule_raw(db, src, type, cls, low, high, effect,\n                                   invert);\n            }\n        };\n    } else if (cls == NULL) {\n        struct hashtab_node *node;\n        ksu_hashtab_for_each(db->p_classes.table, node)\n        {\n            add_xperm_rule_raw(db, src, tgt,\n                               (struct class_datum *)(node->datum), low, high,\n                               effect, invert);\n        };\n    } else {\n        struct avtab_key key;\n        key.source_type = src->value;\n        key.target_type = tgt->value;\n        key.target_class = cls->value;\n        key.specified = effect;\n\n        struct avtab_datum *datum;\n        struct avtab_node *node;\n        struct avtab_extended_perms xperms;\n\n        memset(&xperms, 0, sizeof(xperms));\n        if (ioctl_driver(low) != ioctl_driver(high)) {\n            xperms.specified = AVTAB_XPERMS_IOCTLDRIVER;\n            xperms.driver = 0;\n        } else {\n            xperms.specified = AVTAB_XPERMS_IOCTLFUNCTION;\n            xperms.driver = ioctl_driver(low);\n        }\n        int i;\n        if (xperms.specified == AVTAB_XPERMS_IOCTLDRIVER) {\n            for (i = ioctl_driver(low); i <= ioctl_driver(high); ++i) {\n                if (invert)\n                    xperm_clear(i, xperms.perms.p);\n                else\n                    xperm_set(i, xperms.perms.p);\n            }\n        } else {\n            for (i = ioctl_func(low); i <= ioctl_func(high); ++i) {\n                if (invert)\n                    xperm_clear(i, xperms.perms.p);\n                else\n                    xperm_set(i, xperms.perms.p);\n            }\n        }\n\n        node = get_avtab_node(db, &key, &xperms);\n        if (!node) {\n            pr_warn(\"add_xperm_rule_raw cannot found node!\\n\");\n            return;\n        }\n        datum = &node->datum;\n\n        if (datum->u.xperms == NULL) {\n            datum->u.xperms = (struct avtab_extended_perms *)(kzalloc(\n                sizeof(xperms), GFP_KERNEL));\n            if (!datum->u.xperms) {\n                pr_err(\"alloc xperms failed\\n\");\n                return;\n            }\n            memcpy(datum->u.xperms, &xperms, sizeof(xperms));\n        }\n    }\n}\n\nstatic bool add_xperm_rule(struct policydb *db, const char *s, const char *t,\n                           const char *c, const char *range, int effect,\n                           bool invert)\n{\n    struct type_datum *src = NULL, *tgt = NULL;\n    struct class_datum *cls = NULL;\n\n    if (s) {\n        src = symtab_search(&db->p_types, s);\n        if (src == NULL) {\n            pr_info(\"source type %s does not exist\\n\", s);\n            return false;\n        }\n    }\n\n    if (t) {\n        tgt = symtab_search(&db->p_types, t);\n        if (tgt == NULL) {\n            pr_info(\"target type %s does not exist\\n\", t);\n            return false;\n        }\n    }\n\n    if (c) {\n        cls = symtab_search(&db->p_classes, c);\n        if (cls == NULL) {\n            pr_info(\"class %s does not exist\\n\", c);\n            return false;\n        }\n    }\n\n    u16 low, high;\n\n    if (range) {\n        if (strchr(range, '-')) {\n            sscanf(range, \"%hx-%hx\", &low, &high);\n        } else {\n            sscanf(range, \"%hx\", &low);\n            high = low;\n        }\n    } else {\n        low = 0;\n        high = 0xFFFF;\n    }\n\n    add_xperm_rule_raw(db, src, tgt, cls, low, high, effect, invert);\n    return true;\n}\n\nstatic bool add_type_rule(struct policydb *db, const char *s, const char *t,\n                          const char *c, const char *d, int effect)\n{\n    struct type_datum *src, *tgt, *def;\n    struct class_datum *cls;\n\n    src = symtab_search(&db->p_types, s);\n    if (src == NULL) {\n        pr_info(\"source type %s does not exist\\n\", s);\n        return false;\n    }\n    tgt = symtab_search(&db->p_types, t);\n    if (tgt == NULL) {\n        pr_info(\"target type %s does not exist\\n\", t);\n        return false;\n    }\n    cls = symtab_search(&db->p_classes, c);\n    if (cls == NULL) {\n        pr_info(\"class %s does not exist\\n\", c);\n        return false;\n    }\n    def = symtab_search(&db->p_types, d);\n    if (def == NULL) {\n        pr_info(\"default type %s does not exist\\n\", d);\n        return false;\n    }\n\n    struct avtab_key key;\n    key.source_type = src->value;\n    key.target_type = tgt->value;\n    key.target_class = cls->value;\n    key.specified = effect;\n\n    struct avtab_node *node = get_avtab_node(db, &key, NULL);\n    node->datum.u.data = def->value;\n\n    return true;\n}\n\n// 5.9.0 : static inline int hashtab_insert(struct hashtab *h, void *key, void\n// *datum, struct hashtab_key_params key_params) 5.8.0: int\n// hashtab_insert(struct hashtab *h, void *k, void *d);\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0)\nstatic u32 filenametr_hash(const void *k)\n{\n    const struct filename_trans_key *ft = k;\n    unsigned long hash;\n    unsigned int byte_num;\n    unsigned char focus;\n\n    hash = ft->ttype ^ ft->tclass;\n\n    byte_num = 0;\n    while ((focus = ft->name[byte_num++]))\n        hash = partial_name_hash(focus, hash);\n    return hash;\n}\n\nstatic int filenametr_cmp(const void *k1, const void *k2)\n{\n    const struct filename_trans_key *ft1 = k1;\n    const struct filename_trans_key *ft2 = k2;\n    int v;\n\n    v = ft1->ttype - ft2->ttype;\n    if (v)\n        return v;\n\n    v = ft1->tclass - ft2->tclass;\n    if (v)\n        return v;\n\n    return strcmp(ft1->name, ft2->name);\n}\n\nstatic const struct hashtab_key_params filenametr_key_params = {\n    .hash = filenametr_hash,\n    .cmp = filenametr_cmp,\n};\n#endif\n\nstatic bool add_filename_trans(struct policydb *db, const char *s,\n                               const char *t, const char *c, const char *d,\n                               const char *o)\n{\n    struct type_datum *src, *tgt, *def;\n    struct class_datum *cls;\n\n    src = symtab_search(&db->p_types, s);\n    if (src == NULL) {\n        pr_warn(\"source type %s does not exist\\n\", s);\n        return false;\n    }\n    tgt = symtab_search(&db->p_types, t);\n    if (tgt == NULL) {\n        pr_warn(\"target type %s does not exist\\n\", t);\n        return false;\n    }\n    cls = symtab_search(&db->p_classes, c);\n    if (cls == NULL) {\n        pr_warn(\"class %s does not exist\\n\", c);\n        return false;\n    }\n    def = symtab_search(&db->p_types, d);\n    if (def == NULL) {\n        pr_warn(\"default type %s does not exist\\n\", d);\n        return false;\n    }\n\n    struct filename_trans_key key;\n    key.ttype = tgt->value;\n    key.tclass = cls->value;\n    key.name = (char *)o;\n\n    struct filename_trans_datum *last = NULL;\n\n    struct filename_trans_datum *trans = policydb_filenametr_search(db, &key);\n    while (trans) {\n        if (ebitmap_get_bit(&trans->stypes, src->value - 1)) {\n            // Duplicate, overwrite existing data and return\n            trans->otype = def->value;\n            return true;\n        }\n        if (trans->otype == def->value)\n            break;\n        last = trans;\n        trans = trans->next;\n    }\n\n    if (trans == NULL) {\n        trans = (struct filename_trans_datum *)kcalloc(1, sizeof(*trans),\n                                                       GFP_KERNEL);\n        struct filename_trans_key *new_key =\n            (struct filename_trans_key *)kzalloc(sizeof(*new_key), GFP_KERNEL);\n        *new_key = key;\n        new_key->name = kstrdup(key.name, GFP_KERNEL);\n        trans->next = last;\n        trans->otype = def->value;\n        hashtab_insert(&db->filename_trans, new_key, trans,\n                       filenametr_key_params);\n    }\n\n    db->compat_filename_trans_count++;\n    return ebitmap_set_bit(&trans->stypes, src->value - 1, 1) == 0;\n}\n\nstatic bool add_genfscon(struct policydb *db, const char *fs_name,\n                         const char *path, const char *context)\n{\n    return false;\n}\n\n// https://github.com/torvalds/linux/commit/590b9d576caec6b4c46bba49ed36223a399c3fc5#diff-cc9aa90e094e6e0f47bd7300db4f33cf4366b98b55d8753744f31eb69c691016R844-R845\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0)\n#define ksu_kvrealloc(p, new_size, _old_size) kvrealloc(p, new_size, GFP_KERNEL)\n// https://github.com/torvalds/linux/commit/de2860f4636256836450c6543be744a50118fc66#diff-fa19cdd9c3369d7f59aa2e8404628109408dbf8e1b568d1157a27328f75b8410R638-R652\n#elif LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0)\n#define ksu_kvrealloc(p, new_size, old_size)                                   \\\n    kvrealloc(p, old_size, new_size, GFP_KERNEL)\n#else\n// https://cs.android.com/android/_/android/kernel/common/+/f5f3e54f811679761c33526e695bd296190faade\n// Some 5.10 kernel don't have this backport, so copy one.\nvoid *ksu_kvrealloc_compat(const void *p, size_t oldsize, size_t newsize,\n                           gfp_t flags)\n{\n    void *newp;\n\n    if (oldsize >= newsize)\n        return (void *)p;\n    newp = kvmalloc(newsize, flags);\n    if (!newp)\n        return NULL;\n    memcpy(newp, p, oldsize);\n    kvfree(p);\n    return newp;\n}\n#define ksu_kvrealloc(p, new_size, old_size)                                   \\\n    ksu_kvrealloc_compat(p, old_size, new_size, GFP_KERNEL)\n#endif\n\nstatic bool add_type(struct policydb *db, const char *type_name, bool attr)\n{\n    struct type_datum *type = symtab_search(&db->p_types, type_name);\n    if (type) {\n        pr_warn(\"Type %s already exists\\n\", type_name);\n        return true;\n    }\n\n    u32 value = ++db->p_types.nprim;\n    type = (struct type_datum *)kzalloc(sizeof(struct type_datum), GFP_KERNEL);\n    if (!type) {\n        pr_err(\"add_type: alloc type_datum failed.\\n\");\n        return false;\n    }\n\n    type->primary = 1;\n    type->value = value;\n    type->attribute = attr;\n\n    char *key = kstrdup(type_name, GFP_KERNEL);\n    if (!key) {\n        pr_err(\"add_type: alloc key failed.\\n\");\n        return false;\n    }\n\n    if (symtab_insert(&db->p_types, key, type)) {\n        pr_err(\"add_type: insert symtab failed.\\n\");\n        return false;\n    }\n\n    struct ebitmap *new_type_attr_map_array =\n        ksu_kvrealloc(db->type_attr_map_array, value * sizeof(struct ebitmap),\n                      (value - 1) * sizeof(struct ebitmap));\n\n    if (!new_type_attr_map_array) {\n        pr_err(\"add_type: alloc type_attr_map_array failed\\n\");\n        return false;\n    }\n\n    struct type_datum **new_type_val_to_struct =\n        ksu_kvrealloc(db->type_val_to_struct,\n                      sizeof(*db->type_val_to_struct) * value,\n                      sizeof(*db->type_val_to_struct) * (value - 1));\n\n    if (!new_type_val_to_struct) {\n        pr_err(\"add_type: alloc type_val_to_struct failed\\n\");\n        return false;\n    }\n\n    char **new_val_to_name_types =\n        ksu_kvrealloc(db->sym_val_to_name[SYM_TYPES], sizeof(char *) * value,\n                      sizeof(char *) * (value - 1));\n    if (!new_val_to_name_types) {\n        pr_err(\"add_type: alloc val_to_name failed\\n\");\n        return false;\n    }\n\n    db->type_attr_map_array = new_type_attr_map_array;\n    ebitmap_init(&db->type_attr_map_array[value - 1]);\n    ebitmap_set_bit(&db->type_attr_map_array[value - 1], value - 1, 1);\n\n    db->type_val_to_struct = new_type_val_to_struct;\n    db->type_val_to_struct[value - 1] = type;\n\n    db->sym_val_to_name[SYM_TYPES] = new_val_to_name_types;\n    db->sym_val_to_name[SYM_TYPES][value - 1] = key;\n\n    int i;\n    for (i = 0; i < db->p_roles.nprim; ++i) {\n        ebitmap_set_bit(&db->role_val_to_struct[i]->types, value - 1, 1);\n    }\n\n    return true;\n}\n\nstatic bool set_type_state(struct policydb *db, const char *type_name,\n                           bool permissive)\n{\n    struct type_datum *type;\n    if (type_name == NULL) {\n        struct hashtab_node *node;\n        ksu_hashtab_for_each(db->p_types.table, node)\n        {\n            type = (struct type_datum *)(node->datum);\n            if (ebitmap_set_bit(&db->permissive_map, type->value, permissive))\n                pr_info(\"Could not set bit in permissive map\\n\");\n        };\n    } else {\n        type = (struct type_datum *)symtab_search(&db->p_types, type_name);\n        if (type == NULL) {\n            pr_info(\"type %s does not exist\\n\", type_name);\n            return false;\n        }\n        if (ebitmap_set_bit(&db->permissive_map, type->value, permissive)) {\n            pr_info(\"Could not set bit in permissive map\\n\");\n            return false;\n        }\n    }\n    return true;\n}\n\nstatic void add_typeattribute_raw(struct policydb *db, struct type_datum *type,\n                                  struct type_datum *attr)\n{\n    struct ebitmap *sattr = &db->type_attr_map_array[type->value - 1];\n    ebitmap_set_bit(sattr, attr->value - 1, 1);\n\n    struct hashtab_node *node;\n    struct constraint_node *n;\n    struct constraint_expr *e;\n    ksu_hashtab_for_each(db->p_classes.table, node)\n    {\n        struct class_datum *cls = (struct class_datum *)(node->datum);\n        for (n = cls->constraints; n; n = n->next) {\n            for (e = n->expr; e; e = e->next) {\n                if (e->expr_type == CEXPR_NAMES &&\n                    ebitmap_get_bit(&e->type_names->types, attr->value - 1)) {\n                    ebitmap_set_bit(&e->names, type->value - 1, 1);\n                }\n            }\n        }\n    };\n}\n\nstatic bool add_typeattribute(struct policydb *db, const char *type,\n                              const char *attr)\n{\n    struct type_datum *type_d = symtab_search(&db->p_types, type);\n    if (type_d == NULL) {\n        pr_info(\"type %s does not exist\\n\", type);\n        return false;\n    } else if (type_d->attribute) {\n        pr_info(\"type %s is an attribute\\n\", attr);\n        return false;\n    }\n\n    struct type_datum *attr_d = symtab_search(&db->p_types, attr);\n    if (attr_d == NULL) {\n        pr_info(\"attribute %s does not exist\\n\", type);\n        return false;\n    } else if (!attr_d->attribute) {\n        pr_info(\"type %s is not an attribute \\n\", attr);\n        return false;\n    }\n\n    add_typeattribute_raw(db, type_d, attr_d);\n    return true;\n}\n\n//////////////////////////////////////////////////////////////////////////\n\n// Operation on types\nbool ksu_type(struct policydb *db, const char *name, const char *attr)\n{\n    return add_type(db, name, false) && add_typeattribute(db, name, attr);\n}\n\nbool ksu_attribute(struct policydb *db, const char *name)\n{\n    return add_type(db, name, true);\n}\n\nbool ksu_permissive(struct policydb *db, const char *type)\n{\n    return set_type_state(db, type, true);\n}\n\nbool ksu_enforce(struct policydb *db, const char *type)\n{\n    return set_type_state(db, type, false);\n}\n\nbool ksu_typeattribute(struct policydb *db, const char *type, const char *attr)\n{\n    return add_typeattribute(db, type, attr);\n}\n\nbool ksu_exists(struct policydb *db, const char *type)\n{\n    return symtab_search(&db->p_types, type) != NULL;\n}\n\n// Access vector rules\nbool ksu_allow(struct policydb *db, const char *src, const char *tgt,\n               const char *cls, const char *perm)\n{\n    return add_rule(db, src, tgt, cls, perm, AVTAB_ALLOWED, false);\n}\n\nbool ksu_deny(struct policydb *db, const char *src, const char *tgt,\n              const char *cls, const char *perm)\n{\n    return add_rule(db, src, tgt, cls, perm, AVTAB_ALLOWED, true);\n}\n\nbool ksu_auditallow(struct policydb *db, const char *src, const char *tgt,\n                    const char *cls, const char *perm)\n{\n    return add_rule(db, src, tgt, cls, perm, AVTAB_AUDITALLOW, false);\n}\nbool ksu_dontaudit(struct policydb *db, const char *src, const char *tgt,\n                   const char *cls, const char *perm)\n{\n    return add_rule(db, src, tgt, cls, perm, AVTAB_AUDITDENY, true);\n}\n\n// Extended permissions access vector rules\nbool ksu_allowxperm(struct policydb *db, const char *src, const char *tgt,\n                    const char *cls, const char *range)\n{\n    return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_ALLOWED,\n                          false);\n}\n\nbool ksu_auditallowxperm(struct policydb *db, const char *src, const char *tgt,\n                         const char *cls, const char *range)\n{\n    return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_AUDITALLOW,\n                          false);\n}\n\nbool ksu_dontauditxperm(struct policydb *db, const char *src, const char *tgt,\n                        const char *cls, const char *range)\n{\n    return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_DONTAUDIT,\n                          false);\n}\n\n// Type rules\nbool ksu_type_transition(struct policydb *db, const char *src, const char *tgt,\n                         const char *cls, const char *def, const char *obj)\n{\n    if (obj) {\n        return add_filename_trans(db, src, tgt, cls, def, obj);\n    } else {\n        return add_type_rule(db, src, tgt, cls, def, AVTAB_TRANSITION);\n    }\n}\n\nbool ksu_type_change(struct policydb *db, const char *src, const char *tgt,\n                     const char *cls, const char *def)\n{\n    return add_type_rule(db, src, tgt, cls, def, AVTAB_CHANGE);\n}\n\nbool ksu_type_member(struct policydb *db, const char *src, const char *tgt,\n                     const char *cls, const char *def)\n{\n    return add_type_rule(db, src, tgt, cls, def, AVTAB_MEMBER);\n}\n\n// File system labeling\nbool ksu_genfscon(struct policydb *db, const char *fs_name, const char *path,\n                  const char *ctx)\n{\n    return add_genfscon(db, fs_name, path, ctx);\n}\n\n// https://github.com/torvalds/linux/commit/581646c3fb98494009671f6d347ea125bc0e663a\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 10, 0)\n#define CONST_IF_6_10 const\n#else\n#define CONST_IF_6_10\n#endif\n\n// ======== begin copy ========\n\nstatic int copy_hashtab_node(struct hashtab_node *new_node,\n                             CONST_IF_6_10 struct hashtab_node *old_node,\n                             void *data)\n{\n    new_node->datum = old_node->datum;\n    new_node->key = old_node->key;\n    return 0;\n}\n\nstatic int destroy_hashtab_node(void *key, void *datum, void *data)\n{\n    // just copied pointer, no need to free\n    return 0;\n}\n\nstatic int shallow_copy_hashtab(struct hashtab *new_tab,\n                                struct hashtab *old_tab)\n{\n    return hashtab_duplicate(new_tab, old_tab, copy_hashtab_node,\n                             destroy_hashtab_node, NULL);\n}\n\n// ======== class_datum ========\n\nstatic int\ncopy_class_datum_partially_callback(struct hashtab_node *new_node,\n                                    CONST_IF_6_10 struct hashtab_node *old_node,\n                                    void *data)\n{\n    struct policydb *db = data;\n    struct class_datum *cls = old_node->datum, *new_cls;\n    struct constraint_node *oldn, *n, *nprev = NULL;\n    struct constraint_expr *olde, *e, *eprev;\n    new_node->key = old_node->key;\n    new_cls = kmemdup(cls, sizeof(struct class_datum), GFP_KERNEL);\n    if (!new_cls)\n        return -ENOMEM;\n    new_node->datum = new_cls;\n    new_cls->constraints = NULL;\n    for (oldn = cls->constraints; oldn; oldn = oldn->next) {\n        n = kmemdup(oldn, sizeof(struct constraint_node), GFP_KERNEL);\n        if (!n)\n            goto out_nomem;\n        if (nprev) {\n            nprev->next = n;\n        } else {\n            new_cls->constraints = n;\n        }\n        eprev = NULL;\n        n->expr = NULL;\n        for (olde = oldn->expr; olde; olde = olde->next) {\n            e = kmemdup(olde, sizeof(struct constraint_expr), GFP_KERNEL);\n            if (!e) {\n                goto out_nomem;\n            }\n            if (eprev) {\n                eprev->next = e;\n            } else {\n                n->expr = e;\n            }\n            if (olde->expr_type == CEXPR_NAMES) {\n                if (ebitmap_cpy(&e->names, &olde->names) < 0) {\n                    goto out_nomem;\n                }\n            }\n            eprev = e;\n        }\n        nprev = n;\n    }\n\n    db->class_val_to_struct[new_cls->value - 1] = new_cls;\n\n    return 0;\nout_nomem:\n    return -ENOMEM;\n}\n\nstatic int destroy_class_datum_partially_callback(void *key, void *datum,\n                                                  void *data)\n{\n    struct class_datum *cls = datum;\n    struct constraint_node *n, *nprev;\n    struct constraint_expr *e, *eprev;\n    if (cls) {\n        for (n = cls->constraints; n;) {\n            for (e = n->expr; e;) {\n                if (e->expr_type == CEXPR_NAMES) {\n                    ebitmap_destroy(&e->names);\n                }\n                eprev = e;\n                e = e->next;\n                kfree(eprev);\n            }\n            nprev = n;\n            n = n->next;\n            kfree(nprev);\n        }\n    }\n    kfree(cls);\n\n    return 0;\n}\n\nstatic void free_class_datum_partially(struct policydb *db)\n{\n    if (db->class_val_to_struct) {\n        kfree(db->class_val_to_struct);\n    }\n\n    if (db->p_classes.table.htable) {\n        hashtab_map(&db->p_classes.table,\n                    destroy_class_datum_partially_callback, NULL);\n        hashtab_destroy(&db->p_classes.table);\n    }\n}\n\nstatic int copy_class_datum_partially(struct policydb *new_db,\n                                      struct policydb *old_db)\n{\n    int ret;\n    u32 n = new_db->symtab[SYM_CLASSES].nprim;\n    struct class_datum **new_class_val_to_struct;\n\n    new_db->class_val_to_struct = NULL;\n    memset(&new_db->p_classes.table, 0, sizeof(new_db->p_classes.table));\n\n    new_class_val_to_struct =\n        kcalloc(n, sizeof(struct class_datum *), GFP_KERNEL);\n    if (!new_class_val_to_struct) {\n        ret = -ENOMEM;\n        goto exit;\n    }\n    new_db->class_val_to_struct = new_class_val_to_struct;\n\n    ret = hashtab_duplicate(&new_db->p_classes.table, &old_db->p_classes.table,\n                            copy_class_datum_partially_callback,\n                            destroy_class_datum_partially_callback, new_db);\n\n    if (ret) {\n        goto exit;\n    }\n\n    return 0;\n\nexit:\n    free_class_datum_partially(new_db);\n    return ret;\n}\n\n// ======== avtab ========\n\nstatic int copy_avtab(struct avtab *new_avtab, struct avtab *old_avtab)\n{\n    int ret, i;\n    struct avtab_node *n, *p;\n    ret = avtab_alloc_dup(new_avtab, old_avtab);\n    if (ret < 0)\n        return ret;\n\n    for (i = 0; i < old_avtab->nslot; i++) {\n        n = old_avtab->htable[i];\n        while (n) {\n            p = avtab_insert_nonunique(new_avtab, &n->key, &n->datum);\n            if (!p) {\n                ret = -ENOMEM;\n                goto out_free;\n            }\n            n = n->next;\n        }\n    }\n\n    return 0;\n\nout_free:\n    avtab_destroy(new_avtab);\n    return ret;\n}\n\n// ======== role_datum ========\n\nstatic int\ncopy_role_datum_partially_callback(struct hashtab_node *new_node,\n                                   CONST_IF_6_10 struct hashtab_node *old_node,\n                                   void *data)\n{\n    int ret = 0;\n    struct policydb *db = data;\n    struct role_datum *role = old_node->datum, *new_role;\n    new_role = kmemdup(role, sizeof(struct role_datum), GFP_KERNEL);\n    if (!new_role) {\n        ret = -ENOMEM;\n        goto out;\n    }\n    new_node->datum = new_role;\n    new_node->key = old_node->key;\n\n    ret = ebitmap_cpy(&new_role->types, &role->types);\n    if (ret) {\n        goto out;\n    }\n    db->role_val_to_struct[role->value - 1] = new_role;\n\nout:\n    return ret;\n}\n\nstatic int destroy_role_datum_partially_callback(void *key, void *datum,\n                                                 void *data)\n{\n    struct role_datum *role = datum;\n    if (role) {\n        ebitmap_destroy(&role->types);\n        kfree(role);\n    }\n    return 0;\n}\n\nstatic void free_role_datum_partially(struct policydb *db)\n{\n    if (db->role_val_to_struct) {\n        kfree(db->role_val_to_struct);\n    }\n    if (db->p_roles.table.htable) {\n        hashtab_map(&db->p_roles.table, destroy_role_datum_partially_callback,\n                    NULL);\n        hashtab_destroy(&db->p_roles.table);\n    }\n}\n\nstatic int copy_role_datum_partially(struct policydb *new_db,\n                                     struct policydb *old_db)\n{\n    int ret;\n    struct role_datum **new_role_val_to_struct;\n    u32 n = old_db->p_roles.nprim;\n\n    new_db->role_val_to_struct = NULL;\n    memset(&new_db->p_roles.table, 0, sizeof(new_db->p_roles.table));\n\n    new_role_val_to_struct =\n        kcalloc(n, sizeof(*new_db->role_val_to_struct), GFP_KERNEL);\n    if (!new_role_val_to_struct) {\n        ret = -ENOMEM;\n        goto out_free;\n    }\n    new_db->role_val_to_struct = new_role_val_to_struct;\n\n    ret = hashtab_duplicate(&new_db->p_roles.table, &old_db->p_roles.table,\n                            copy_role_datum_partially_callback,\n                            destroy_role_datum_partially_callback, new_db);\n    if (ret)\n        goto out_free;\n    return 0;\n\nout_free:\n    free_role_datum_partially(new_db);\n\n    return ret;\n}\n\n// ======== type_datum ========\n\nstatic void free_type_datum_partially(struct policydb *db)\n{\n    u32 sz = db->p_types.nprim, i;\n    if (db->type_attr_map_array) {\n        for (i = 0; i < sz; i++) {\n            ebitmap_destroy(&db->type_attr_map_array[i]);\n        }\n\n        kvfree(db->type_attr_map_array);\n    }\n\n    if (db->type_val_to_struct) {\n        kvfree(db->type_val_to_struct);\n    }\n\n    if (db->sym_val_to_name[SYM_TYPES]) {\n        kvfree(db->sym_val_to_name[SYM_TYPES]);\n    }\n\n    hashtab_destroy(&db->p_types.table);\n}\n\nstatic int copy_type_datum_partially(struct policydb *new_db,\n                                     struct policydb *old_db)\n{\n    int ret = -ENOMEM;\n    u32 sz = new_db->p_types.nprim, i;\n    struct ebitmap *new_type_attr_map_array;\n    struct type_datum **new_type_val_to_struct;\n    char **new_sym_val_to_name_types;\n\n    new_db->type_attr_map_array = NULL;\n    new_db->type_val_to_struct = NULL;\n    new_db->sym_val_to_name[SYM_TYPES] = NULL;\n    memset(&new_db->p_types.table, 0, sizeof(new_db->p_types.table));\n\n    // ======== type_attr_map_array ========\n\n    new_type_attr_map_array = kvcalloc(sz, sizeof(struct ebitmap), GFP_KERNEL);\n\n    if (!new_type_attr_map_array) {\n        goto out;\n    }\n\n    new_db->type_attr_map_array = new_type_attr_map_array;\n    for (i = 0; i < sz; i++) {\n        ret = ebitmap_cpy(&new_db->type_attr_map_array[i],\n                          &old_db->type_attr_map_array[i]);\n        if (ret < 0)\n            goto out;\n    }\n\n    // ======== type_val_to_struct ========\n    ret = -ENOMEM;\n\n    new_type_val_to_struct =\n        kvcalloc(sz, sizeof(*new_db->type_val_to_struct), GFP_KERNEL);\n    if (!new_type_val_to_struct) {\n        goto out;\n    }\n    new_db->type_val_to_struct = new_type_val_to_struct;\n    memcpy(new_db->type_val_to_struct, old_db->type_val_to_struct,\n           sz * sizeof(*new_db->type_val_to_struct));\n\n    // ======== sym_val_to_name[SYM_TYPES] ========\n\n    new_sym_val_to_name_types =\n        kvcalloc(sz, sizeof(*new_db->sym_val_to_name[SYM_TYPES]), GFP_KERNEL);\n    if (!new_sym_val_to_name_types)\n        goto out;\n    new_db->sym_val_to_name[SYM_TYPES] = new_sym_val_to_name_types;\n    memcpy(new_db->sym_val_to_name[SYM_TYPES],\n           old_db->sym_val_to_name[SYM_TYPES],\n           sz * sizeof(*new_db->sym_val_to_name[SYM_TYPES]));\n\n    // ======== p_types ========\n\n    ret = shallow_copy_hashtab(&new_db->p_types.table, &old_db->p_types.table);\n    if (ret < 0)\n        goto out;\n\n    return 0;\nout:\n    free_type_datum_partially(new_db);\n    return ret;\n}\n\n// ======== permissive_map ========\n\nstatic void free_permissive_map(struct policydb *db)\n{\n    ebitmap_destroy(&db->permissive_map);\n}\n\nstatic int copy_permissive_map(struct policydb *new_db, struct policydb *old_db)\n{\n    // On failure, the old ebitmap is cleaned.\n    return ebitmap_cpy(&new_db->permissive_map, &old_db->permissive_map);\n}\n\n// ======== filename_trans ========\n\nstatic void free_filename_trans(struct policydb *db)\n{\n    hashtab_destroy(&db->filename_trans);\n}\n\nstatic int copy_filename_trans(struct policydb *new_db, struct policydb *old_db)\n{\n    // On failure, the old hashtab is cleaned.\n    return shallow_copy_hashtab(&new_db->filename_trans,\n                                &old_db->filename_trans);\n}\n\n// ======== sepolicy ========\n\nvoid ksu_destroy_sepolicy(struct selinux_policy *pol)\n{\n    if (!pol)\n        return;\n\n    struct policydb *db = &pol->policydb;\n\n    free_class_datum_partially(db);\n\n    avtab_destroy(&db->te_avtab);\n\n    free_role_datum_partially(db);\n\n    free_type_datum_partially(db);\n\n    free_permissive_map(db);\n\n    free_filename_trans(db);\n\n    kfree(pol);\n}\n\nstruct selinux_policy *ksu_dup_sepolicy(struct selinux_policy *old_pol)\n{\n    int ret;\n    struct selinux_policy *new_pol =\n        kmemdup(old_pol, sizeof(*old_pol), GFP_KERNEL);\n    if (!new_pol) {\n        return NULL;\n    }\n    struct policydb *new_db = &new_pol->policydb, *old_db = &old_pol->policydb;\n\n    ret = copy_class_datum_partially(new_db, old_db);\n    if (ret < 0) {\n        pr_err(\"ksu_dup_sepolicy: copy_class_datum_partially\\n\");\n        goto out;\n    }\n\n    ret = copy_avtab(&new_db->te_avtab, &old_db->te_avtab);\n    if (ret < 0) {\n        pr_err(\"ksu_dup_sepolicy: copy_avtab\\n\");\n        goto out;\n    }\n\n    ret = copy_role_datum_partially(new_db, old_db);\n    if (ret < 0) {\n        pr_err(\"ksu_dup_sepolicy: copy_role_datum_partially\\n\");\n        goto out;\n    }\n\n    ret = copy_type_datum_partially(new_db, old_db);\n    if (ret < 0) {\n        pr_err(\"ksu_dup_sepolicy: copy_type_datum_partially\\n\");\n        goto out;\n    }\n\n    ret = copy_permissive_map(new_db, old_db);\n    if (ret < 0) {\n        pr_err(\"ksu_dup_sepolicy: copy_permissive_map\\n\");\n        goto out;\n    }\n\n    ret = copy_filename_trans(new_db, old_db);\n    if (ret < 0) {\n        pr_err(\"ksu_dup_sepolicy: copy_filename_trans\\n\");\n        goto out;\n    }\n\n    return new_pol;\n\nout:\n    kfree(new_pol);\n    return NULL;\n}\n"
  },
  {
    "path": "kernel/selinux/sepolicy.h",
    "content": "#ifndef __KSU_H_SEPOLICY\n#define __KSU_H_SEPOLICY\n\n#include <linux/types.h>\n\n#include \"ss/policydb.h\"\n\nstruct selinux_policy *ksu_dup_sepolicy(struct selinux_policy *old_pol);\n\nvoid ksu_destroy_sepolicy(struct selinux_policy *orig);\n\n// Operation on types\nbool ksu_type(struct policydb *db, const char *name, const char *attr);\nbool ksu_attribute(struct policydb *db, const char *name);\nbool ksu_permissive(struct policydb *db, const char *type);\nbool ksu_enforce(struct policydb *db, const char *type);\nbool ksu_typeattribute(struct policydb *db, const char *type, const char *attr);\nbool ksu_exists(struct policydb *db, const char *type);\n\n// Access vector rules\nbool ksu_allow(struct policydb *db, const char *src, const char *tgt,\n               const char *cls, const char *perm);\nbool ksu_deny(struct policydb *db, const char *src, const char *tgt,\n              const char *cls, const char *perm);\nbool ksu_auditallow(struct policydb *db, const char *src, const char *tgt,\n                    const char *cls, const char *perm);\nbool ksu_dontaudit(struct policydb *db, const char *src, const char *tgt,\n                   const char *cls, const char *perm);\n\n// Extended permissions access vector rules\nbool ksu_allowxperm(struct policydb *db, const char *src, const char *tgt,\n                    const char *cls, const char *range);\nbool ksu_auditallowxperm(struct policydb *db, const char *src, const char *tgt,\n                         const char *cls, const char *range);\nbool ksu_dontauditxperm(struct policydb *db, const char *src, const char *tgt,\n                        const char *cls, const char *range);\n\n// Type rules\nbool ksu_type_transition(struct policydb *db, const char *src, const char *tgt,\n                         const char *cls, const char *def, const char *obj);\nbool ksu_type_change(struct policydb *db, const char *src, const char *tgt,\n                     const char *cls, const char *def);\nbool ksu_type_member(struct policydb *db, const char *src, const char *tgt,\n                     const char *cls, const char *def);\n\n// File system labeling\nbool ksu_genfscon(struct policydb *db, const char *fs_name, const char *path,\n                  const char *ctx);\n\n#endif\n"
  },
  {
    "path": "kernel/setuid_hook.c",
    "content": "#include <linux/compiler.h>\n#include <linux/version.h>\n#include <linux/slab.h>\n#include <linux/task_work.h>\n#include <linux/thread_info.h>\n#include <linux/seccomp.h>\n#include <linux/printk.h>\n#include <linux/sched.h>\n#include <linux/sched/signal.h>\n#include <linux/string.h>\n#include <linux/types.h>\n#include <linux/uaccess.h>\n#include <linux/uidgid.h>\n\n#include \"allowlist.h\"\n#include \"setuid_hook.h\"\n#include \"klog.h\" // IWYU pragma: keep\n#include \"manager.h\"\n#include \"selinux/selinux.h\"\n#include \"seccomp_cache.h\"\n#include \"supercalls.h\"\n#include \"syscall_hook_manager.h\"\n#include \"kernel_umount.h\"\n\nstatic void ksu_install_manager_fd_tw_func(struct callback_head *cb)\n{\n    ksu_install_fd();\n    kfree(cb);\n}\n\nint ksu_handle_setresuid(uid_t ruid, uid_t euid, uid_t suid)\n{\n    // we rely on the fact that zygote always call setresuid(3) with same uids\n    uid_t new_uid = ruid;\n    uid_t old_uid = current_uid().val;\n\n    pr_info(\"handle_setresuid from %d to %d\\n\", old_uid, new_uid);\n\n    if (likely(ksu_is_manager_appid_valid()) &&\n        unlikely(ksu_get_manager_appid() == new_uid % PER_USER_RANGE)) {\n        spin_lock_irq(&current->sighand->siglock);\n        ksu_seccomp_allow_cache(current->seccomp.filter, __NR_reboot);\n        ksu_set_task_tracepoint_flag(current);\n        spin_unlock_irq(&current->sighand->siglock);\n\n        pr_info(\"install fd for manager: %d\\n\", new_uid);\n        struct callback_head *cb = kzalloc(sizeof(*cb), GFP_ATOMIC);\n        if (!cb)\n            return 0;\n        cb->func = ksu_install_manager_fd_tw_func;\n        if (task_work_add(current, cb, TWA_RESUME)) {\n            kfree(cb);\n            pr_warn(\"install manager fd add task_work failed\\n\");\n        }\n        return 0;\n    }\n\n    if (ksu_is_allow_uid_for_current(new_uid)) {\n        if (current->seccomp.mode == SECCOMP_MODE_FILTER &&\n            current->seccomp.filter) {\n            spin_lock_irq(&current->sighand->siglock);\n            ksu_seccomp_allow_cache(current->seccomp.filter, __NR_reboot);\n            spin_unlock_irq(&current->sighand->siglock);\n        }\n        ksu_set_task_tracepoint_flag(current);\n    } else {\n        ksu_clear_task_tracepoint_flag_if_needed(current);\n    }\n\n    // Handle kernel umount\n    ksu_handle_umount(old_uid, new_uid);\n\n    return 0;\n}\n\nvoid ksu_setuid_hook_init(void)\n{\n    ksu_kernel_umount_init();\n}\n\nvoid ksu_setuid_hook_exit(void)\n{\n    pr_info(\"ksu_core_exit\\n\");\n    ksu_kernel_umount_exit();\n}\n"
  },
  {
    "path": "kernel/setuid_hook.h",
    "content": "#ifndef __KSU_H_KSU_CORE\n#define __KSU_H_KSU_CORE\n\n#include <linux/init.h>\n#include <linux/types.h>\n\nvoid ksu_setuid_hook_init(void);\nvoid ksu_setuid_hook_exit(void);\n\n// Handler functions for hook_manager\nint ksu_handle_setresuid(uid_t ruid, uid_t euid, uid_t suid);\n\n#endif\n"
  },
  {
    "path": "kernel/setup.sh",
    "content": "#!/bin/sh\nset -eu\n\nGKI_ROOT=$(pwd)\n\ndisplay_usage() {\n    echo \"Usage: $0 [--cleanup | <commit-or-tag>]\"\n    echo \"  --cleanup:              Cleans up previous modifications made by the script.\"\n    echo \"  <commit-or-tag>:        Sets up or updates the KernelSU to specified tag or commit.\"\n    echo \"  -h, --help:             Displays this usage information.\"\n    echo \"  (no args):              Sets up or updates the KernelSU environment to the latest tagged version.\"\n}\n\ninitialize_variables() {\n    if test -d \"$GKI_ROOT/common/drivers\"; then\n         DRIVER_DIR=\"$GKI_ROOT/common/drivers\"\n    elif test -d \"$GKI_ROOT/drivers\"; then\n         DRIVER_DIR=\"$GKI_ROOT/drivers\"\n    else\n         echo '[ERROR] \"drivers/\" directory not found.'\n         exit 127\n    fi\n\n    DRIVER_MAKEFILE=$DRIVER_DIR/Makefile\n    DRIVER_KCONFIG=$DRIVER_DIR/Kconfig\n}\n\n# Reverts modifications made by this script\nperform_cleanup() {\n    echo \"[+] Cleaning up...\"\n    [ -L \"$DRIVER_DIR/kernelsu\" ] && rm \"$DRIVER_DIR/kernelsu\" && echo \"[-] Symlink removed.\"\n    grep -q \"kernelsu\" \"$DRIVER_MAKEFILE\" && sed -i '/kernelsu/d' \"$DRIVER_MAKEFILE\" && echo \"[-] Makefile reverted.\"\n    grep -q \"drivers/kernelsu/Kconfig\" \"$DRIVER_KCONFIG\" && sed -i '/drivers\\/kernelsu\\/Kconfig/d' \"$DRIVER_KCONFIG\" && echo \"[-] Kconfig reverted.\"\n    if [ -d \"$GKI_ROOT/KernelSU\" ]; then\n        rm -rf \"$GKI_ROOT/KernelSU\" && echo \"[-] KernelSU directory deleted.\"\n    fi\n}\n\n# Sets up or update KernelSU environment\nsetup_kernelsu() {\n    echo \"[+] Setting up KernelSU...\"\n    test -d \"$GKI_ROOT/KernelSU\" || git clone https://github.com/tiann/KernelSU && echo \"[+] Repository cloned.\"\n    cd \"$GKI_ROOT/KernelSU\"\n    git stash && echo \"[-] Stashed current changes.\"\n    if [ \"$(git status | grep -Po 'v\\d+(\\.\\d+)*' | head -n1)\" ]; then\n        git checkout main && echo \"[-] Switched to main branch.\"\n    fi\n    git pull && echo \"[+] Repository updated.\"\n    if [ -z \"${1-}\" ]; then\n        git checkout \"$(git describe --abbrev=0 --tags)\" && echo \"[-] Checked out latest tag.\"\n    else\n        git checkout \"$1\" && echo \"[-] Checked out $1.\" || echo \"[-] Checkout default branch\"\n    fi\n    cd \"$DRIVER_DIR\"\n    ln -sf \"$(realpath --relative-to=\"$DRIVER_DIR\" \"$GKI_ROOT/KernelSU/kernel\")\" \"kernelsu\" && echo \"[+] Symlink created.\"\n\n    # Add entries in Makefile and Kconfig if not already existing\n    grep -q \"kernelsu\" \"$DRIVER_MAKEFILE\" || printf \"\\nobj-\\$(CONFIG_KSU) += kernelsu/\\n\" >> \"$DRIVER_MAKEFILE\" && echo \"[+] Modified Makefile.\"\n    grep -q \"source \\\"drivers/kernelsu/Kconfig\\\"\" \"$DRIVER_KCONFIG\" || sed -i \"/endmenu/i\\source \\\"drivers/kernelsu/Kconfig\\\"\" \"$DRIVER_KCONFIG\" && echo \"[+] Modified Kconfig.\"\n    echo '[+] Done.'\n}\n\n# Process command-line arguments\nif [ \"$#\" -eq 0 ]; then\n    initialize_variables\n    setup_kernelsu\nelif [ \"$1\" = \"-h\" ] || [ \"$1\" = \"--help\" ]; then\n    display_usage\nelif [ \"$1\" = \"--cleanup\" ]; then\n    initialize_variables\n    perform_cleanup\nelse\n    initialize_variables\n    setup_kernelsu \"$@\"\nfi\n"
  },
  {
    "path": "kernel/su_mount_ns.c",
    "content": "#include <linux/dcache.h>\n#include <linux/errno.h>\n#include <linux/fdtable.h>\n#include <linux/file.h>\n#include <linux/fs.h>\n#include <linux/fs_struct.h>\n#include <linux/limits.h>\n#include <linux/namei.h>\n#include <linux/proc_ns.h>\n#include <linux/pid.h>\n#include <linux/sched/task.h>\n#include <linux/slab.h>\n#include <linux/syscalls.h>\n#include <linux/task_work.h>\n#include <linux/version.h>\n#include <uapi/linux/mount.h>\n\n#include \"arch.h\"\n#include \"klog.h\" // IWYU pragma: keep\n#include \"ksu.h\"\n#include \"su_mount_ns.h\"\n\nextern int path_mount(const char *dev_name, struct path *path,\n                      const char *type_page, unsigned long flags,\n                      void *data_page);\n\n#if defined(__aarch64__)\nextern long __arm64_sys_setns(const struct pt_regs *regs);\n#elif defined(__x86_64__)\nextern long __x64_sys_setns(const struct pt_regs *regs);\n#endif\n\nstatic long ksu_sys_setns(int fd, int flags)\n{\n    struct pt_regs regs;\n    memset(&regs, 0, sizeof(regs));\n\n    PT_REGS_PARM1(&regs) = fd;\n    PT_REGS_PARM2(&regs) = flags;\n\n#if defined(__aarch64__)\n    return __arm64_sys_setns(&regs);\n#elif defined(__x86_64__)\n    return __x64_sys_setns(&regs);\n#else\n#error \"Unsupported arch\"\n#endif\n}\n\n// global mode , need CAP_SYS_ADMIN and CAP_SYS_CHROOT to perform setns\nstatic void ksu_mnt_ns_global(void)\n{\n    // save current working directory as absolute path before setns\n    char *pwd_path = NULL;\n    char *pwd_buf = kmalloc(PATH_MAX, GFP_KERNEL);\n    if (!pwd_buf) {\n        pr_warn(\"no mem for pwd buffer, skip restore pwd!!\\n\");\n        goto try_setns;\n    }\n\n    struct path saved_pwd;\n    get_fs_pwd(current->fs, &saved_pwd);\n    pwd_path = d_path(&saved_pwd, pwd_buf, PATH_MAX);\n    path_put(&saved_pwd);\n\n    if (IS_ERR(pwd_path)) {\n        if (PTR_ERR(pwd_path) == -ENAMETOOLONG) {\n            pr_warn(\"absolute pwd longer than: %d, skip restore pwd!!\\n\",\n                    PATH_MAX);\n        } else {\n            pr_warn(\"get absolute pwd failed: %ld\\n\", PTR_ERR(pwd_path));\n        }\n        pwd_path = NULL;\n    }\n\ntry_setns:\n\n    rcu_read_lock();\n    // &init_task is not init, but swapper/idle, which forks the init process\n    // so we need find init process\n    struct pid *pid_struct = find_pid_ns(1, &init_pid_ns);\n    if (unlikely(!pid_struct)) {\n        rcu_read_unlock();\n        pr_warn(\"failed to find pid_struct for PID 1\\n\");\n        goto out;\n    }\n\n    struct task_struct *pid1_task = get_pid_task(pid_struct, PIDTYPE_PID);\n    rcu_read_unlock();\n    if (unlikely(!pid1_task)) {\n        pr_warn(\"failed to get task_struct for PID 1\\n\");\n        goto out;\n    }\n    struct path ns_path;\n    long ret = ns_get_path(&ns_path, pid1_task, &mntns_operations);\n    put_task_struct(pid1_task);\n    if (ret) {\n        pr_warn(\"failed get path for init mount namespace: %ld\\n\", ret);\n        goto out;\n    }\n    struct file *ns_file = dentry_open(&ns_path, O_RDONLY, ksu_cred);\n\n    path_put(&ns_path);\n    if (IS_ERR(ns_file)) {\n        pr_warn(\"failed open file for init mount namespace: %ld\\n\",\n                PTR_ERR(ns_file));\n        goto out;\n    }\n\n    int fd = get_unused_fd_flags(O_CLOEXEC);\n    if (fd < 0) {\n        pr_warn(\"failed to get an unused fd: %d\\n\", fd);\n        fput(ns_file);\n        goto out;\n    }\n\n    fd_install(fd, ns_file);\n    ret = ksu_sys_setns(fd, CLONE_NEWNS);\n\n#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 11, 0)\n    ksys_close(fd);\n#else\n    close_fd(fd);\n#endif\n\n    if (ret) {\n        pr_warn(\"call setns failed: %ld\\n\", ret);\n        goto out;\n    }\n    // try to restore working directory using absolute path after setns\n    if (pwd_path) {\n        struct path new_pwd;\n        int err = kern_path(pwd_path, 0, &new_pwd);\n        if (!err) {\n            set_fs_pwd(current->fs, &new_pwd);\n            path_put(&new_pwd);\n        } else {\n            pr_warn(\"restore pwd failed: %d, path: %s\\n\", err, pwd_path);\n        }\n    }\nout:\n    kfree(pwd_buf);\n}\n\n// individual mode , need CAP_SYS_ADMIN to perform unshare and remount\nstatic void ksu_mnt_ns_individual(void)\n{\n    long ret = ksys_unshare(CLONE_NEWNS);\n    if (ret) {\n        pr_warn(\"call ksys_unshare failed: %ld\\n\", ret);\n        return;\n    }\n\n    // make root mount private\n    struct path root_path;\n    get_fs_root(current->fs, &root_path);\n    int pm_ret = path_mount(NULL, &root_path, NULL, MS_PRIVATE | MS_REC, NULL);\n    path_put(&root_path);\n\n    if (pm_ret < 0) {\n        pr_err(\"failed to make root private, err: %d\\n\", pm_ret);\n    }\n}\n\nstatic void ksu_setup_mount_ns_tw_func(struct callback_head *cb)\n{\n    struct ksu_mns_tw *tw = container_of(cb, struct ksu_mns_tw, cb);\n    const struct cred *old_cred = override_creds(ksu_cred);\n    if (tw->ns_mode == KSU_NS_GLOBAL) {\n        ksu_mnt_ns_global();\n    } else {\n        ksu_mnt_ns_individual();\n    }\n    revert_creds(old_cred);\n    kfree(tw);\n}\n\nvoid setup_mount_ns(int32_t ns_mode)\n{\n    // inherit mode\n    if (ns_mode == KSU_NS_INHERITED) {\n        // do nothing\n        return;\n    }\n\n    if (ns_mode != KSU_NS_GLOBAL && ns_mode != KSU_NS_INDIVIDUAL) {\n        pr_warn(\"pid: %d ,unknown mount namespace mode: %d\\n\", current->pid,\n                ns_mode);\n        return;\n    }\n\n    if (!ksu_cred) {\n        pr_err(\"no ksu cred! skip mnt_ns magic for pid: %d.\\n\", current->pid);\n        return;\n    }\n\n    struct ksu_mns_tw *tw = kzalloc(sizeof(*tw), GFP_ATOMIC);\n    if (!tw) {\n        pr_err(\"no mem for tw! skip mnt_ns magic for pid: %d.\\n\", current->pid);\n        return;\n    }\n    tw->cb.func = ksu_setup_mount_ns_tw_func;\n    tw->ns_mode = ns_mode;\n    if (task_work_add(current, &tw->cb, TWA_RESUME)) {\n        kfree(tw);\n        pr_err(\"add task work failed! skip mnt_ns magic for pid: %d.\\n\",\n               current->pid);\n    }\n}\n"
  },
  {
    "path": "kernel/su_mount_ns.h",
    "content": "#ifndef __KSU_SU_MOUNT_NS_H\n#define __KSU_SU_MOUNT_NS_H\n\n#define KSU_NS_INHERITED 0\n#define KSU_NS_GLOBAL 1\n#define KSU_NS_INDIVIDUAL 2\n\nstruct ksu_mns_tw {\n    struct callback_head cb;\n    int32_t ns_mode;\n};\n\nvoid setup_mount_ns(int32_t ns_mode);\n\n#endif\n"
  },
  {
    "path": "kernel/sucompat.c",
    "content": "#include <linux/compiler_types.h>\n#include <linux/preempt.h>\n#include <linux/printk.h>\n#include <linux/mm.h>\n#include <linux/pgtable.h>\n#include <linux/uaccess.h>\n#include <asm/current.h>\n#include <linux/cred.h>\n#include <linux/fs.h>\n#include <linux/types.h>\n#include <linux/version.h>\n#include <linux/sched/task_stack.h>\n#include <linux/ptrace.h>\n\n#include \"allowlist.h\"\n#include \"feature.h\"\n#include \"klog.h\" // IWYU pragma: keep\n#include \"ksud.h\"\n#include \"sucompat.h\"\n#include \"app_profile.h\"\n#include \"util.h\"\n\n#define SU_PATH \"/system/bin/su\"\n#define SH_PATH \"/system/bin/sh\"\n\nbool ksu_su_compat_enabled __read_mostly = true;\n\nstatic int su_compat_feature_get(u64 *value)\n{\n    *value = ksu_su_compat_enabled ? 1 : 0;\n    return 0;\n}\n\nstatic int su_compat_feature_set(u64 value)\n{\n    bool enable = value != 0;\n    ksu_su_compat_enabled = enable;\n    pr_info(\"su_compat: set to %d\\n\", enable);\n    return 0;\n}\n\nstatic const struct ksu_feature_handler su_compat_handler = {\n    .feature_id = KSU_FEATURE_SU_COMPAT,\n    .name = \"su_compat\",\n    .get_handler = su_compat_feature_get,\n    .set_handler = su_compat_feature_set,\n};\n\nstatic void __user *userspace_stack_buffer(const void *d, size_t len)\n{\n    // To avoid having to mmap a page in userspace, just write below the stack\n    // pointer.\n    char __user *p = (void __user *)current_user_stack_pointer() - len;\n\n    return copy_to_user(p, d, len) ? NULL : p;\n}\n\nstatic char __user *sh_user_path(void)\n{\n    static const char sh_path[] = \"/system/bin/sh\";\n\n    return userspace_stack_buffer(sh_path, sizeof(sh_path));\n}\n\nstatic char __user *ksud_user_path(void)\n{\n    static const char ksud_path[] = KSUD_PATH;\n\n    return userspace_stack_buffer(ksud_path, sizeof(ksud_path));\n}\n\nint ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n                         int *__unused_flags)\n{\n    const char su[] = SU_PATH;\n\n    if (!ksu_is_allow_uid_for_current(current_uid().val)) {\n        return 0;\n    }\n\n    char path[sizeof(su) + 1];\n    memset(path, 0, sizeof(path));\n    strncpy_from_user_nofault(path, *filename_user, sizeof(path));\n\n    if (unlikely(!memcmp(path, su, sizeof(su)))) {\n        pr_info(\"faccessat su->sh!\\n\");\n        *filename_user = sh_user_path();\n    }\n\n    return 0;\n}\n\nint ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)\n{\n    // const char sh[] = SH_PATH;\n    const char su[] = SU_PATH;\n\n    if (!ksu_is_allow_uid_for_current(current_uid().val)) {\n        return 0;\n    }\n\n    if (unlikely(!filename_user)) {\n        return 0;\n    }\n\n    char path[sizeof(su) + 1];\n    memset(path, 0, sizeof(path));\n    strncpy_from_user_nofault(path, *filename_user, sizeof(path));\n\n    if (unlikely(!memcmp(path, su, sizeof(su)))) {\n        pr_info(\"newfstatat su->sh!\\n\");\n        *filename_user = sh_user_path();\n    }\n\n    return 0;\n}\n\nint ksu_handle_execve_sucompat(const char __user **filename_user,\n                               void *__never_use_argv, void *__never_use_envp,\n                               int *__never_use_flags)\n{\n    const char su[] = SU_PATH;\n    const char __user *fn;\n    char path[sizeof(su) + 1];\n    long ret;\n    unsigned long addr;\n\n    if (unlikely(!filename_user))\n        return 0;\n\n    if (!ksu_is_allow_uid_for_current(current_uid().val))\n        return 0;\n\n    addr = untagged_addr((unsigned long)*filename_user);\n    fn = (const char __user *)addr;\n    memset(path, 0, sizeof(path));\n    ret = strncpy_from_user_nofault(path, fn, sizeof(path));\n\n    if (ret < 0 && try_set_access_flag(addr)) {\n        ret = strncpy_from_user_nofault(path, fn, sizeof(path));\n    }\n\n    if (ret < 0 && preempt_count()) {\n        /* This is crazy, but we know what we are doing:\n         * Temporarily exit atomic context to handle page faults, then restore it */\n        pr_info(\"Access filename failed, try rescue..\\n\");\n        preempt_enable_no_resched_notrace();\n        ret = strncpy_from_user(path, fn, sizeof(path));\n        preempt_disable_notrace();\n    }\n\n    if (ret < 0) {\n        pr_warn(\"Access filename when execve failed: %ld\", ret);\n        return 0;\n    }\n\n    if (likely(memcmp(path, su, sizeof(su))))\n        return 0;\n\n    pr_info(\"sys_execve su found\\n\");\n    *filename_user = ksud_user_path();\n\n    escape_with_root_profile();\n\n    return 0;\n}\n\n// sucompat: permitted process can execute 'su' to gain root access.\nvoid ksu_sucompat_init()\n{\n    if (ksu_register_feature_handler(&su_compat_handler)) {\n        pr_err(\"Failed to register su_compat feature handler\\n\");\n    }\n}\n\nvoid ksu_sucompat_exit()\n{\n    ksu_unregister_feature_handler(KSU_FEATURE_SU_COMPAT);\n}\n"
  },
  {
    "path": "kernel/sucompat.h",
    "content": "#ifndef __KSU_H_SUCOMPAT\n#define __KSU_H_SUCOMPAT\n#include <linux/types.h>\n\nextern bool ksu_su_compat_enabled;\n\nvoid ksu_sucompat_init(void);\nvoid ksu_sucompat_exit(void);\n\n// Handler functions exported for hook_manager\nint ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n                         int *__unused_flags);\nint ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\nint ksu_handle_execve_sucompat(const char __user **filename_user,\n                               void *__never_use_argv, void *__never_use_envp,\n                               int *__never_use_flags);\n\n#endif"
  },
  {
    "path": "kernel/supercalls.c",
    "content": "#include <linux/anon_inodes.h>\n#include <linux/capability.h>\n#include <linux/cred.h>\n#include <linux/err.h>\n#include <linux/fdtable.h>\n#include <linux/file.h>\n#include <linux/fs.h>\n#include <linux/slab.h>\n#include <linux/kprobes.h>\n#include <linux/syscalls.h>\n#include <linux/task_work.h>\n#include <linux/uaccess.h>\n#include <linux/version.h>\n\n#include \"supercalls.h\"\n#include \"arch.h\"\n#include \"allowlist.h\"\n#include \"feature.h\"\n#include \"klog.h\" // IWYU pragma: keep\n#include \"ksu.h\"\n#include \"ksud.h\"\n#include \"kernel_umount.h\"\n#include \"manager.h\"\n#include \"selinux/selinux.h\"\n#include \"file_wrapper.h\"\n#include \"syscall_hook_manager.h\"\n\n// Permission check functions\nbool only_manager(void)\n{\n    return is_manager();\n}\n\nbool only_root(void)\n{\n    return current_uid().val == 0;\n}\n\nbool manager_or_root(void)\n{\n    return current_uid().val == 0 || is_manager();\n}\n\nbool always_allow(void)\n{\n    return true; // No permission check\n}\n\nbool allowed_for_su(void)\n{\n    bool is_allowed =\n        is_manager() || ksu_is_allow_uid_for_current(current_uid().val);\n    return is_allowed;\n}\n\nstatic int do_grant_root(void __user *arg)\n{\n    // we already check uid above on allowed_for_su()\n\n    pr_info(\"allow root for: %d\\n\", current_uid().val);\n    escape_with_root_profile();\n\n    return 0;\n}\n\nstatic int do_get_info(void __user *arg)\n{\n    struct ksu_get_info_cmd cmd = { .version = KERNEL_SU_VERSION, .flags = 0 };\n\n#ifdef MODULE\n    cmd.flags |= KSU_GET_INFO_FLAG_LKM;\n#endif\n\n    if (is_manager()) {\n        cmd.flags |= KSU_GET_INFO_FLAG_MANAGER;\n    }\n    if (ksu_late_loaded) {\n        cmd.flags |= KSU_GET_INFO_FLAG_LATE_LOAD;\n    }\n#ifdef EXPECTED_SIZE2\n    cmd.flags |= KSU_GET_INFO_FLAG_PR_BUILD;\n#endif\n    cmd.features = KSU_FEATURE_MAX;\n\n    if (copy_to_user(arg, &cmd, sizeof(cmd))) {\n        pr_err(\"get_version: copy_to_user failed\\n\");\n        return -EFAULT;\n    }\n\n    return 0;\n}\n\nstatic int do_report_event(void __user *arg)\n{\n    struct ksu_report_event_cmd cmd;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        return -EFAULT;\n    }\n\n    switch (cmd.event) {\n    case EVENT_POST_FS_DATA: {\n        static bool post_fs_data_lock = false;\n        if (!post_fs_data_lock) {\n            post_fs_data_lock = true;\n            if (ksu_late_loaded) {\n                pr_info(\"post-fs-data skipped (late load)\\n\");\n            } else {\n                pr_info(\"post-fs-data triggered\\n\");\n                on_post_fs_data();\n            }\n        }\n        break;\n    }\n    case EVENT_BOOT_COMPLETED: {\n        static bool boot_complete_lock = false;\n        if (!boot_complete_lock) {\n            boot_complete_lock = true;\n            if (ksu_late_loaded) {\n                pr_info(\"boot_complete skipped (late load)\\n\");\n            } else {\n                pr_info(\"boot_complete triggered\\n\");\n                on_boot_completed();\n            }\n        }\n        break;\n    }\n    case EVENT_MODULE_MOUNTED: {\n        pr_info(\"module mounted!\\n\");\n        on_module_mounted();\n        break;\n    }\n    default:\n        break;\n    }\n\n    return 0;\n}\n\nstatic int do_set_sepolicy(void __user *arg)\n{\n    struct ksu_set_sepolicy_cmd cmd;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        return -EFAULT;\n    }\n\n    return handle_sepolicy((void __user *)cmd.data, cmd.data_len);\n}\n\nstatic int do_check_safemode(void __user *arg)\n{\n    struct ksu_check_safemode_cmd cmd;\n\n    cmd.in_safe_mode = ksu_is_safe_mode();\n\n    if (cmd.in_safe_mode) {\n        pr_warn(\"safemode enabled!\\n\");\n    }\n\n    if (copy_to_user(arg, &cmd, sizeof(cmd))) {\n        pr_err(\"check_safemode: copy_to_user failed\\n\");\n        return -EFAULT;\n    }\n\n    return 0;\n}\n\nstatic int do_new_get_allow_list_common(void __user *arg, bool allow)\n{\n    struct ksu_new_get_allow_list_cmd cmd;\n    int *arr = NULL;\n    int err = 0;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        return -EFAULT;\n    }\n\n    if (cmd.count) {\n        arr = kmalloc(sizeof(int) * cmd.count, GFP_KERNEL);\n        if (!arr) {\n            return -ENOMEM;\n        }\n    }\n\n    bool success =\n        ksu_get_allow_list(arr, cmd.count, &cmd.count, &cmd.total_count, allow);\n\n    if (!success) {\n        err = -EFAULT;\n        goto out;\n    }\n\n    if (copy_to_user(arg, &cmd, sizeof(cmd))) {\n        pr_err(\"new_get_allow_list: copy_to_user count failed\\n\");\n        err = -EFAULT;\n        goto out;\n    }\n\n    if (cmd.count &&\n        copy_to_user(&((struct ksu_new_get_allow_list_cmd *)arg)->uids, arr,\n                     sizeof(int) * cmd.count)) {\n        pr_err(\"new_get_allow_list: copy_to_user uids failed\\n\");\n        err = -EFAULT;\n    }\n\nout:\n    if (arr) {\n        kfree(arr);\n    }\n    return err;\n}\n\nstatic int do_new_get_deny_list(void __user *arg)\n{\n    return do_new_get_allow_list_common(arg, false);\n}\n\nstatic int do_new_get_allow_list(void __user *arg)\n{\n    return do_new_get_allow_list_common(arg, true);\n}\n\nstatic int do_get_allow_list_common(void __user *arg, bool allow)\n{\n    int *arr = NULL;\n    int err = 0;\n    u16 count;\n    u32 out_count;\n    static const u16 kSize = 128;\n\n    arr = kmalloc(sizeof(int) * kSize, GFP_KERNEL);\n    if (!arr) {\n        return -ENOMEM;\n    }\n\n    bool success = ksu_get_allow_list(arr, kSize, &count, NULL, allow);\n\n    if (!success) {\n        err = -EFAULT;\n        goto out;\n    }\n\n    out_count = count;\n\n    if (copy_to_user(arg + offsetof(struct ksu_get_allow_list_cmd, count),\n                     &out_count, sizeof(u32))) {\n        pr_err(\"get_allow_list: copy_to_user count failed\\n\");\n        err = -EFAULT;\n        goto out;\n    }\n\n    if (copy_to_user(arg, arr, sizeof(u32) * count)) {\n        pr_err(\"get_allow_list: copy_to_user uids failed\\n\");\n        err = -EFAULT;\n    }\n\nout:\n    if (arr) {\n        kfree(arr);\n    }\n    return err;\n}\n\nstatic int do_get_deny_list(void __user *arg)\n{\n    return do_get_allow_list_common(arg, false);\n}\n\nstatic int do_get_allow_list(void __user *arg)\n{\n    return do_get_allow_list_common(arg, true);\n}\n\nstatic int do_uid_granted_root(void __user *arg)\n{\n    struct ksu_uid_granted_root_cmd cmd;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        return -EFAULT;\n    }\n\n    cmd.granted = ksu_is_allow_uid_for_current(cmd.uid);\n\n    if (copy_to_user(arg, &cmd, sizeof(cmd))) {\n        pr_err(\"uid_granted_root: copy_to_user failed\\n\");\n        return -EFAULT;\n    }\n\n    return 0;\n}\n\nstatic int do_uid_should_umount(void __user *arg)\n{\n    struct ksu_uid_should_umount_cmd cmd;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        return -EFAULT;\n    }\n\n    cmd.should_umount = ksu_uid_should_umount(cmd.uid);\n\n    if (copy_to_user(arg, &cmd, sizeof(cmd))) {\n        pr_err(\"uid_should_umount: copy_to_user failed\\n\");\n        return -EFAULT;\n    }\n\n    return 0;\n}\n\nstatic int do_get_manager_appid(void __user *arg)\n{\n    struct ksu_get_manager_appid_cmd cmd;\n\n    cmd.appid = ksu_get_manager_appid();\n\n    if (copy_to_user(arg, &cmd, sizeof(cmd))) {\n        pr_err(\"get_manager_appid: copy_to_user failed\\n\");\n        return -EFAULT;\n    }\n\n    return 0;\n}\n\nstatic int do_get_app_profile(void __user *arg)\n{\n    struct ksu_get_app_profile_cmd cmd;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        pr_err(\"get_app_profile: copy_from_user failed\\n\");\n        return -EFAULT;\n    }\n\n    if (!ksu_get_app_profile(&cmd.profile)) {\n        return -ENOENT;\n    }\n\n    if (copy_to_user(arg, &cmd, sizeof(cmd))) {\n        pr_err(\"get_app_profile: copy_to_user failed\\n\");\n        return -EFAULT;\n    }\n\n    return 0;\n}\n\nstatic int do_set_app_profile(void __user *arg)\n{\n    struct ksu_set_app_profile_cmd cmd;\n    int ret;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        pr_err(\"set_app_profile: copy_from_user failed\\n\");\n        return -EFAULT;\n    }\n\n    ret = ksu_set_app_profile(&cmd.profile);\n    if (!ret) {\n        ksu_persistent_allow_list();\n        ksu_mark_running_process();\n    }\n    return ret;\n}\n\nstatic int do_get_feature(void __user *arg)\n{\n    struct ksu_get_feature_cmd cmd;\n    bool supported;\n    int ret;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        pr_err(\"get_feature: copy_from_user failed\\n\");\n        return -EFAULT;\n    }\n\n    ret = ksu_get_feature(cmd.feature_id, &cmd.value, &supported);\n    cmd.supported = supported ? 1 : 0;\n\n    if (ret && supported) {\n        pr_err(\"get_feature: failed for feature %u: %d\\n\", cmd.feature_id, ret);\n        return ret;\n    }\n\n    if (copy_to_user(arg, &cmd, sizeof(cmd))) {\n        pr_err(\"get_feature: copy_to_user failed\\n\");\n        return -EFAULT;\n    }\n\n    return 0;\n}\n\nstatic int do_set_feature(void __user *arg)\n{\n    struct ksu_set_feature_cmd cmd;\n    int ret;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        pr_err(\"set_feature: copy_from_user failed\\n\");\n        return -EFAULT;\n    }\n\n    ret = ksu_set_feature(cmd.feature_id, cmd.value);\n    if (ret) {\n        pr_err(\"set_feature: failed for feature %u: %d\\n\", cmd.feature_id, ret);\n        return ret;\n    }\n\n    return 0;\n}\n\nstatic int do_get_wrapper_fd(void __user *arg)\n{\n    if (!ksu_file_sid) {\n        return -EINVAL;\n    }\n\n    struct ksu_get_wrapper_fd_cmd cmd;\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        pr_err(\"get_wrapper_fd: copy_from_user failed\\n\");\n        return -EFAULT;\n    }\n\n    return ksu_install_file_wrapper(cmd.fd);\n}\n\nstatic int do_manage_mark(void __user *arg)\n{\n    struct ksu_manage_mark_cmd cmd;\n    int ret = 0;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd))) {\n        pr_err(\"manage_mark: copy_from_user failed\\n\");\n        return -EFAULT;\n    }\n\n    switch (cmd.operation) {\n    case KSU_MARK_GET: {\n        // Get task mark status\n        ret = ksu_get_task_mark(cmd.pid);\n        if (ret < 0) {\n            pr_err(\"manage_mark: get failed for pid %d: %d\\n\", cmd.pid, ret);\n            return ret;\n        }\n        cmd.result = (u32)ret;\n        break;\n    }\n    case KSU_MARK_MARK: {\n        if (cmd.pid == 0) {\n            ksu_mark_all_process();\n        } else {\n            ret = ksu_set_task_mark(cmd.pid, true);\n            if (ret < 0) {\n                pr_err(\"manage_mark: set_mark failed for pid %d: %d\\n\", cmd.pid,\n                       ret);\n                return ret;\n            }\n        }\n        break;\n    }\n    case KSU_MARK_UNMARK: {\n        if (cmd.pid == 0) {\n            ksu_unmark_all_process();\n        } else {\n            ret = ksu_set_task_mark(cmd.pid, false);\n            if (ret < 0) {\n                pr_err(\"manage_mark: set_unmark failed for pid %d: %d\\n\",\n                       cmd.pid, ret);\n                return ret;\n            }\n        }\n        break;\n    }\n    case KSU_MARK_REFRESH: {\n        ksu_mark_running_process();\n        pr_info(\"manage_mark: refreshed running processes\\n\");\n        break;\n    }\n    default: {\n        pr_err(\"manage_mark: invalid operation %u\\n\", cmd.operation);\n        return -EINVAL;\n    }\n    }\n    if (copy_to_user(arg, &cmd, sizeof(cmd))) {\n        pr_err(\"manage_mark: copy_to_user failed\\n\");\n        return -EFAULT;\n    }\n\n    return 0;\n}\n\nstatic int do_nuke_ext4_sysfs(void __user *arg)\n{\n    struct ksu_nuke_ext4_sysfs_cmd cmd;\n    char mnt[256];\n    long ret;\n\n    if (copy_from_user(&cmd, arg, sizeof(cmd)))\n        return -EFAULT;\n\n    if (!cmd.arg)\n        return -EINVAL;\n\n    memset(mnt, 0, sizeof(mnt));\n\n    ret = strncpy_from_user(mnt, cmd.arg, sizeof(mnt));\n    if (ret < 0) {\n        pr_err(\"nuke ext4 copy mnt failed: %ld\\\\n\", ret);\n        return -EFAULT; // 或者 return ret;\n    }\n\n    if (ret == sizeof(mnt)) {\n        pr_err(\"nuke ext4 mnt path too long\\\\n\");\n        return -ENAMETOOLONG;\n    }\n\n    pr_info(\"do_nuke_ext4_sysfs: %s\\n\", mnt);\n\n    return nuke_ext4_sysfs(mnt);\n}\n\nstruct list_head mount_list = LIST_HEAD_INIT(mount_list);\nDECLARE_RWSEM(mount_list_lock);\n\nstatic int add_try_umount(void __user *arg)\n{\n    struct mount_entry *new_entry, *entry, *tmp;\n    struct ksu_add_try_umount_cmd cmd;\n    char buf[256] = { 0 };\n\n    if (copy_from_user(&cmd, arg, sizeof cmd))\n        return -EFAULT;\n\n    switch (cmd.mode) {\n    case KSU_UMOUNT_WIPE: {\n        struct mount_entry *entry, *tmp;\n        down_write(&mount_list_lock);\n        list_for_each_entry_safe (entry, tmp, &mount_list, list) {\n            pr_info(\"wipe_umount_list: removing entry: %s\\n\",\n                    entry->umountable);\n            list_del(&entry->list);\n            kfree(entry->umountable);\n            kfree(entry);\n        }\n        up_write(&mount_list_lock);\n\n        return 0;\n    }\n\n    case KSU_UMOUNT_ADD: {\n        long len = strncpy_from_user(buf, (const char __user *)cmd.arg, 256);\n        if (len <= 0)\n            return -EFAULT;\n\n        buf[sizeof(buf) - 1] = '\\0';\n\n        new_entry = kzalloc(sizeof(*new_entry), GFP_KERNEL);\n        if (!new_entry)\n            return -ENOMEM;\n\n        new_entry->umountable = kstrdup(buf, GFP_KERNEL);\n        if (!new_entry->umountable) {\n            kfree(new_entry);\n            return -ENOMEM;\n        }\n\n        down_write(&mount_list_lock);\n\n        // disallow dupes\n        // if this gets too many, we can consider moving this whole task to a kthread\n        list_for_each_entry (entry, &mount_list, list) {\n            if (!strcmp(entry->umountable, buf)) {\n                pr_info(\"cmd_add_try_umount: %s is already here!\\n\", buf);\n                up_write(&mount_list_lock);\n                kfree(new_entry->umountable);\n                kfree(new_entry);\n                return -EEXIST;\n            }\n        }\n\n        // now check flags and add\n        // this also serves as a null check\n        if (cmd.flags)\n            new_entry->flags = cmd.flags;\n        else\n            new_entry->flags = 0;\n\n        // debug\n        list_add(&new_entry->list, &mount_list);\n        up_write(&mount_list_lock);\n        pr_info(\"cmd_add_try_umount: %s added!\\n\", buf);\n\n        return 0;\n    }\n\n    // this is just strcmp'd wipe anyway\n    case KSU_UMOUNT_DEL: {\n        long len = strncpy_from_user(buf, (const char __user *)cmd.arg,\n                                     sizeof(buf) - 1);\n        if (len <= 0)\n            return -EFAULT;\n\n        buf[sizeof(buf) - 1] = '\\0';\n\n        down_write(&mount_list_lock);\n        list_for_each_entry_safe (entry, tmp, &mount_list, list) {\n            if (!strcmp(entry->umountable, buf)) {\n                pr_info(\"cmd_add_try_umount: entry removed: %s\\n\",\n                        entry->umountable);\n                list_del(&entry->list);\n                kfree(entry->umountable);\n                kfree(entry);\n            }\n        }\n        up_write(&mount_list_lock);\n\n        return 0;\n    }\n\n    default: {\n        pr_err(\"cmd_add_try_umount: invalid operation %u\\n\", cmd.mode);\n        return -EINVAL;\n    }\n\n    } // switch(cmd.mode)\n\n    return 0;\n}\n\n// IOCTL handlers mapping table\nstatic const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = {\n    { .cmd = KSU_IOCTL_GRANT_ROOT,\n      .name = \"GRANT_ROOT\",\n      .handler = do_grant_root,\n      .perm_check = allowed_for_su },\n    { .cmd = KSU_IOCTL_GET_INFO,\n      .name = \"GET_INFO\",\n      .handler = do_get_info,\n      .perm_check = always_allow },\n    { .cmd = KSU_IOCTL_REPORT_EVENT,\n      .name = \"REPORT_EVENT\",\n      .handler = do_report_event,\n      .perm_check = only_root },\n    { .cmd = KSU_IOCTL_SET_SEPOLICY,\n      .name = \"SET_SEPOLICY\",\n      .handler = do_set_sepolicy,\n      .perm_check = only_root },\n    { .cmd = KSU_IOCTL_CHECK_SAFEMODE,\n      .name = \"CHECK_SAFEMODE\",\n      .handler = do_check_safemode,\n      .perm_check = always_allow },\n    { .cmd = KSU_IOCTL_GET_ALLOW_LIST,\n      .name = \"GET_ALLOW_LIST\",\n      .handler = do_get_allow_list,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_GET_DENY_LIST,\n      .name = \"GET_DENY_LIST\",\n      .handler = do_get_deny_list,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_NEW_GET_ALLOW_LIST,\n      .name = \"NEW_GET_ALLOW_LIST\",\n      .handler = do_new_get_allow_list,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_NEW_GET_DENY_LIST,\n      .name = \"NEW_GET_DENY_LIST\",\n      .handler = do_new_get_deny_list,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_UID_GRANTED_ROOT,\n      .name = \"UID_GRANTED_ROOT\",\n      .handler = do_uid_granted_root,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_UID_SHOULD_UMOUNT,\n      .name = \"UID_SHOULD_UMOUNT\",\n      .handler = do_uid_should_umount,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_GET_MANAGER_APPID,\n      .name = \"GET_MANAGER_APPID\",\n      .handler = do_get_manager_appid,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_GET_APP_PROFILE,\n      .name = \"GET_APP_PROFILE\",\n      .handler = do_get_app_profile,\n      .perm_check = only_manager },\n    { .cmd = KSU_IOCTL_SET_APP_PROFILE,\n      .name = \"SET_APP_PROFILE\",\n      .handler = do_set_app_profile,\n      .perm_check = only_manager },\n    { .cmd = KSU_IOCTL_GET_FEATURE,\n      .name = \"GET_FEATURE\",\n      .handler = do_get_feature,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_SET_FEATURE,\n      .name = \"SET_FEATURE\",\n      .handler = do_set_feature,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_GET_WRAPPER_FD,\n      .name = \"GET_WRAPPER_FD\",\n      .handler = do_get_wrapper_fd,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_MANAGE_MARK,\n      .name = \"MANAGE_MARK\",\n      .handler = do_manage_mark,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_NUKE_EXT4_SYSFS,\n      .name = \"NUKE_EXT4_SYSFS\",\n      .handler = do_nuke_ext4_sysfs,\n      .perm_check = manager_or_root },\n    { .cmd = KSU_IOCTL_ADD_TRY_UMOUNT,\n      .name = \"ADD_TRY_UMOUNT\",\n      .handler = add_try_umount,\n      .perm_check = manager_or_root },\n    { .cmd = 0, .name = NULL, .handler = NULL, .perm_check = NULL } // Sentinel\n};\n\nstruct ksu_install_fd_tw {\n    struct callback_head cb;\n    int __user *outp;\n};\n\nstatic void ksu_install_fd_tw_func(struct callback_head *cb)\n{\n    struct ksu_install_fd_tw *tw =\n        container_of(cb, struct ksu_install_fd_tw, cb);\n    int fd = ksu_install_fd();\n    pr_info(\"[%d] install ksu fd: %d\\n\", current->pid, fd);\n\n    if (copy_to_user(tw->outp, &fd, sizeof(fd))) {\n        pr_err(\"install ksu fd reply err\\n\");\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)\n        close_fd(fd);\n#else\n        ksys_close(fd);\n#endif\n    }\n\n    kfree(tw);\n}\n\nstatic int reboot_handler_pre(struct kprobe *p, struct pt_regs *regs)\n{\n    struct pt_regs *real_regs = PT_REAL_REGS(regs);\n    int magic1 = (int)PT_REGS_PARM1(real_regs);\n    int magic2 = (int)PT_REGS_PARM2(real_regs);\n    unsigned long arg4;\n\n    // Check if this is a request to install KSU fd\n    if (magic1 == KSU_INSTALL_MAGIC1 && magic2 == KSU_INSTALL_MAGIC2) {\n        struct ksu_install_fd_tw *tw;\n\n        arg4 = (unsigned long)PT_REGS_SYSCALL_PARM4(real_regs);\n\n        tw = kzalloc(sizeof(*tw), GFP_ATOMIC);\n        if (!tw)\n            return 0;\n\n        tw->outp = (int __user *)arg4;\n        tw->cb.func = ksu_install_fd_tw_func;\n\n        if (task_work_add(current, &tw->cb, TWA_RESUME)) {\n            kfree(tw);\n            pr_warn(\"install fd add task_work failed\\n\");\n        }\n    }\n\n    return 0;\n}\n\nstatic struct kprobe reboot_kp = {\n    .symbol_name = REBOOT_SYMBOL,\n    .pre_handler = reboot_handler_pre,\n};\n\nvoid ksu_supercalls_init(void)\n{\n    int i;\n\n    pr_info(\"KernelSU IOCTL Commands:\\n\");\n    for (i = 0; ksu_ioctl_handlers[i].handler; i++) {\n        pr_info(\"  %-18s = 0x%08x\\n\", ksu_ioctl_handlers[i].name,\n                ksu_ioctl_handlers[i].cmd);\n    }\n\n    int rc = register_kprobe(&reboot_kp);\n    if (rc) {\n        pr_err(\"reboot kprobe failed: %d\\n\", rc);\n    } else {\n        pr_info(\"reboot kprobe registered successfully\\n\");\n    }\n}\n\nvoid ksu_supercalls_exit(void)\n{\n    unregister_kprobe(&reboot_kp);\n}\n\n// IOCTL dispatcher\nstatic long anon_ksu_ioctl(struct file *filp, unsigned int cmd,\n                           unsigned long arg)\n{\n    void __user *argp = (void __user *)arg;\n    int i;\n\n#ifdef CONFIG_KSU_DEBUG\n    pr_info(\"ksu ioctl: cmd=0x%x from uid=%d\\n\", cmd, current_uid().val);\n#endif\n\n    for (i = 0; ksu_ioctl_handlers[i].handler; i++) {\n        if (cmd == ksu_ioctl_handlers[i].cmd) {\n            // Check permission first\n            if (ksu_ioctl_handlers[i].perm_check &&\n                !ksu_ioctl_handlers[i].perm_check()) {\n                pr_warn(\"ksu ioctl: permission denied for cmd=0x%x uid=%d\\n\",\n                        cmd, current_uid().val);\n                return -EPERM;\n            }\n            // Execute handler\n            return ksu_ioctl_handlers[i].handler(argp);\n        }\n    }\n\n    pr_warn(\"ksu ioctl: unsupported command 0x%x\\n\", cmd);\n    return -ENOTTY;\n}\n\n// File release handler\nstatic int anon_ksu_release(struct inode *inode, struct file *filp)\n{\n    pr_info(\"ksu fd released\\n\");\n    return 0;\n}\n\n// File operations structure\nstatic const struct file_operations anon_ksu_fops = {\n    .owner = THIS_MODULE,\n    .unlocked_ioctl = anon_ksu_ioctl,\n    .compat_ioctl = anon_ksu_ioctl,\n    .release = anon_ksu_release,\n};\n\n// Install KSU fd to current process\nint ksu_install_fd(void)\n{\n    struct file *filp;\n    int fd;\n\n    // Get unused fd\n    fd = get_unused_fd_flags(O_CLOEXEC);\n    if (fd < 0) {\n        pr_err(\"ksu_install_fd: failed to get unused fd\\n\");\n        return fd;\n    }\n\n    // Create anonymous inode file\n    filp = anon_inode_getfile(\"[ksu_driver]\", &anon_ksu_fops, NULL,\n                              O_RDWR | O_CLOEXEC);\n    if (IS_ERR(filp)) {\n        pr_err(\"ksu_install_fd: failed to create anon inode file\\n\");\n        put_unused_fd(fd);\n        return PTR_ERR(filp);\n    }\n\n    // Install fd\n    fd_install(fd, filp);\n\n    pr_info(\"ksu fd installed: %d for pid %d\\n\", fd, current->pid);\n\n    return fd;\n}\n"
  },
  {
    "path": "kernel/supercalls.h",
    "content": "#ifndef __KSU_H_SUPERCALLS\n#define __KSU_H_SUPERCALLS\n\n#include <linux/types.h>\n#include <linux/ioctl.h>\n#include \"app_profile.h\"\n\n// Magic numbers for reboot hook to install fd\n#define KSU_INSTALL_MAGIC1 0xDEADBEEF\n#define KSU_INSTALL_MAGIC2 0xCAFEBABE\n\n// Command structures for ioctl\n\nstruct ksu_become_daemon_cmd {\n    __u8 token[65]; // Input: daemon token (null-terminated)\n};\n\n#define KSU_GET_INFO_FLAG_LKM (1U << 0)\n#define KSU_GET_INFO_FLAG_MANAGER (1U << 1)\n#define KSU_GET_INFO_FLAG_LATE_LOAD (1U << 2)\n#define KSU_GET_INFO_FLAG_PR_BUILD (1U << 3)\n\nstruct ksu_get_info_cmd {\n    __u32 version; // Output: KERNEL_SU_VERSION\n    __u32 flags; // Output: KSU_GET_INFO_FLAG_* bits\n    __u32 features; // Output: max feature ID supported\n};\n\nstruct ksu_report_event_cmd {\n    __u32 event; // Input: EVENT_POST_FS_DATA, EVENT_BOOT_COMPLETED, etc.\n};\n\nstruct ksu_set_sepolicy_cmd {\n    __u64 data_len; // Input: bytes of serialized command payload\n    __aligned_u64 data; // Input: pointer to serialized payload\n};\n\nstruct ksu_sepolicy_cmd_hdr {\n    __u32 cmd; // Input: command type, CMD_*\n    __u32 subcmd; // Input: command subtype\n};\n// After each ksu_sepolicy_cmd_hdr, command arguments are encoded sequentially as:\n// [u32 len][len bytes][\\0], where len excludes the trailing '\\0'.\n// len == 0 represents ALL.\n// Argument count is derived from cmd:\n// CMD_NORMAL_PERM=4, CMD_XPERM=5, CMD_TYPE_STATE=1, CMD_TYPE=2,\n// CMD_TYPE_ATTR=2, CMD_ATTR=1, CMD_TYPE_TRANSITION=5,\n// CMD_TYPE_CHANGE=4, CMD_GENFSCON=3.\n\nstruct ksu_check_safemode_cmd {\n    __u8 in_safe_mode; // Output: true if in safe mode, false otherwise\n};\n\n// deprecated\nstruct ksu_get_allow_list_cmd {\n    __u32 uids[128]; // Output: array of allowed/denied UIDs\n    __u32 count; // Output: number of UIDs in array\n    __u8 allow; // Input: true for allow list, false for deny list\n};\n\nstruct ksu_new_get_allow_list_cmd {\n    __u16 count; // Input / Output: number of UIDs in array\n    __u16 total_count; // Output: total number of UIDs in requested list\n    __u32 uids[0]; // Output: array of allowed/denied UIDs\n};\n\nstruct ksu_uid_granted_root_cmd {\n    __u32 uid; // Input: target UID to check\n    __u8 granted; // Output: true if granted, false otherwise\n};\n\nstruct ksu_uid_should_umount_cmd {\n    __u32 uid; // Input: target UID to check\n    __u8 should_umount; // Output: true if should umount, false otherwise\n};\n\nstruct ksu_get_manager_appid_cmd {\n    __u32 appid; // Output: manager app id\n};\n\nstruct ksu_get_app_profile_cmd {\n    struct app_profile profile; // Input/Output: app profile structure\n};\n\nstruct ksu_set_app_profile_cmd {\n    struct app_profile profile; // Input: app profile structure\n};\n\nstruct ksu_get_feature_cmd {\n    __u32 feature_id; // Input: feature ID (enum ksu_feature_id)\n    __u64 value; // Output: feature value/state\n    __u8 supported; // Output: true if feature is supported, false otherwise\n};\n\nstruct ksu_set_feature_cmd {\n    __u32 feature_id; // Input: feature ID (enum ksu_feature_id)\n    __u64 value; // Input: feature value/state to set\n};\n\nstruct ksu_get_wrapper_fd_cmd {\n    __u32 fd; // Input: userspace fd\n    __u32 flags; // Input: flags of userspace fd\n};\n\nstruct ksu_manage_mark_cmd {\n    __u32 operation; // Input: KSU_MARK_*\n    __s32 pid; // Input: target pid (0 for all processes)\n    __u32 result; // Output: for get operation - mark status or reg_count\n};\n\n#define KSU_MARK_GET 1\n#define KSU_MARK_MARK 2\n#define KSU_MARK_UNMARK 3\n#define KSU_MARK_REFRESH 4\n\nstruct ksu_nuke_ext4_sysfs_cmd {\n    __aligned_u64 arg; // Input: mnt pointer\n};\n\nstruct ksu_add_try_umount_cmd {\n    __aligned_u64 arg; // char ptr, this is the mountpoint\n    __u32 flags; // this is the flag we use for it\n    __u8 mode; // denotes what to do with it 0:wipe_list 1:add_to_list 2:delete_entry\n};\n\n#define KSU_UMOUNT_WIPE 0 // ignore everything and wipe list\n#define KSU_UMOUNT_ADD 1 // add entry (path + flags)\n#define KSU_UMOUNT_DEL 2 // delete entry, strcmp\n\n// IOCTL command definitions\n#define KSU_IOCTL_GRANT_ROOT _IOC(_IOC_NONE, 'K', 1, 0)\n#define KSU_IOCTL_GET_INFO _IOC(_IOC_READ, 'K', 2, 0)\n#define KSU_IOCTL_REPORT_EVENT _IOC(_IOC_WRITE, 'K', 3, 0)\n#define KSU_IOCTL_SET_SEPOLICY _IOC(_IOC_READ | _IOC_WRITE, 'K', 4, 0)\n#define KSU_IOCTL_CHECK_SAFEMODE _IOC(_IOC_READ, 'K', 5, 0)\n// deprecated\n#define KSU_IOCTL_GET_ALLOW_LIST _IOC(_IOC_READ | _IOC_WRITE, 'K', 6, 0)\n// deprecated\n#define KSU_IOCTL_GET_DENY_LIST _IOC(_IOC_READ | _IOC_WRITE, 'K', 7, 0)\n#define KSU_IOCTL_NEW_GET_ALLOW_LIST                                           \\\n    _IOWR('K', 6, struct ksu_new_get_allow_list_cmd)\n#define KSU_IOCTL_NEW_GET_DENY_LIST                                            \\\n    _IOWR('K', 7, struct ksu_new_get_allow_list_cmd)\n#define KSU_IOCTL_UID_GRANTED_ROOT _IOC(_IOC_READ | _IOC_WRITE, 'K', 8, 0)\n#define KSU_IOCTL_UID_SHOULD_UMOUNT _IOC(_IOC_READ | _IOC_WRITE, 'K', 9, 0)\n#define KSU_IOCTL_GET_MANAGER_APPID _IOC(_IOC_READ, 'K', 10, 0)\n#define KSU_IOCTL_GET_APP_PROFILE _IOC(_IOC_READ | _IOC_WRITE, 'K', 11, 0)\n#define KSU_IOCTL_SET_APP_PROFILE _IOC(_IOC_WRITE, 'K', 12, 0)\n#define KSU_IOCTL_GET_FEATURE _IOC(_IOC_READ | _IOC_WRITE, 'K', 13, 0)\n#define KSU_IOCTL_SET_FEATURE _IOC(_IOC_WRITE, 'K', 14, 0)\n#define KSU_IOCTL_GET_WRAPPER_FD _IOC(_IOC_WRITE, 'K', 15, 0)\n#define KSU_IOCTL_MANAGE_MARK _IOC(_IOC_READ | _IOC_WRITE, 'K', 16, 0)\n#define KSU_IOCTL_NUKE_EXT4_SYSFS _IOC(_IOC_WRITE, 'K', 17, 0)\n#define KSU_IOCTL_ADD_TRY_UMOUNT _IOC(_IOC_WRITE, 'K', 18, 0)\n\n// IOCTL handler types\ntypedef int (*ksu_ioctl_handler_t)(void __user *arg);\ntypedef bool (*ksu_perm_check_t)(void);\n\n// IOCTL command mapping\nstruct ksu_ioctl_cmd_map {\n    unsigned int cmd;\n    const char *name;\n    ksu_ioctl_handler_t handler;\n    ksu_perm_check_t perm_check; // Permission check function\n};\n\n// Install KSU fd to current process\nint ksu_install_fd(void);\n\nvoid ksu_supercalls_init(void);\nvoid ksu_supercalls_exit(void);\n#endif // __KSU_H_SUPERCALLS\n"
  },
  {
    "path": "kernel/syscall_hook_manager.c",
    "content": "#include \"linux/compiler.h\"\n#include \"linux/cred.h\"\n#include \"linux/printk.h\"\n#include \"selinux/selinux.h\"\n#include <linux/spinlock.h>\n#include <linux/kprobes.h>\n#include <linux/tracepoint.h>\n#include <asm/syscall.h>\n#include <linux/ptrace.h>\n#include <linux/slab.h>\n#include <trace/events/syscalls.h>\n\n#include \"allowlist.h\"\n#include \"arch.h\"\n#include \"klog.h\" // IWYU pragma: keep\n#include \"syscall_hook_manager.h\"\n#include \"sucompat.h\"\n#include \"setuid_hook.h\"\n#include \"selinux/selinux.h\"\n#include \"util.h\"\n#include \"ksud.h\"\n\n// Tracepoint registration count management\n// == 1: just us\n// >  1: someone else is also using syscall tracepoint e.g. ftrace\nstatic int tracepoint_reg_count = 0;\nstatic DEFINE_SPINLOCK(tracepoint_reg_lock);\n\nvoid ksu_clear_task_tracepoint_flag_if_needed(struct task_struct *t)\n{\n    unsigned long flags;\n    spin_lock_irqsave(&tracepoint_reg_lock, flags);\n    if (tracepoint_reg_count <= 1) {\n        ksu_clear_task_tracepoint_flag(t);\n    }\n    spin_unlock_irqrestore(&tracepoint_reg_lock, flags);\n}\n\n// Process marking management\nstatic void handle_process_mark(bool mark)\n{\n    struct task_struct *p, *t;\n    read_lock(&tasklist_lock);\n    for_each_process_thread (p, t) {\n        if (mark)\n            ksu_set_task_tracepoint_flag(t);\n        else\n            ksu_clear_task_tracepoint_flag(t);\n    }\n    read_unlock(&tasklist_lock);\n}\n\nvoid ksu_mark_all_process(void)\n{\n    handle_process_mark(true);\n    pr_info(\"hook_manager: mark all user process done!\\n\");\n}\n\nvoid ksu_unmark_all_process(void)\n{\n    handle_process_mark(false);\n    pr_info(\"hook_manager: unmark all user process done!\\n\");\n}\n\nstatic void ksu_mark_running_process_locked()\n{\n    struct task_struct *p, *t;\n    read_lock(&tasklist_lock);\n    for_each_process_thread (p, t) {\n        if (!t->mm) { // only user processes\n            continue;\n        }\n        int uid = task_uid(t).val;\n        const struct cred *cred = get_task_cred(t);\n        bool ksu_root_process = uid == 0 && is_task_ksu_domain(cred);\n        bool is_zygote_process = is_zygote(cred);\n        bool is_shell = uid == 2000;\n        // before boot completed, we shall mark init for marking zygote\n        bool is_init = t->pid == 1;\n        if (ksu_root_process || is_zygote_process || is_shell || is_init ||\n            ksu_is_allow_uid(uid)) {\n            ksu_set_task_tracepoint_flag(t);\n            pr_info(\"hook_manager: mark process: pid:%d, uid: %d, comm:%s\\n\",\n                    t->pid, uid, t->comm);\n        } else {\n            ksu_clear_task_tracepoint_flag(t);\n            pr_info(\"hook_manager: unmark process: pid:%d, uid: %d, comm:%s\\n\",\n                    t->pid, uid, t->comm);\n        }\n        put_cred(cred);\n    }\n    read_unlock(&tasklist_lock);\n}\n\nvoid ksu_mark_running_process()\n{\n    unsigned long flags;\n    spin_lock_irqsave(&tracepoint_reg_lock, flags);\n    if (tracepoint_reg_count <= 1) {\n        ksu_mark_running_process_locked();\n    } else {\n        pr_info(\n            \"hook_manager: not mark running process since syscall tracepoint is in use\\n\");\n    }\n    spin_unlock_irqrestore(&tracepoint_reg_lock, flags);\n}\n\n// Get task mark status\n// Returns: 1 if marked, 0 if not marked, -ESRCH if task not found\nint ksu_get_task_mark(pid_t pid)\n{\n    struct task_struct *task;\n    int marked = -ESRCH;\n\n    rcu_read_lock();\n    task = find_task_by_vpid(pid);\n    if (task) {\n        get_task_struct(task);\n        rcu_read_unlock();\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)\n        marked = test_task_syscall_work(task, SYSCALL_TRACEPOINT) ? 1 : 0;\n#else\n        marked = test_tsk_thread_flag(task, TIF_SYSCALL_TRACEPOINT) ? 1 : 0;\n#endif\n        put_task_struct(task);\n    } else {\n        rcu_read_unlock();\n    }\n\n    return marked;\n}\n\n// Set task mark status\n// Returns: 0 on success, -ESRCH if task not found\nint ksu_set_task_mark(pid_t pid, bool mark)\n{\n    struct task_struct *task;\n    int ret = -ESRCH;\n\n    rcu_read_lock();\n    task = find_task_by_vpid(pid);\n    if (task) {\n        get_task_struct(task);\n        rcu_read_unlock();\n        if (mark) {\n            ksu_set_task_tracepoint_flag(task);\n            pr_info(\"hook_manager: marked task pid=%d comm=%s\\n\", pid,\n                    task->comm);\n        } else {\n            ksu_clear_task_tracepoint_flag(task);\n            pr_info(\"hook_manager: unmarked task pid=%d comm=%s\\n\", pid,\n                    task->comm);\n        }\n        put_task_struct(task);\n        ret = 0;\n    } else {\n        rcu_read_unlock();\n    }\n\n    return ret;\n}\n\n#ifdef CONFIG_KRETPROBES\n\nstatic struct kretprobe *init_kretprobe(const char *name,\n                                        kretprobe_handler_t handler)\n{\n    struct kretprobe *rp = kzalloc(sizeof(struct kretprobe), GFP_KERNEL);\n    if (!rp)\n        return NULL;\n    rp->kp.symbol_name = name;\n    rp->handler = handler;\n    rp->data_size = 0;\n    rp->maxactive = 0;\n\n    int ret = register_kretprobe(rp);\n    pr_info(\"hook_manager: register_%s kretprobe: %d\\n\", name, ret);\n    if (ret) {\n        kfree(rp);\n        return NULL;\n    }\n\n    return rp;\n}\n\nstatic void destroy_kretprobe(struct kretprobe **rp_ptr)\n{\n    struct kretprobe *rp = *rp_ptr;\n    if (!rp)\n        return;\n    unregister_kretprobe(rp);\n    synchronize_rcu();\n    kfree(rp);\n    *rp_ptr = NULL;\n}\n\nstatic int syscall_regfunc_handler(struct kretprobe_instance *ri,\n                                   struct pt_regs *regs)\n{\n    unsigned long flags;\n    spin_lock_irqsave(&tracepoint_reg_lock, flags);\n    if (tracepoint_reg_count < 1) {\n        // while install our tracepoint, mark our processes\n        ksu_mark_running_process_locked();\n    } else if (tracepoint_reg_count == 1) {\n        // while other tracepoint first added, mark all processes\n        ksu_mark_all_process();\n    }\n    tracepoint_reg_count++;\n    spin_unlock_irqrestore(&tracepoint_reg_lock, flags);\n    return 0;\n}\n\nstatic int syscall_unregfunc_handler(struct kretprobe_instance *ri,\n                                     struct pt_regs *regs)\n{\n    unsigned long flags;\n    spin_lock_irqsave(&tracepoint_reg_lock, flags);\n    tracepoint_reg_count--;\n    if (tracepoint_reg_count <= 0) {\n        // while no tracepoint left, unmark all processes\n        ksu_unmark_all_process();\n    } else if (tracepoint_reg_count == 1) {\n        // while just our tracepoint left, unmark disallowed processes\n        ksu_mark_running_process_locked();\n    }\n    spin_unlock_irqrestore(&tracepoint_reg_lock, flags);\n    return 0;\n}\n\nstatic struct kretprobe *syscall_regfunc_rp = NULL;\nstatic struct kretprobe *syscall_unregfunc_rp = NULL;\n#endif\n\nstatic inline bool check_syscall_fastpath(int nr)\n{\n    switch (nr) {\n    case __NR_newfstatat:\n    case __NR_faccessat:\n    case __NR_execve:\n    case __NR_setresuid:\n        return true;\n    default:\n        return false;\n    }\n}\n\n// Unmark init's child that are not zygote, adbd or ksud\nint ksu_handle_init_mark_tracker(const char __user **filename_user)\n{\n    char path[64];\n    unsigned long addr;\n    const char __user *fn;\n    long ret;\n\n    if (unlikely(!filename_user))\n        return 0;\n\n    addr = untagged_addr((unsigned long)*filename_user);\n    fn = (const char __user *)addr;\n\n    memset(path, 0, sizeof(path));\n    ret = strncpy_from_user_nofault(path, fn, sizeof(path));\n    if (ret < 0 && try_set_access_flag(addr)) {\n        ret = strncpy_from_user_nofault(path, fn, sizeof(path));\n        pr_info(\"ksu_handle_init_mark_tracker: %ld\\n\", ret);\n    }\n\n    if (unlikely(strcmp(path, KSUD_PATH) == 0)) {\n        pr_info(\"hook_manager: escape to root for init executing ksud: %d\\n\",\n                current->pid);\n        escape_to_root_for_init();\n    } else if (likely(strstr(path, \"/app_process\") == NULL &&\n                      strstr(path, \"/adbd\") == NULL)) {\n        pr_info(\"hook_manager: unmark %d exec %s\\n\", current->pid, path);\n        ksu_clear_task_tracepoint_flag_if_needed(current);\n    }\n\n    return 0;\n}\n\n#ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS\n// Generic sys_enter handler that dispatches to specific handlers\nstatic void ksu_sys_enter_handler(void *data, struct pt_regs *regs, long id)\n{\n    if (unlikely(check_syscall_fastpath(id))) {\n        if (ksu_su_compat_enabled) {\n            // Handle newfstatat\n            if (id == __NR_newfstatat) {\n                int *dfd = (int *)&PT_REGS_PARM1(regs);\n                const char __user **filename_user =\n                    (const char __user **)&PT_REGS_PARM2(regs);\n                int *flags = (int *)&PT_REGS_SYSCALL_PARM4(regs);\n                ksu_handle_stat(dfd, filename_user, flags);\n                return;\n            }\n\n            // Handle faccessat\n            if (id == __NR_faccessat) {\n                int *dfd = (int *)&PT_REGS_PARM1(regs);\n                const char __user **filename_user =\n                    (const char __user **)&PT_REGS_PARM2(regs);\n                int *mode = (int *)&PT_REGS_PARM3(regs);\n                ksu_handle_faccessat(dfd, filename_user, mode, NULL);\n                return;\n            }\n\n            // Handle execve\n            if (id == __NR_execve) {\n                const char __user **filename_user =\n                    (const char __user **)&PT_REGS_PARM1(regs);\n                if (current->pid != 1 && is_init(get_current_cred())) {\n                    ksu_handle_init_mark_tracker(filename_user);\n                } else {\n                    ksu_handle_execve_sucompat(filename_user, NULL, NULL, NULL);\n                }\n                return;\n            }\n        }\n\n        // Handle setresuid\n        if (id == __NR_setresuid) {\n            uid_t ruid = (uid_t)PT_REGS_PARM1(regs);\n            uid_t euid = (uid_t)PT_REGS_PARM2(regs);\n            uid_t suid = (uid_t)PT_REGS_PARM3(regs);\n            ksu_handle_setresuid(ruid, euid, suid);\n            return;\n        }\n    }\n}\n#endif\n\nvoid ksu_syscall_hook_manager_init(void)\n{\n    int ret;\n    pr_info(\"hook_manager: ksu_hook_manager_init called\\n\");\n\n#ifdef CONFIG_KRETPROBES\n    // Register kretprobe for syscall_regfunc\n    syscall_regfunc_rp =\n        init_kretprobe(\"syscall_regfunc\", syscall_regfunc_handler);\n    // Register kretprobe for syscall_unregfunc\n    syscall_unregfunc_rp =\n        init_kretprobe(\"syscall_unregfunc\", syscall_unregfunc_handler);\n#endif\n\n#ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS\n    ret = register_trace_sys_enter(ksu_sys_enter_handler, NULL);\n#ifndef CONFIG_KRETPROBES\n    ksu_mark_running_process_locked();\n#endif\n    if (ret) {\n        pr_err(\"hook_manager: failed to register sys_enter tracepoint: %d\\n\",\n               ret);\n    } else {\n        pr_info(\"hook_manager: sys_enter tracepoint registered\\n\");\n    }\n#endif\n\n    ksu_setuid_hook_init();\n    ksu_sucompat_init();\n}\n\nvoid ksu_syscall_hook_manager_exit(void)\n{\n    pr_info(\"hook_manager: ksu_hook_manager_exit called\\n\");\n#ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS\n    unregister_trace_sys_enter(ksu_sys_enter_handler, NULL);\n    tracepoint_synchronize_unregister();\n    pr_info(\"hook_manager: sys_enter tracepoint unregistered\\n\");\n#endif\n\n#ifdef CONFIG_KRETPROBES\n    destroy_kretprobe(&syscall_regfunc_rp);\n    destroy_kretprobe(&syscall_unregfunc_rp);\n#endif\n\n    ksu_sucompat_exit();\n    ksu_setuid_hook_exit();\n}\n"
  },
  {
    "path": "kernel/syscall_hook_manager.h",
    "content": "#ifndef __KSU_H_HOOK_MANAGER\n#define __KSU_H_HOOK_MANAGER\n\n#include <linux/version.h>\n#include <linux/sched.h>\n#include <linux/thread_info.h>\n\n// Hook manager initialization and cleanup\nvoid ksu_syscall_hook_manager_init(void);\nvoid ksu_syscall_hook_manager_exit(void);\n\n// Process marking for tracepoint\nvoid ksu_mark_all_process(void);\nvoid ksu_unmark_all_process(void);\nvoid ksu_mark_running_process(void);\n\n// Per-task mark operations\nint ksu_get_task_mark(pid_t pid);\nint ksu_set_task_mark(pid_t pid, bool mark);\n\nstatic inline void ksu_set_task_tracepoint_flag(struct task_struct *t)\n{\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)\n    set_task_syscall_work(t, SYSCALL_TRACEPOINT);\n#else\n    set_tsk_thread_flag(t, TIF_SYSCALL_TRACEPOINT);\n#endif\n}\n\nstatic inline void ksu_clear_task_tracepoint_flag(struct task_struct *t)\n{\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)\n    clear_task_syscall_work(t, SYSCALL_TRACEPOINT);\n#else\n    clear_tsk_thread_flag(t, TIF_SYSCALL_TRACEPOINT);\n#endif\n}\n\nvoid ksu_clear_task_tracepoint_flag_if_needed(struct task_struct *t);\n\n#endif\n"
  },
  {
    "path": "kernel/throne_tracker.c",
    "content": "#include <linux/err.h>\n#include <linux/fs.h>\n#include <linux/list.h>\n#include <linux/slab.h>\n#include <linux/string.h>\n#include <linux/types.h>\n#include <linux/version.h>\n\n#include \"allowlist.h\"\n#include \"apk_sign.h\"\n#include \"klog.h\" // IWYU pragma: keep\n#include \"manager.h\"\n#include \"throne_tracker.h\"\n\nuid_t ksu_manager_appid = KSU_INVALID_APPID;\n\n#define SYSTEM_PACKAGES_LIST_PATH \"/data/system/packages.list\"\n\nstruct uid_data {\n    struct list_head list;\n    u32 uid;\n    char package[KSU_MAX_PACKAGE_NAME];\n};\n\nstatic void crown_manager(const char *apk, struct list_head *uid_data)\n{\n    char pkg[KSU_MAX_PACKAGE_NAME];\n    if (get_pkg_from_apk_path(pkg, apk) < 0) {\n        pr_err(\"Failed to get package name from apk path: %s\\n\", apk);\n        return;\n    }\n\n    pr_info(\"manager pkg: %s\\n\", pkg);\n\n    struct list_head *list = (struct list_head *)uid_data;\n    struct uid_data *np;\n\n    list_for_each_entry (np, list, list) {\n        if (strncmp(np->package, pkg, KSU_MAX_PACKAGE_NAME) == 0) {\n            pr_info(\"Crowning manager: %s(uid=%d)\\n\", pkg, np->uid);\n            ksu_set_manager_appid(np->uid);\n            break;\n        }\n    }\n}\n\n#define DATA_PATH_LEN 384 // 384 is enough for /data/app/<package>/base.apk\n\nstruct data_path {\n    char dirpath[DATA_PATH_LEN];\n    int depth;\n    struct list_head list;\n};\n\nstruct apk_path_hash {\n    unsigned int hash;\n    bool exists;\n    struct list_head list;\n};\n\nstatic struct list_head apk_path_hash_list = LIST_HEAD_INIT(apk_path_hash_list);\n\nstruct my_dir_context {\n    struct dir_context ctx;\n    struct list_head *data_path_list;\n    char *parent_dir;\n    void *private_data;\n    int depth;\n    int *stop;\n};\n// https://docs.kernel.org/filesystems/porting.html\n// filldir_t (readdir callbacks) calling conventions have changed. Instead of returning 0 or -E... it returns bool now. false means \"no more\" (as -E... used to) and true - \"keep going\" (as 0 in old calling conventions). Rationale: callers never looked at specific -E... values anyway. -> iterate_shared() instances require no changes at all, all filldir_t ones in the tree converted.\n#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 1, 0)\n#define FILLDIR_RETURN_TYPE bool\n#define FILLDIR_ACTOR_CONTINUE true\n#define FILLDIR_ACTOR_STOP false\n#else\n#define FILLDIR_RETURN_TYPE int\n#define FILLDIR_ACTOR_CONTINUE 0\n#define FILLDIR_ACTOR_STOP -EINVAL\n#endif\nextern bool is_manager_apk(char *path);\nFILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name,\n                             int namelen, loff_t off, u64 ino,\n                             unsigned int d_type)\n{\n    struct my_dir_context *my_ctx =\n        container_of(ctx, struct my_dir_context, ctx);\n    char dirpath[DATA_PATH_LEN];\n\n    if (!my_ctx) {\n        pr_err(\"Invalid context\\n\");\n        return FILLDIR_ACTOR_STOP;\n    }\n    if (my_ctx->stop && *my_ctx->stop) {\n        pr_info(\"Stop searching\\n\");\n        return FILLDIR_ACTOR_STOP;\n    }\n\n    if (!strncmp(name, \"..\", namelen) || !strncmp(name, \".\", namelen))\n        return FILLDIR_ACTOR_CONTINUE; // Skip \".\" and \"..\"\n\n    if (d_type == DT_DIR && namelen >= 8 && !strncmp(name, \"vmdl\", 4) &&\n        !strncmp(name + namelen - 4, \".tmp\", 4)) {\n        pr_info(\"Skipping directory: %.*s\\n\", namelen, name);\n        return FILLDIR_ACTOR_CONTINUE; // Skip staging package\n    }\n\n    if (snprintf(dirpath, DATA_PATH_LEN, \"%s/%.*s\", my_ctx->parent_dir, namelen,\n                 name) >= DATA_PATH_LEN) {\n        pr_err(\"Path too long: %s/%.*s\\n\", my_ctx->parent_dir, namelen, name);\n        return FILLDIR_ACTOR_CONTINUE;\n    }\n\n    if (d_type == DT_DIR && my_ctx->depth > 0 &&\n        (my_ctx->stop && !*my_ctx->stop)) {\n        struct data_path *data = kzalloc(sizeof(struct data_path), GFP_ATOMIC);\n\n        if (!data) {\n            pr_err(\"Failed to allocate memory for %s\\n\", dirpath);\n            return FILLDIR_ACTOR_CONTINUE;\n        }\n\n        strscpy(data->dirpath, dirpath, DATA_PATH_LEN);\n        data->depth = my_ctx->depth - 1;\n        list_add_tail(&data->list, my_ctx->data_path_list);\n    } else {\n        if ((namelen == 8) && (strncmp(name, \"base.apk\", namelen) == 0)) {\n            struct apk_path_hash *pos, *n;\n            unsigned int hash = full_name_hash(NULL, dirpath, strlen(dirpath));\n            list_for_each_entry (pos, &apk_path_hash_list, list) {\n                if (hash == pos->hash) {\n                    pos->exists = true;\n                    return FILLDIR_ACTOR_CONTINUE;\n                }\n            }\n\n            bool is_manager = is_manager_apk(dirpath);\n            pr_info(\"Found new base.apk at path: %s, is_manager: %d\\n\", dirpath,\n                    is_manager);\n            if (is_manager) {\n                crown_manager(dirpath, my_ctx->private_data);\n                *my_ctx->stop = 1;\n\n                // Manager found, clear APK cache list\n                list_for_each_entry_safe (pos, n, &apk_path_hash_list, list) {\n                    list_del(&pos->list);\n                    kfree(pos);\n                }\n            } else {\n                struct apk_path_hash *apk_data =\n                    kzalloc(sizeof(struct apk_path_hash), GFP_ATOMIC);\n                apk_data->hash = hash;\n                apk_data->exists = true;\n                list_add_tail(&apk_data->list, &apk_path_hash_list);\n            }\n        }\n    }\n\n    return FILLDIR_ACTOR_CONTINUE;\n}\n\nvoid search_manager(const char *path, int depth, struct list_head *uid_data)\n{\n    int i, stop = 0;\n    struct list_head data_path_list;\n    INIT_LIST_HEAD(&data_path_list);\n    unsigned long data_app_magic = 0;\n\n    // Initialize APK cache list\n    struct apk_path_hash *pos, *n;\n    list_for_each_entry (pos, &apk_path_hash_list, list) {\n        pos->exists = false;\n    }\n\n    // First depth\n    struct data_path data;\n    strscpy(data.dirpath, path, DATA_PATH_LEN);\n    data.depth = depth;\n    list_add_tail(&data.list, &data_path_list);\n\n    for (i = depth; i >= 0; i--) {\n        struct data_path *pos, *n;\n\n        list_for_each_entry_safe (pos, n, &data_path_list, list) {\n            struct my_dir_context ctx = { .ctx.actor = my_actor,\n                                          .data_path_list = &data_path_list,\n                                          .parent_dir = pos->dirpath,\n                                          .private_data = uid_data,\n                                          .depth = pos->depth,\n                                          .stop = &stop };\n            struct file *file;\n\n            if (!stop) {\n                file = filp_open(pos->dirpath, O_RDONLY | O_NOFOLLOW, 0);\n                if (IS_ERR(file)) {\n                    pr_err(\"Failed to open directory: %s, err: %ld\\n\",\n                           pos->dirpath, PTR_ERR(file));\n                    goto skip_iterate;\n                }\n\n                // grab magic on first folder, which is /data/app\n                if (!data_app_magic) {\n                    if (file->f_inode->i_sb->s_magic) {\n                        data_app_magic = file->f_inode->i_sb->s_magic;\n                        pr_info(\"%s: dir: %s got magic! 0x%lx\\n\", __func__,\n                                pos->dirpath, data_app_magic);\n                    } else {\n                        filp_close(file, NULL);\n                        goto skip_iterate;\n                    }\n                }\n\n                if (file->f_inode->i_sb->s_magic != data_app_magic) {\n                    pr_info(\"%s: skip: %s magic: 0x%lx expected: 0x%lx\\n\",\n                            __func__, pos->dirpath,\n                            file->f_inode->i_sb->s_magic, data_app_magic);\n                    filp_close(file, NULL);\n                    goto skip_iterate;\n                }\n\n                iterate_dir(file, &ctx.ctx);\n                filp_close(file, NULL);\n            }\n        skip_iterate:\n            list_del(&pos->list);\n            if (pos != &data)\n                kfree(pos);\n        }\n    }\n\n    // Remove stale cached APK entries\n    list_for_each_entry_safe (pos, n, &apk_path_hash_list, list) {\n        if (!pos->exists) {\n            list_del(&pos->list);\n            kfree(pos);\n        }\n    }\n}\n\nstatic bool is_uid_exist(uid_t uid, char *package, void *data)\n{\n    struct list_head *list = (struct list_head *)data;\n    struct uid_data *np;\n\n    bool exist = false;\n    list_for_each_entry (np, list, list) {\n        if (np->uid == uid % PER_USER_RANGE &&\n            strncmp(np->package, package, KSU_MAX_PACKAGE_NAME) == 0) {\n            exist = true;\n            break;\n        }\n    }\n    return exist;\n}\n\nvoid track_throne(bool prune_only)\n{\n    struct file *fp = filp_open(SYSTEM_PACKAGES_LIST_PATH, O_RDONLY, 0);\n    if (IS_ERR(fp)) {\n        pr_err(\"%s: open \" SYSTEM_PACKAGES_LIST_PATH \" failed: %ld\\n\", __func__,\n               PTR_ERR(fp));\n        return;\n    }\n\n    struct list_head uid_list;\n    INIT_LIST_HEAD(&uid_list);\n\n    char chr = 0;\n    loff_t pos = 0;\n    loff_t line_start = 0;\n    char buf[KSU_MAX_PACKAGE_NAME];\n    for (;;) {\n        ssize_t count = kernel_read(fp, &chr, sizeof(chr), &pos);\n        if (count != sizeof(chr))\n            break;\n        if (chr != '\\n')\n            continue;\n\n        count = kernel_read(fp, buf, sizeof(buf), &line_start);\n\n        struct uid_data *data = kzalloc(sizeof(struct uid_data), GFP_ATOMIC);\n        if (!data) {\n            filp_close(fp, 0);\n            goto out;\n        }\n\n        char *tmp = buf;\n        const char *delim = \" \";\n        char *package = strsep(&tmp, delim);\n        char *uid = strsep(&tmp, delim);\n        if (!uid || !package) {\n            kfree(data);\n            pr_err(\"update_uid: package or uid is NULL!\\n\");\n            break;\n        }\n\n        u32 res;\n        if (kstrtou32(uid, 10, &res)) {\n            kfree(data);\n            pr_err(\"update_uid: uid parse err\\n\");\n            break;\n        }\n        data->uid = res;\n        strncpy(data->package, package, KSU_MAX_PACKAGE_NAME);\n        list_add_tail(&data->list, &uid_list);\n        // reset line start\n        line_start = pos;\n    }\n    filp_close(fp, 0);\n\n    // now update uid list\n    struct uid_data *np;\n    struct uid_data *n;\n\n    if (prune_only)\n        goto prune;\n\n    // first, check if manager_uid exist!\n    bool manager_exist = false;\n    list_for_each_entry (np, &uid_list, list) {\n        if (np->uid == ksu_get_manager_appid()) {\n            manager_exist = true;\n            break;\n        }\n    }\n\n    if (!manager_exist) {\n        if (ksu_is_manager_appid_valid()) {\n            pr_info(\"manager is uninstalled, invalidate it!\\n\");\n            ksu_invalidate_manager_uid();\n            goto prune;\n        }\n        pr_info(\"Searching manager...\\n\");\n        search_manager(\"/data/app\", 2, &uid_list);\n        pr_info(\"Search manager finished\\n\");\n    }\n\nprune:\n    // then prune the allowlist\n    ksu_prune_allowlist(is_uid_exist, &uid_list);\nout:\n    // free uid_list\n    list_for_each_entry_safe (np, n, &uid_list, list) {\n        list_del(&np->list);\n        kfree(np);\n    }\n}\n\nvoid ksu_throne_tracker_init()\n{\n    // nothing to do\n}\n\nvoid ksu_throne_tracker_exit()\n{\n    // nothing to do\n}\n"
  },
  {
    "path": "kernel/throne_tracker.h",
    "content": "#ifndef __KSU_H_UID_OBSERVER\n#define __KSU_H_UID_OBSERVER\n\nvoid ksu_throne_tracker_init();\n\nvoid ksu_throne_tracker_exit();\n\nvoid track_throne(bool prune_only);\n\n#endif\n"
  },
  {
    "path": "kernel/tools/check_symbol.c",
    "content": "#include <stdio.h>\n#include <stdlib.h>\n#include <string.h>\n#include <elf.h>\n#include <fcntl.h>\n#include <unistd.h>\n#include <sys/mman.h>\n#include <sys/stat.h>\n\ntypedef struct {\n    void *data;\n    size_t size;\n    Elf64_Ehdr *ehdr;\n    Elf64_Shdr *shdr;\n    char *shstrtab;\n} ElfFile;\n\nint open_elf(const char *path, ElfFile *elf)\n{\n    int fd = open(path, O_RDONLY);\n    if (fd < 0) {\n        fprintf(stderr, \"Error: Cannot open file %s\\n\", path);\n        return -1;\n    }\n\n    struct stat st;\n    if (fstat(fd, &st) < 0) {\n        fprintf(stderr, \"Error: Cannot stat file %s\\n\", path);\n        close(fd);\n        return -1;\n    }\n\n    elf->size = st.st_size;\n    elf->data = mmap(NULL, elf->size, PROT_READ, MAP_PRIVATE, fd, 0);\n    close(fd);\n\n    if (elf->data == MAP_FAILED) {\n        fprintf(stderr, \"Error: Cannot mmap file %s\\n\", path);\n        return -1;\n    }\n\n    elf->ehdr = (Elf64_Ehdr *)elf->data;\n\n    if (memcmp(elf->ehdr->e_ident, ELFMAG, SELFMAG) != 0) {\n        fprintf(stderr, \"Error: %s is not a valid ELF file\\n\", path);\n        munmap(elf->data, elf->size);\n        return -1;\n    }\n\n    if (elf->ehdr->e_ident[EI_CLASS] != ELFCLASS64) {\n        fprintf(stderr, \"Error: %s is not a 64-bit ELF file\\n\", path);\n        munmap(elf->data, elf->size);\n        return -1;\n    }\n\n    elf->shdr = (Elf64_Shdr *)((char *)elf->data + elf->ehdr->e_shoff);\n\n    elf->shstrtab =\n        (char *)elf->data + elf->shdr[elf->ehdr->e_shstrndx].sh_offset;\n\n    return 0;\n}\n\nvoid close_elf(ElfFile *elf)\n{\n    munmap(elf->data, elf->size);\n}\n\nElf64_Shdr *find_symtab(ElfFile *elf)\n{\n    for (int i = 0; i < elf->ehdr->e_shnum; i++) {\n        if (elf->shdr[i].sh_type == SHT_SYMTAB) {\n            return &elf->shdr[i];\n        }\n    }\n    return NULL;\n}\n\nElf64_Shdr *find_section(ElfFile *elf, const char *name)\n{\n    for (int i = 0; i < elf->ehdr->e_shnum; i++) {\n        const char *section_name = elf->shstrtab + elf->shdr[i].sh_name;\n        if (strcmp(section_name, name) == 0) {\n            return &elf->shdr[i];\n        }\n    }\n    return NULL;\n}\n\nElf64_Sym *find_symbol(ElfFile *elf, const char *name, Elf64_Shdr *symtab,\n                       char *strtab)\n{\n    Elf64_Sym *syms = (Elf64_Sym *)((char *)elf->data + symtab->sh_offset);\n    int sym_count = symtab->sh_size / sizeof(Elf64_Sym);\n\n    for (int i = 0; i < sym_count; i++) {\n        const char *sym_name = strtab + syms[i].st_name;\n        if (strcmp(sym_name, name) == 0) {\n            return &syms[i];\n        }\n    }\n    return NULL;\n}\n\nint main(int argc, char *argv[])\n{\n    if (argc != 3) {\n        fprintf(stderr, \"Usage: %s <ko_elf> <vmlinux>\\n\", argv[0]);\n        return 1;\n    }\n\n    const char *ko_path = argv[1];\n    const char *vmlinux_path = argv[2];\n\n    ElfFile ko_elf, vmlinux;\n\n    if (open_elf(ko_path, &ko_elf) < 0) {\n        return 1;\n    }\n\n    if (open_elf(vmlinux_path, &vmlinux) < 0) {\n        close_elf(&ko_elf);\n        return 1;\n    }\n\n    Elf64_Shdr *ko_symtab = find_symtab(&ko_elf);\n    Elf64_Shdr *vmlinux_symtab = find_symtab(&vmlinux);\n    Elf64_Shdr *ko_version_sec = find_section(&ko_elf, \"__versions\");\n\n    if (!ko_symtab) {\n        fprintf(stderr, \"Error: No symbol table found in %s\\n\", ko_path);\n        close_elf(&ko_elf);\n        close_elf(&vmlinux);\n        return 1;\n    }\n\n    if (!vmlinux_symtab) {\n        fprintf(stderr, \"Error: No symbol table found in %s\\n\", vmlinux_path);\n        close_elf(&ko_elf);\n        close_elf(&vmlinux);\n        return 1;\n    }\n\n    if (!ko_version_sec) {\n        fprintf(stderr, \"Error: No __versions section found in %s\\n\", ko_path);\n        close_elf(&ko_elf);\n        close_elf(&vmlinux);\n        return 1;\n    }\n\n    if (ko_version_sec->sh_size != 0) {\n        fprintf(\n            stderr,\n            \"Error: __versions section in %s must have size 0 (actual=%llu)\\n\",\n            ko_path, (unsigned long long)ko_version_sec->sh_size);\n        close_elf(&ko_elf);\n        close_elf(&vmlinux);\n        return 1;\n    }\n\n    char *ko_strtab =\n        (char *)ko_elf.data + ko_elf.shdr[ko_symtab->sh_link].sh_offset;\n    char *vmlinux_strtab =\n        (char *)vmlinux.data + vmlinux.shdr[vmlinux_symtab->sh_link].sh_offset;\n\n    Elf64_Sym *ko_syms =\n        (Elf64_Sym *)((char *)ko_elf.data + ko_symtab->sh_offset);\n    int ko_sym_count = ko_symtab->sh_size / sizeof(Elf64_Sym);\n\n    int has_error = 0;\n\n    for (int i = 0; i < ko_sym_count; i++) {\n        if (ko_syms[i].st_shndx == SHN_UNDEF && ko_syms[i].st_name != 0) {\n            const char *sym_name = ko_strtab + ko_syms[i].st_name;\n\n            Elf64_Sym *vmlinux_sym =\n                find_symbol(&vmlinux, sym_name, vmlinux_symtab, vmlinux_strtab);\n\n            if (!vmlinux_sym || vmlinux_sym->st_shndx == SHN_UNDEF) {\n                fprintf(stderr,\n                        \"Error: Symbol '%s' not found or undefined in %s\\n\",\n                        sym_name, vmlinux_path);\n                has_error = 1;\n            } else {\n                int binding = ELF64_ST_BIND(vmlinux_sym->st_info);\n                if (binding != STB_GLOBAL && binding != STB_WEAK) {\n                    fprintf(\n                        stderr,\n                        \"Warning: Symbol '%s' is defined in %s but not global (binding=%d)\\n\",\n                        sym_name, vmlinux_path, binding);\n                }\n            }\n        }\n    }\n\n    close_elf(&ko_elf);\n    close_elf(&vmlinux);\n\n    return has_error ? 1 : 0;\n}\n"
  },
  {
    "path": "kernel/util.c",
    "content": "#include <linux/mm.h>\n#include <linux/pgtable.h>\n#include <linux/printk.h>\n#include <asm/current.h>\n\n#include \"util.h\"\n\nbool try_set_access_flag(unsigned long addr)\n{\n#ifdef CONFIG_ARM64\n    struct mm_struct *mm = current->mm;\n    struct vm_area_struct *vma;\n    pgd_t *pgd;\n    p4d_t *p4d;\n    pud_t *pud;\n    pmd_t *pmd;\n    pte_t *ptep, pte;\n    spinlock_t *ptl;\n    bool ret = false;\n\n    if (!mm)\n        return false;\n\n    if (!mmap_read_trylock(mm))\n        return false;\n\n    vma = find_vma(mm, addr);\n    if (!vma || addr < vma->vm_start)\n        goto out_unlock;\n\n    pgd = pgd_offset(mm, addr);\n    if (!pgd_present(*pgd))\n        goto out_unlock;\n\n    p4d = p4d_offset(pgd, addr);\n    if (!p4d_present(*p4d))\n        goto out_unlock;\n\n    pud = pud_offset(p4d, addr);\n    if (!pud_present(*pud))\n        goto out_unlock;\n\n    pmd = pmd_offset(pud, addr);\n    if (!pmd_present(*pmd))\n        goto out_unlock;\n\n    if (pmd_trans_huge(*pmd))\n        goto out_unlock;\n\n    ptep = pte_offset_map_lock(mm, pmd, addr, &ptl);\n    if (!ptep)\n        goto out_unlock;\n\n    pte = *ptep;\n\n    if (!pte_present(pte))\n        goto out_pte_unlock;\n\n    if (pte_young(pte)) {\n        ret = true;\n        goto out_pte_unlock;\n    }\n\n    ptep_set_access_flags(vma, addr, ptep, pte_mkyoung(pte), 0);\n    pr_info(\"set AF for addr %lx\\n\", addr);\n    ret = true;\n\nout_pte_unlock:\n    pte_unmap_unlock(ptep, ptl);\nout_unlock:\n    mmap_read_unlock(mm);\n    return ret;\n#else\n    return false;\n#endif\n}\n"
  },
  {
    "path": "kernel/util.h",
    "content": "#ifndef __KSU_UTIL_H\n#define __KSU_UTIL_H\n\n#include <linux/types.h>\n\n#ifndef preempt_enable_no_resched_notrace\n#define preempt_enable_no_resched_notrace()                                    \\\n    do {                                                                       \\\n        barrier();                                                             \\\n        __preempt_count_dec();                                                 \\\n    } while (0)\n#endif\n\n#ifndef preempt_disable_notrace\n#define preempt_disable_notrace()                                              \\\n    do {                                                                       \\\n        __preempt_count_inc();                                                 \\\n        barrier();                                                             \\\n    } while (0)\n#endif\n\nbool try_set_access_flag(unsigned long addr);\n\n#endif\n"
  },
  {
    "path": "manager/.gitignore",
    "content": "*.iml\n.gradle\n.idea\n.kotlin\n.DS_Store\nbuild\ncaptures\n.cxx\nlocal.properties\nkey.jks\n"
  },
  {
    "path": "manager/app/.gitignore",
    "content": "/build\n/release/\n"
  },
  {
    "path": "manager/app/build.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nplugins {\n    alias(libs.plugins.agp.app)\n    alias(libs.plugins.compose.compiler)\n    alias(libs.plugins.kotlin.serialization)\n    alias(libs.plugins.lsplugin.apksign)\n    id(\"kotlin-parcelize\")\n}\n\nval androidCompileSdkVersion: Int by rootProject.extra\nval androidCompileNdkVersion: String by rootProject.extra\nval androidBuildToolsVersion: String by rootProject.extra\nval androidMinSdkVersion: Int by rootProject.extra\nval androidTargetSdkVersion: Int by rootProject.extra\nval androidSourceCompatibility: JavaVersion by rootProject.extra\nval androidTargetCompatibility: JavaVersion by rootProject.extra\nval managerVersionCode: Int by rootProject.extra\nval managerVersionName: String by rootProject.extra\n\napksign {\n    storeFileProperty = \"KEYSTORE_FILE\"\n    storePasswordProperty = \"KEYSTORE_PASSWORD\"\n    keyAliasProperty = \"KEY_ALIAS\"\n    keyPasswordProperty = \"KEY_PASSWORD\"\n}\n\nval baseCFlags = listOf(\n    \"-Wall\", \"-Qunused-arguments\", \"-fvisibility=hidden\", \"-fvisibility-inlines-hidden\",\n    \"-fno-exceptions\", \"-fno-stack-protector\", \"-fomit-frame-pointer\",\n    \"-Wno-builtin-macro-redefined\", \"-Wno-unused-value\", \"-D__FILE__=__FILE_NAME__\"\n)\nval baseCppFlags = baseCFlags + \"-fno-rtti\"\n\nandroid {\n    namespace = \"me.weishu.kernelsu\"\n\n    buildTypes {\n        debug {\n            externalNativeBuild {\n                cmake {\n                    arguments += listOf(\"-DCMAKE_CXX_FLAGS_DEBUG=-Og\", \"-DCMAKE_C_FLAGS_DEBUG=-Og\")\n                }\n            }\n        }\n        release {\n            isMinifyEnabled = true\n            isShrinkResources = true\n            vcsInfo.include = false\n            proguardFiles(getDefaultProguardFile(\"proguard-android-optimize.txt\"), \"proguard-rules.pro\")\n            externalNativeBuild {\n                cmake {\n                    arguments += \"-DDEBUG_SYMBOLS_PATH=${layout.buildDirectory.get().asFile.absolutePath}/symbols\"\n                    arguments += \"-DCMAKE_BUILD_TYPE=Release\"\n\n                    val releaseFlags = listOf(\n                        \"-flto\", \"-ffunction-sections\", \"-fdata-sections\", \"-Wl,--gc-sections\",\n                        \"-fno-unwind-tables\", \"-fno-asynchronous-unwind-tables\", \"-Wl,--exclude-libs,ALL\"\n                    )\n                    val configFlags = listOf(\"-Oz\", \"-DNDEBUG\").joinToString(\" \")\n\n                    cppFlags += releaseFlags\n                    cFlags += releaseFlags\n\n                    arguments += listOf(\n                        \"-DCMAKE_CXX_FLAGS_RELEASE=$configFlags\",\n                        \"-DCMAKE_C_FLAGS_RELEASE=$configFlags\",\n                        \"-DCMAKE_SHARED_LINKER_FLAGS=-Wl,--gc-sections -Wl,--exclude-libs,ALL -Wl,--icf=all -s -Wl,--hash-style=sysv -Wl,-z,norelro\"\n                    )\n                }\n            }\n        }\n    }\n\n    buildFeatures {\n        aidl = true\n        buildConfig = true\n        compose = true\n        prefab = true\n    }\n\n    packaging {\n        dex {\n            useLegacyPackaging = true\n        }\n        jniLibs {\n            useLegacyPackaging = true\n        }\n    }\n\n    externalNativeBuild {\n        cmake {\n            path = file(\"src/main/cpp/CMakeLists.txt\")\n        }\n    }\n\n    dependenciesInfo {\n        includeInApk = false\n        includeInBundle = false\n    }\n\n    androidResources {\n        generateLocaleConfig = true\n    }\n\n    compileSdk = androidCompileSdkVersion\n    ndkVersion = androidCompileNdkVersion\n    buildToolsVersion = androidBuildToolsVersion\n\n    defaultConfig {\n        minSdk = androidMinSdkVersion\n        targetSdk = androidTargetSdkVersion\n        versionCode = managerVersionCode\n        versionName = managerVersionName\n\n        val isPrBuild = project.findProperty(\"IS_PR_BUILD\")?.toString()?.toBoolean() ?: false\n        buildConfigField(\"boolean\", \"IS_PR_BUILD\", isPrBuild.toString())\n\n        externalNativeBuild {\n            cmake {\n                arguments += \"-DANDROID_STL=none\"\n                cFlags += baseCFlags + \"-std=c2x\"\n                cppFlags += baseCppFlags + \"-std=c++2b\"\n            }\n        }\n\n        ndk {\n            abiFilters += listOf(\"arm64-v8a\", \"x86_64\")\n        }\n    }\n\n    lint {\n        abortOnError = true\n        checkReleaseBuilds = false\n    }\n\n    compileOptions {\n        sourceCompatibility = androidSourceCompatibility\n        targetCompatibility = androidTargetCompatibility\n    }\n}\n\nandroidComponents {\n    onVariants(selector().withBuildType(\"release\")) {\n        it.packaging.resources.excludes.addAll(listOf(\"META-INF/**\", \"kotlin/**\", \"org/**\", \"**.bin\"))\n    }\n}\n\nbase {\n    archivesName.set(\n        \"KernelSU_${managerVersionName}_${managerVersionCode}\"\n    )\n}\n\ndependencies {\n    implementation(libs.androidx.activity.compose)\n\n    implementation(platform(libs.androidx.compose.bom))\n    implementation(libs.androidx.compose.material.icons.extended)\n    implementation(libs.androidx.compose.material3)\n    implementation(libs.androidx.compose.ui)\n    implementation(libs.androidx.compose.ui.tooling.preview)\n\n    debugImplementation(libs.androidx.compose.ui.test.manifest)\n    debugImplementation(libs.androidx.compose.ui.tooling)\n\n    implementation(libs.androidx.lifecycle.runtime.compose)\n    implementation(libs.androidx.lifecycle.runtime.ktx)\n    implementation(libs.androidx.lifecycle.viewmodel.compose)\n    implementation(libs.androidx.lifecycle.viewmodel.navigation3)\n\n    implementation(libs.androidx.navigation3.runtime)\n    implementation(libs.androidx.navigationevent.compose)\n\n    implementation(libs.com.github.topjohnwu.libsu.core)\n    implementation(libs.com.github.topjohnwu.libsu.service)\n    implementation(libs.com.github.topjohnwu.libsu.io)\n\n    implementation(libs.dev.rikka.rikkax.parcelablelist)\n\n    implementation(libs.kotlinx.coroutines.core)\n\n    implementation(libs.markwon)\n\n    implementation(libs.androidx.webkit)\n\n    implementation(libs.lsposed.cxx)\n\n    implementation(libs.hiddenapibypass)\n\n    implementation(libs.miuix)\n    implementation(libs.miuix.icons)\n    implementation(libs.miuix.navigation3.ui)\n\n    implementation(platform(libs.okhttp.bom))\n    implementation(libs.okhttp)\n\n    implementation(libs.backdrop)\n    implementation(libs.capsule)\n    implementation(libs.haze)\n\n    implementation(libs.material.kolor)\n}\n"
  },
  {
    "path": "manager/app/proguard-rules.pro",
    "content": ""
  },
  {
    "path": "manager/app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.RECEIVE_BOOT_COMPLETED\" />\n\n    <application\n        android:name=\".KernelSUApplication\"\n        android:allowBackup=\"true\"\n        android:dataExtractionRules=\"@xml/data_extraction_rules\"\n        android:fullBackupContent=\"@xml/backup_rules\"\n        android:hasFragileUserData=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.KernelSU\"\n        android:directBootAware=\"true\"\n        android:zygotePreloadName=\"me.weishu.kernelsu.magica.AppZygotePreload\">\n        <activity\n            android:name=\".ui.MainActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/Theme.KernelSU\"\n            android:windowSoftInputMode=\"adjustResize\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"android.intent.action.VIEW\" />\n\n                <category android:name=\"android.intent.category.DEFAULT\" />\n                <category android:name=\"android.intent.category.BROWSABLE\" />\n\n                <data\n                    android:mimeType=\"application/zip\"\n                    android:scheme=\"content\" />\n            </intent-filter>\n        </activity>\n\n        <activity\n            android:name=\".ui.webui.WebUIActivity\"\n            android:autoRemoveFromRecents=\"true\"\n            android:configChanges=\"density|orientation|screenSize|screenLayout|smallestScreenSize|uiMode|locale|layoutDirection|fontScale|keyboard|keyboardHidden\"\n            android:documentLaunchMode=\"intoExisting\"\n            android:exported=\"false\"\n            android:taskAffinity=\"${applicationId}.webui\"\n            android:theme=\"@style/Theme.KernelSU.WebUI\" />\n\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/filepaths\" />\n        </provider>\n\n        <service\n            android:name=\".magica.MagicaService\"\n            android:directBootAware=\"true\"\n            android:exported=\"false\"\n            android:isolatedProcess=\"true\"\n            android:useAppZygote=\"true\" />\n\n        <receiver\n            android:name=\".magica.BootCompletedReceiver\"\n            android:directBootAware=\"true\"\n            android:enabled=\"false\"\n            android:exported=\"true\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.LOCKED_BOOT_COMPLETED\" />\n                <action android:name=\"android.intent.action.BOOT_COMPLETED\" />\n            </intent-filter>\n            <intent-filter>\n                <action android:name=\"me.weishu.kernelsu.magica.LAUNCH\" />\n            </intent-filter>\n        </receiver>\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "manager/app/src/main/aidl/me/weishu/kernelsu/IKsuInterface.aidl",
    "content": "// IKsuInterface.aidl\npackage me.weishu.kernelsu;\n\nimport android.content.pm.PackageInfo;\nimport rikka.parcelablelist.ParcelableListSlice;\n\ninterface IKsuInterface {\n    ParcelableListSlice<PackageInfo> getPackages(int flags);\n\n    int[] getUserIds();\n}"
  },
  {
    "path": "manager/app/src/main/assets/github-markdown.css",
    "content": "/* https://raw.githubusercontent.com/sindresorhus/github-markdown-css/gh-pages/github-markdown.css */\n.markdown-body {\n  --base-size-4: 0.25rem;\n  --base-size-8: 0.5rem;\n  --base-size-16: 1rem;\n  --base-size-24: 1.5rem;\n  --base-size-40: 2.5rem;\n  --base-text-weight-normal: 400;\n  --base-text-weight-medium: 500;\n  --base-text-weight-semibold: 600;\n  --fontStack-monospace: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;\n  --fgColor-accent: Highlight;\n}\n\n[data-theme=\"dark\"] {\n  color-scheme: dark;\n  --focus-outlineColor: #1f6feb;\n  --fgColor-default: #f0f6fc;\n  --fgColor-muted: #9198a1;\n  --fgColor-accent: #4493f8;\n  --fgColor-success: #3fb950;\n  --fgColor-attention: #d29922;\n  --fgColor-danger: #f85149;\n  --fgColor-done: #ab7df8;\n  --bgColor-default: #0d1117;\n  --bgColor-muted: #151b23;\n  --bgColor-neutral-muted: #656c7633;\n  --bgColor-attention-muted: #bb800926;\n  --borderColor-default: #3d444d;\n  --borderColor-muted: #3d444db3;\n  --borderColor-neutral-muted: #3d444db3;\n  --borderColor-accent-emphasis: #1f6feb;\n  --borderColor-success-emphasis: #238636;\n  --borderColor-attention-emphasis: #9e6a03;\n  --borderColor-danger-emphasis: #da3633;\n  --borderColor-done-emphasis: #8957e5;\n  --color-prettylights-syntax-comment: #9198a1;\n  --color-prettylights-syntax-constant: #79c0ff;\n  --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;\n  --color-prettylights-syntax-entity: #d2a8ff;\n  --color-prettylights-syntax-storage-modifier-import: #f0f6fc;\n  --color-prettylights-syntax-entity-tag: #7ee787;\n  --color-prettylights-syntax-keyword: #ff7b72;\n  --color-prettylights-syntax-string: #a5d6ff;\n  --color-prettylights-syntax-variable: #ffa657;\n  --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;\n  --color-prettylights-syntax-brackethighlighter-angle: #9198a1;\n  --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;\n  --color-prettylights-syntax-invalid-illegal-bg: #8e1519;\n  --color-prettylights-syntax-carriage-return-text: #f0f6fc;\n  --color-prettylights-syntax-carriage-return-bg: #b62324;\n  --color-prettylights-syntax-string-regexp: #7ee787;\n  --color-prettylights-syntax-markup-list: #f2cc60;\n  --color-prettylights-syntax-markup-heading: #1f6feb;\n  --color-prettylights-syntax-markup-italic: #f0f6fc;\n  --color-prettylights-syntax-markup-bold: #f0f6fc;\n  --color-prettylights-syntax-markup-deleted-text: #ffdcd7;\n  --color-prettylights-syntax-markup-deleted-bg: #67060c;\n  --color-prettylights-syntax-markup-inserted-text: #aff5b4;\n  --color-prettylights-syntax-markup-inserted-bg: #033a16;\n  --color-prettylights-syntax-markup-changed-text: #ffdfb6;\n  --color-prettylights-syntax-markup-changed-bg: #5a1e02;\n  --color-prettylights-syntax-markup-ignored-text: #f0f6fc;\n  --color-prettylights-syntax-markup-ignored-bg: #1158c7;\n  --color-prettylights-syntax-meta-diff-range: #d2a8ff;\n  --color-prettylights-syntax-sublimelinter-gutter-mark: #3d444d;\n}\n\n[data-theme=\"light\"] {\n  color-scheme: light;\n  --focus-outlineColor: #0969da;\n  --fgColor-default: #1f2328;\n  --fgColor-muted: #59636e;\n  --fgColor-accent: #0969da;\n  --fgColor-success: #1a7f37;\n  --fgColor-attention: #9a6700;\n  --fgColor-danger: #d1242f;\n  --fgColor-done: #8250df;\n  --bgColor-default: #ffffff;\n  --bgColor-muted: #f6f8fa;\n  --bgColor-neutral-muted: #818b981f;\n  --bgColor-attention-muted: #fff8c5;\n  --borderColor-default: #d1d9e0;\n  --borderColor-muted: #d1d9e0b3;\n  --borderColor-neutral-muted: #d1d9e0b3;\n  --borderColor-accent-emphasis: #0969da;\n  --borderColor-success-emphasis: #1a7f37;\n  --borderColor-attention-emphasis: #9a6700;\n  --borderColor-danger-emphasis: #cf222e;\n  --borderColor-done-emphasis: #8250df;\n  --color-prettylights-syntax-comment: #59636e;\n  --color-prettylights-syntax-constant: #0550ae;\n  --color-prettylights-syntax-constant-other-reference-link: #0a3069;\n  --color-prettylights-syntax-entity: #6639ba;\n  --color-prettylights-syntax-storage-modifier-import: #1f2328;\n  --color-prettylights-syntax-entity-tag: #0550ae;\n  --color-prettylights-syntax-keyword: #cf222e;\n  --color-prettylights-syntax-string: #0a3069;\n  --color-prettylights-syntax-variable: #953800;\n  --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;\n  --color-prettylights-syntax-brackethighlighter-angle: #59636e;\n  --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;\n  --color-prettylights-syntax-invalid-illegal-bg: #82071e;\n  --color-prettylights-syntax-carriage-return-text: #f6f8fa;\n  --color-prettylights-syntax-carriage-return-bg: #cf222e;\n  --color-prettylights-syntax-string-regexp: #116329;\n  --color-prettylights-syntax-markup-list: #3b2300;\n  --color-prettylights-syntax-markup-heading: #0550ae;\n  --color-prettylights-syntax-markup-italic: #1f2328;\n  --color-prettylights-syntax-markup-bold: #1f2328;\n  --color-prettylights-syntax-markup-deleted-text: #82071e;\n  --color-prettylights-syntax-markup-deleted-bg: #ffebe9;\n  --color-prettylights-syntax-markup-inserted-text: #116329;\n  --color-prettylights-syntax-markup-inserted-bg: #dafbe1;\n  --color-prettylights-syntax-markup-changed-text: #953800;\n  --color-prettylights-syntax-markup-changed-bg: #ffd8b5;\n  --color-prettylights-syntax-markup-ignored-text: #d1d9e0;\n  --color-prettylights-syntax-markup-ignored-bg: #0550ae;\n  --color-prettylights-syntax-meta-diff-range: #8250df;\n  --color-prettylights-syntax-sublimelinter-gutter-mark: #818b98;\n}\n\n.markdown-body {\n  -ms-text-size-adjust: 100%;\n  -webkit-text-size-adjust: 100%;\n  margin: 0;\n  color: var(--fgColor-default);\n  background-color: var(--bgColor-default);\n  font-family: -apple-system,BlinkMacSystemFont,\"Segoe UI\",\"Noto Sans\",Helvetica,Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\";\n  font-size: 16px;\n  line-height: 1.5;\n  word-wrap: break-word;\n}\n\n.markdown-body .octicon {\n  display: inline-block;\n  fill: currentColor;\n  vertical-align: text-bottom;\n}\n\n.markdown-body h1:hover .anchor .octicon-link:before,\n.markdown-body h2:hover .anchor .octicon-link:before,\n.markdown-body h3:hover .anchor .octicon-link:before,\n.markdown-body h4:hover .anchor .octicon-link:before,\n.markdown-body h5:hover .anchor .octicon-link:before,\n.markdown-body h6:hover .anchor .octicon-link:before {\n  width: 16px;\n  height: 16px;\n  content: ' ';\n  display: inline-block;\n  background-color: currentColor;\n  -webkit-mask-image: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>\");\n  mask-image: url(\"data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' version='1.1' aria-hidden='true'><path fill-rule='evenodd' d='M7.775 3.275a.75.75 0 001.06 1.06l1.25-1.25a2 2 0 112.83 2.83l-2.5 2.5a2 2 0 01-2.83 0 .75.75 0 00-1.06 1.06 3.5 3.5 0 004.95 0l2.5-2.5a3.5 3.5 0 00-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 010-2.83l2.5-2.5a2 2 0 012.83 0 .75.75 0 001.06-1.06 3.5 3.5 0 00-4.95 0l-2.5 2.5a3.5 3.5 0 004.95 4.95l1.25-1.25a.75.75 0 00-1.06-1.06l-1.25 1.25a2 2 0 01-2.83 0z'></path></svg>\");\n}\n\n.markdown-body details,\n.markdown-body figcaption,\n.markdown-body figure {\n  display: block;\n}\n\n.markdown-body summary {\n  display: list-item;\n}\n\n.markdown-body [hidden] {\n  display: none !important;\n}\n\n.markdown-body a {\n  background-color: transparent;\n  color: var(--fgColor-accent);\n  text-decoration: none;\n}\n\n.markdown-body abbr[title] {\n  border-bottom: none;\n  -webkit-text-decoration: underline dotted;\n  text-decoration: underline dotted;\n}\n\n.markdown-body b,\n.markdown-body strong {\n  font-weight: var(--base-text-weight-semibold, 600);\n}\n\n.markdown-body dfn {\n  font-style: italic;\n}\n\n.markdown-body h1 {\n  margin: .67em 0;\n  font-weight: var(--base-text-weight-semibold, 600);\n  padding-bottom: .3em;\n  font-size: 2em;\n  border-bottom: 1px solid var(--borderColor-muted);\n}\n\n.markdown-body mark {\n  background-color: var(--bgColor-attention-muted);\n  color: var(--fgColor-default);\n}\n\n.markdown-body small {\n  font-size: 90%;\n}\n\n.markdown-body sub,\n.markdown-body sup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\n.markdown-body sub {\n  bottom: -0.25em;\n}\n\n.markdown-body sup {\n  top: -0.5em;\n}\n\n.markdown-body img {\n  border-style: none;\n  max-width: 100%;\n  box-sizing: content-box;\n}\n\n.markdown-body code,\n.markdown-body kbd,\n.markdown-body pre,\n.markdown-body samp {\n  font-family: monospace;\n  font-size: 1em;\n}\n\n.markdown-body figure {\n  margin: 1em var(--base-size-40);\n}\n\n.markdown-body hr {\n  box-sizing: content-box;\n  overflow: hidden;\n  background: transparent;\n  border-bottom: 1px solid var(--borderColor-muted);\n  height: .25em;\n  padding: 0;\n  margin: var(--base-size-24) 0;\n  background-color: var(--borderColor-default);\n  border: 0;\n}\n\n.markdown-body input {\n  font: inherit;\n  margin: 0;\n  overflow: visible;\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit;\n}\n\n.markdown-body [type=button],\n.markdown-body [type=reset],\n.markdown-body [type=submit] {\n  -webkit-appearance: button;\n  appearance: button;\n}\n\n.markdown-body [type=checkbox],\n.markdown-body [type=radio] {\n  box-sizing: border-box;\n  padding: 0;\n}\n\n.markdown-body [type=number]::-webkit-inner-spin-button,\n.markdown-body [type=number]::-webkit-outer-spin-button {\n  height: auto;\n}\n\n.markdown-body [type=search]::-webkit-search-cancel-button,\n.markdown-body [type=search]::-webkit-search-decoration {\n  -webkit-appearance: none;\n  appearance: none;\n}\n\n.markdown-body ::-webkit-input-placeholder {\n  color: inherit;\n  opacity: .54;\n}\n\n.markdown-body ::-webkit-file-upload-button {\n  -webkit-appearance: button;\n  appearance: button;\n  font: inherit;\n}\n\n.markdown-body a:hover {\n  text-decoration: underline;\n}\n\n.markdown-body ::placeholder {\n  color: var(--fgColor-muted);\n  opacity: 1;\n}\n\n.markdown-body hr::before {\n  display: table;\n  content: \"\";\n}\n\n.markdown-body hr::after {\n  display: table;\n  clear: both;\n  content: \"\";\n}\n\n.markdown-body table {\n  border-spacing: 0;\n  border-collapse: collapse;\n  display: block;\n  width: max-content;\n  max-width: 100%;\n  overflow: auto;\n  font-variant: tabular-nums;\n}\n\n.markdown-body td,\n.markdown-body th {\n  padding: 0;\n}\n\n.markdown-body details summary {\n  cursor: pointer;\n}\n\n.markdown-body a:focus,\n.markdown-body [role=button]:focus,\n.markdown-body input[type=radio]:focus,\n.markdown-body input[type=checkbox]:focus {\n  outline: 2px solid var(--focus-outlineColor);\n  outline-offset: -2px;\n  box-shadow: none;\n}\n\n.markdown-body a:focus:not(:focus-visible),\n.markdown-body [role=button]:focus:not(:focus-visible),\n.markdown-body input[type=radio]:focus:not(:focus-visible),\n.markdown-body input[type=checkbox]:focus:not(:focus-visible) {\n  outline: solid 1px transparent;\n}\n\n.markdown-body a:focus-visible,\n.markdown-body [role=button]:focus-visible,\n.markdown-body input[type=radio]:focus-visible,\n.markdown-body input[type=checkbox]:focus-visible {\n  outline: 2px solid var(--focus-outlineColor);\n  outline-offset: -2px;\n  box-shadow: none;\n}\n\n.markdown-body a:not([class]):focus,\n.markdown-body a:not([class]):focus-visible,\n.markdown-body input[type=radio]:focus,\n.markdown-body input[type=radio]:focus-visible,\n.markdown-body input[type=checkbox]:focus,\n.markdown-body input[type=checkbox]:focus-visible {\n  outline-offset: 0;\n}\n\n.markdown-body kbd {\n  display: inline-block;\n  padding: var(--base-size-4);\n  font: 11px var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace);\n  line-height: 10px;\n  color: var(--fgColor-default);\n  vertical-align: middle;\n  background-color: var(--bgColor-muted);\n  border: solid 1px var(--borderColor-neutral-muted);\n  border-bottom-color: var(--borderColor-neutral-muted);\n  border-radius: 6px;\n  box-shadow: inset 0 -1px 0 var(--borderColor-neutral-muted);\n}\n\n.markdown-body h1,\n.markdown-body h2,\n.markdown-body h3,\n.markdown-body h4,\n.markdown-body h5,\n.markdown-body h6 {\n  margin-top: var(--base-size-24);\n  margin-bottom: var(--base-size-16);\n  font-weight: var(--base-text-weight-semibold, 600);\n  line-height: 1.25;\n}\n\n.markdown-body h2 {\n  font-weight: var(--base-text-weight-semibold, 600);\n  padding-bottom: .3em;\n  font-size: 1.5em;\n  border-bottom: 1px solid var(--borderColor-muted);\n}\n\n.markdown-body h3 {\n  font-weight: var(--base-text-weight-semibold, 600);\n  font-size: 1.25em;\n}\n\n.markdown-body h4 {\n  font-weight: var(--base-text-weight-semibold, 600);\n  font-size: 1em;\n}\n\n.markdown-body h5 {\n  font-weight: var(--base-text-weight-semibold, 600);\n  font-size: .875em;\n}\n\n.markdown-body h6 {\n  font-weight: var(--base-text-weight-semibold, 600);\n  font-size: .85em;\n  color: var(--fgColor-muted);\n}\n\n.markdown-body p {\n  margin-top: 0;\n  margin-bottom: 10px;\n}\n\n.markdown-body blockquote {\n  margin: 0;\n  padding: 0 1em;\n  color: var(--fgColor-muted);\n  border-left: .25em solid var(--borderColor-default);\n}\n\n.markdown-body ul,\n.markdown-body ol {\n  margin-top: 0;\n  margin-bottom: 0;\n  padding-left: 2em;\n}\n\n.markdown-body ol ol,\n.markdown-body ul ol {\n  list-style-type: lower-roman;\n}\n\n.markdown-body ul ul ol,\n.markdown-body ul ol ol,\n.markdown-body ol ul ol,\n.markdown-body ol ol ol {\n  list-style-type: lower-alpha;\n}\n\n.markdown-body dd {\n  margin-left: 0;\n}\n\n.markdown-body tt,\n.markdown-body code,\n.markdown-body samp {\n  font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace);\n  font-size: 12px;\n}\n\n.markdown-body pre {\n  margin-top: 0;\n  margin-bottom: 0;\n  font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace);\n  font-size: 12px;\n  word-wrap: normal;\n}\n\n.markdown-body .octicon {\n  display: inline-block;\n  overflow: visible !important;\n  vertical-align: text-bottom;\n  fill: currentColor;\n}\n\n.markdown-body input::-webkit-outer-spin-button,\n.markdown-body input::-webkit-inner-spin-button {\n  margin: 0;\n  appearance: none;\n}\n\n.markdown-body .mr-2 {\n  margin-right: var(--base-size-8, 8px) !important;\n}\n\n.markdown-body::before {\n  display: table;\n  content: \"\";\n}\n\n.markdown-body::after {\n  display: table;\n  clear: both;\n  content: \"\";\n}\n\n.markdown-body>*:first-child {\n  margin-top: 0 !important;\n}\n\n.markdown-body>*:last-child {\n  margin-bottom: 0 !important;\n}\n\n.markdown-body a:not([href]) {\n  color: inherit;\n  text-decoration: none;\n}\n\n.markdown-body .absent {\n  color: var(--fgColor-danger);\n}\n\n.markdown-body .anchor {\n  float: left;\n  padding-right: var(--base-size-4);\n  margin-left: -20px;\n  line-height: 1;\n}\n\n.markdown-body .anchor:focus {\n  outline: none;\n}\n\n.markdown-body p,\n.markdown-body blockquote,\n.markdown-body ul,\n.markdown-body ol,\n.markdown-body dl,\n.markdown-body table,\n.markdown-body pre,\n.markdown-body details {\n  margin-top: 0;\n  margin-bottom: var(--base-size-16);\n}\n\n.markdown-body blockquote>:first-child {\n  margin-top: 0;\n}\n\n.markdown-body blockquote>:last-child {\n  margin-bottom: 0;\n}\n\n.markdown-body h1 .octicon-link,\n.markdown-body h2 .octicon-link,\n.markdown-body h3 .octicon-link,\n.markdown-body h4 .octicon-link,\n.markdown-body h5 .octicon-link,\n.markdown-body h6 .octicon-link {\n  color: var(--fgColor-default);\n  vertical-align: middle;\n  visibility: hidden;\n}\n\n.markdown-body h1:hover .anchor,\n.markdown-body h2:hover .anchor,\n.markdown-body h3:hover .anchor,\n.markdown-body h4:hover .anchor,\n.markdown-body h5:hover .anchor,\n.markdown-body h6:hover .anchor {\n  text-decoration: none;\n}\n\n.markdown-body h1:hover .anchor .octicon-link,\n.markdown-body h2:hover .anchor .octicon-link,\n.markdown-body h3:hover .anchor .octicon-link,\n.markdown-body h4:hover .anchor .octicon-link,\n.markdown-body h5:hover .anchor .octicon-link,\n.markdown-body h6:hover .anchor .octicon-link {\n  visibility: visible;\n}\n\n.markdown-body h1 tt,\n.markdown-body h1 code,\n.markdown-body h2 tt,\n.markdown-body h2 code,\n.markdown-body h3 tt,\n.markdown-body h3 code,\n.markdown-body h4 tt,\n.markdown-body h4 code,\n.markdown-body h5 tt,\n.markdown-body h5 code,\n.markdown-body h6 tt,\n.markdown-body h6 code {\n  padding: 0 .2em;\n  font-size: inherit;\n}\n\n.markdown-body summary h1,\n.markdown-body summary h2,\n.markdown-body summary h3,\n.markdown-body summary h4,\n.markdown-body summary h5,\n.markdown-body summary h6 {\n  display: inline-block;\n}\n\n.markdown-body summary h1 .anchor,\n.markdown-body summary h2 .anchor,\n.markdown-body summary h3 .anchor,\n.markdown-body summary h4 .anchor,\n.markdown-body summary h5 .anchor,\n.markdown-body summary h6 .anchor {\n  margin-left: -40px;\n}\n\n.markdown-body summary h1,\n.markdown-body summary h2 {\n  padding-bottom: 0;\n  border-bottom: 0;\n}\n\n.markdown-body ul.no-list,\n.markdown-body ol.no-list {\n  padding: 0;\n  list-style-type: none;\n}\n\n.markdown-body ol[type=\"a s\"] {\n  list-style-type: lower-alpha;\n}\n\n.markdown-body ol[type=\"A s\"] {\n  list-style-type: upper-alpha;\n}\n\n.markdown-body ol[type=\"i s\"] {\n  list-style-type: lower-roman;\n}\n\n.markdown-body ol[type=\"I s\"] {\n  list-style-type: upper-roman;\n}\n\n.markdown-body ol[type=\"1\"] {\n  list-style-type: decimal;\n}\n\n.markdown-body div>ol:not([type]) {\n  list-style-type: decimal;\n}\n\n.markdown-body ul ul,\n.markdown-body ul ol,\n.markdown-body ol ol,\n.markdown-body ol ul {\n  margin-top: 0;\n  margin-bottom: 0;\n}\n\n.markdown-body li>p {\n  margin-top: var(--base-size-16);\n}\n\n.markdown-body li+li {\n  margin-top: .25em;\n}\n\n.markdown-body dl {\n  padding: 0;\n}\n\n.markdown-body dl dt {\n  padding: 0;\n  margin-top: var(--base-size-16);\n  font-size: 1em;\n  font-style: italic;\n  font-weight: var(--base-text-weight-semibold, 600);\n}\n\n.markdown-body dl dd {\n  padding: 0 var(--base-size-16);\n  margin-bottom: var(--base-size-16);\n}\n\n.markdown-body table th {\n  font-weight: var(--base-text-weight-semibold, 600);\n}\n\n.markdown-body table th,\n.markdown-body table td {\n  padding: 6px 13px;\n  border: 1px solid var(--borderColor-default);\n}\n\n.markdown-body table td>:last-child {\n  margin-bottom: 0;\n}\n\n.markdown-body table tr {\n  background-color: var(--bgColor-default);\n  border-top: 1px solid var(--borderColor-muted);\n}\n\n.markdown-body table tr:nth-child(2n) {\n  background-color: var(--bgColor-muted);\n}\n\n.markdown-body table img {\n  background-color: transparent;\n}\n\n.markdown-body img[align=right] {\n  padding-left: 20px;\n}\n\n.markdown-body img[align=left] {\n  padding-right: 20px;\n}\n\n.markdown-body .emoji {\n  max-width: none;\n  vertical-align: text-top;\n  background-color: transparent;\n}\n\n.markdown-body span.frame {\n  display: block;\n  overflow: hidden;\n}\n\n.markdown-body span.frame>span {\n  display: block;\n  float: left;\n  width: auto;\n  padding: 7px;\n  margin: 13px 0 0;\n  overflow: hidden;\n  border: 1px solid var(--borderColor-default);\n}\n\n.markdown-body span.frame span img {\n  display: block;\n  float: left;\n}\n\n.markdown-body span.frame span span {\n  display: block;\n  padding: 5px 0 0;\n  clear: both;\n  color: var(--fgColor-default);\n}\n\n.markdown-body span.align-center {\n  display: block;\n  overflow: hidden;\n  clear: both;\n}\n\n.markdown-body span.align-center>span {\n  display: block;\n  margin: 13px auto 0;\n  overflow: hidden;\n  text-align: center;\n}\n\n.markdown-body span.align-center span img {\n  margin: 0 auto;\n  text-align: center;\n}\n\n.markdown-body span.align-right {\n  display: block;\n  overflow: hidden;\n  clear: both;\n}\n\n.markdown-body span.align-right>span {\n  display: block;\n  margin: 13px 0 0;\n  overflow: hidden;\n  text-align: right;\n}\n\n.markdown-body span.align-right span img {\n  margin: 0;\n  text-align: right;\n}\n\n.markdown-body span.float-left {\n  display: block;\n  float: left;\n  margin-right: 13px;\n  overflow: hidden;\n}\n\n.markdown-body span.float-left span {\n  margin: 13px 0 0;\n}\n\n.markdown-body span.float-right {\n  display: block;\n  float: right;\n  margin-left: 13px;\n  overflow: hidden;\n}\n\n.markdown-body span.float-right>span {\n  display: block;\n  margin: 13px auto 0;\n  overflow: hidden;\n  text-align: right;\n}\n\n.markdown-body code,\n.markdown-body tt {\n  padding: .2em .4em;\n  margin: 0;\n  font-size: 85%;\n  white-space: break-spaces;\n  background-color: var(--bgColor-neutral-muted);\n  border-radius: 6px;\n}\n\n.markdown-body code br,\n.markdown-body tt br {\n  display: none;\n}\n\n.markdown-body del code {\n  text-decoration: inherit;\n}\n\n.markdown-body samp {\n  font-size: 85%;\n}\n\n.markdown-body pre code {\n  font-size: 100%;\n}\n\n.markdown-body pre>code {\n  padding: 0;\n  margin: 0;\n  word-break: normal;\n  white-space: pre;\n  background: transparent;\n  border: 0;\n}\n\n.markdown-body .highlight {\n  margin-bottom: var(--base-size-16);\n}\n\n.markdown-body .highlight pre {\n  margin-bottom: 0;\n  word-break: normal;\n}\n\n.markdown-body .highlight pre,\n.markdown-body pre {\n  padding: var(--base-size-16);\n  overflow: auto;\n  font-size: 85%;\n  line-height: 1.45;\n  color: var(--fgColor-default);\n  background-color: var(--bgColor-muted);\n  border-radius: 6px;\n}\n\n.markdown-body pre code,\n.markdown-body pre tt {\n  display: inline;\n  max-width: auto;\n  padding: 0;\n  margin: 0;\n  overflow: visible;\n  line-height: inherit;\n  word-wrap: normal;\n  background-color: transparent;\n  border: 0;\n}\n\n.markdown-body .csv-data td,\n.markdown-body .csv-data th {\n  padding: 5px;\n  overflow: hidden;\n  font-size: 12px;\n  line-height: 1;\n  text-align: left;\n  white-space: nowrap;\n}\n\n.markdown-body .csv-data .blob-num {\n  padding: 10px var(--base-size-8) 9px;\n  text-align: right;\n  background: var(--bgColor-default);\n  border: 0;\n}\n\n.markdown-body .csv-data tr {\n  border-top: 0;\n}\n\n.markdown-body .csv-data th {\n  font-weight: var(--base-text-weight-semibold, 600);\n  background: var(--bgColor-muted);\n  border-top: 0;\n}\n\n.markdown-body [data-footnote-ref]::before {\n  content: \"[\";\n}\n\n.markdown-body [data-footnote-ref]::after {\n  content: \"]\";\n}\n\n.markdown-body .footnotes {\n  font-size: 12px;\n  color: var(--fgColor-muted);\n  border-top: 1px solid var(--borderColor-default);\n}\n\n.markdown-body .footnotes ol {\n  padding-left: var(--base-size-16);\n}\n\n.markdown-body .footnotes ol ul {\n  display: inline-block;\n  padding-left: var(--base-size-16);\n  margin-top: var(--base-size-16);\n}\n\n.markdown-body .footnotes li {\n  position: relative;\n}\n\n.markdown-body .footnotes li:target::before {\n  position: absolute;\n  top: calc(var(--base-size-8)*-1);\n  right: calc(var(--base-size-8)*-1);\n  bottom: calc(var(--base-size-8)*-1);\n  left: calc(var(--base-size-24)*-1);\n  pointer-events: none;\n  content: \"\";\n  border: 2px solid var(--borderColor-accent-emphasis);\n  border-radius: 6px;\n}\n\n.markdown-body .footnotes li:target {\n  color: var(--fgColor-default);\n}\n\n.markdown-body .footnotes .data-footnote-backref g-emoji {\n  font-family: monospace;\n}\n\n.markdown-body body:has(:modal) {\n  padding-right: var(--dialog-scrollgutter) !important;\n}\n\n.markdown-body .pl-c {\n  color: var(--color-prettylights-syntax-comment);\n}\n\n.markdown-body .pl-c1,\n.markdown-body .pl-s .pl-v {\n  color: var(--color-prettylights-syntax-constant);\n}\n\n.markdown-body .pl-e,\n.markdown-body .pl-en {\n  color: var(--color-prettylights-syntax-entity);\n}\n\n.markdown-body .pl-smi,\n.markdown-body .pl-s .pl-s1 {\n  color: var(--color-prettylights-syntax-storage-modifier-import);\n}\n\n.markdown-body .pl-ent {\n  color: var(--color-prettylights-syntax-entity-tag);\n}\n\n.markdown-body .pl-k {\n  color: var(--color-prettylights-syntax-keyword);\n}\n\n.markdown-body .pl-s,\n.markdown-body .pl-pds,\n.markdown-body .pl-s .pl-pse .pl-s1,\n.markdown-body .pl-sr,\n.markdown-body .pl-sr .pl-cce,\n.markdown-body .pl-sr .pl-sre,\n.markdown-body .pl-sr .pl-sra {\n  color: var(--color-prettylights-syntax-string);\n}\n\n.markdown-body .pl-v,\n.markdown-body .pl-smw {\n  color: var(--color-prettylights-syntax-variable);\n}\n\n.markdown-body .pl-bu {\n  color: var(--color-prettylights-syntax-brackethighlighter-unmatched);\n}\n\n.markdown-body .pl-ii {\n  color: var(--color-prettylights-syntax-invalid-illegal-text);\n  background-color: var(--color-prettylights-syntax-invalid-illegal-bg);\n}\n\n.markdown-body .pl-c2 {\n  color: var(--color-prettylights-syntax-carriage-return-text);\n  background-color: var(--color-prettylights-syntax-carriage-return-bg);\n}\n\n.markdown-body .pl-sr .pl-cce {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-string-regexp);\n}\n\n.markdown-body .pl-ml {\n  color: var(--color-prettylights-syntax-markup-list);\n}\n\n.markdown-body .pl-mh,\n.markdown-body .pl-mh .pl-en,\n.markdown-body .pl-ms {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-markup-heading);\n}\n\n.markdown-body .pl-mi {\n  font-style: italic;\n  color: var(--color-prettylights-syntax-markup-italic);\n}\n\n.markdown-body .pl-mb {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-markup-bold);\n}\n\n.markdown-body .pl-md {\n  color: var(--color-prettylights-syntax-markup-deleted-text);\n  background-color: var(--color-prettylights-syntax-markup-deleted-bg);\n}\n\n.markdown-body .pl-mi1 {\n  color: var(--color-prettylights-syntax-markup-inserted-text);\n  background-color: var(--color-prettylights-syntax-markup-inserted-bg);\n}\n\n.markdown-body .pl-mc {\n  color: var(--color-prettylights-syntax-markup-changed-text);\n  background-color: var(--color-prettylights-syntax-markup-changed-bg);\n}\n\n.markdown-body .pl-mi2 {\n  color: var(--color-prettylights-syntax-markup-ignored-text);\n  background-color: var(--color-prettylights-syntax-markup-ignored-bg);\n}\n\n.markdown-body .pl-mdr {\n  font-weight: bold;\n  color: var(--color-prettylights-syntax-meta-diff-range);\n}\n\n.markdown-body .pl-ba {\n  color: var(--color-prettylights-syntax-brackethighlighter-angle);\n}\n\n.markdown-body .pl-sg {\n  color: var(--color-prettylights-syntax-sublimelinter-gutter-mark);\n}\n\n.markdown-body .pl-corl {\n  text-decoration: underline;\n  color: var(--color-prettylights-syntax-constant-other-reference-link);\n}\n\n.markdown-body [role=button]:focus:not(:focus-visible),\n.markdown-body [role=tabpanel][tabindex=\"0\"]:focus:not(:focus-visible),\n.markdown-body button:focus:not(:focus-visible),\n.markdown-body summary:focus:not(:focus-visible),\n.markdown-body a:focus:not(:focus-visible) {\n  outline: none;\n  box-shadow: none;\n}\n\n.markdown-body [tabindex=\"0\"]:focus:not(:focus-visible),\n.markdown-body details-dialog:focus:not(:focus-visible) {\n  outline: none;\n}\n\n.markdown-body g-emoji {\n  display: inline-block;\n  min-width: 1ch;\n  font-family: \"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\";\n  font-size: 1em;\n  font-style: normal !important;\n  font-weight: var(--base-text-weight-normal, 400);\n  line-height: 1;\n  vertical-align: -0.075em;\n}\n\n.markdown-body g-emoji img {\n  width: 1em;\n  height: 1em;\n}\n\n.markdown-body .task-list-item {\n  list-style-type: none;\n}\n\n.markdown-body .task-list-item label {\n  font-weight: var(--base-text-weight-normal, 400);\n}\n\n.markdown-body .task-list-item.enabled label {\n  cursor: pointer;\n}\n\n.markdown-body .task-list-item+.task-list-item {\n  margin-top: var(--base-size-4);\n}\n\n.markdown-body .task-list-item .handle {\n  display: none;\n}\n\n.markdown-body .task-list-item-checkbox {\n  margin: 0 .2em .25em -1.4em;\n  vertical-align: middle;\n}\n\n.markdown-body ul:dir(rtl) .task-list-item-checkbox {\n  margin: 0 -1.6em .25em .2em;\n}\n\n.markdown-body ol:dir(rtl) .task-list-item-checkbox {\n  margin: 0 -1.6em .25em .2em;\n}\n\n.markdown-body .contains-task-list:hover .task-list-item-convert-container,\n.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {\n  display: block;\n  width: auto;\n  height: 24px;\n  overflow: visible;\n  clip: auto;\n}\n\n.markdown-body ::-webkit-calendar-picker-indicator {\n  filter: invert(50%);\n}\n\n.markdown-body .markdown-alert {\n  padding: var(--base-size-8) var(--base-size-16);\n  margin-bottom: var(--base-size-16);\n  color: inherit;\n  border-left: .25em solid var(--borderColor-default);\n}\n\n.markdown-body .markdown-alert>:first-child {\n  margin-top: 0;\n}\n\n.markdown-body .markdown-alert>:last-child {\n  margin-bottom: 0;\n}\n\n.markdown-body .markdown-alert .markdown-alert-title {\n  display: flex;\n  font-weight: var(--base-text-weight-medium, 500);\n  align-items: center;\n  line-height: 1;\n}\n\n.markdown-body .markdown-alert.markdown-alert-note {\n  border-left-color: var(--borderColor-accent-emphasis);\n}\n\n.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title {\n  color: var(--fgColor-accent);\n}\n\n.markdown-body .markdown-alert.markdown-alert-important {\n  border-left-color: var(--borderColor-done-emphasis);\n}\n\n.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title {\n  color: var(--fgColor-done);\n}\n\n.markdown-body .markdown-alert.markdown-alert-warning {\n  border-left-color: var(--borderColor-attention-emphasis);\n}\n\n.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title {\n  color: var(--fgColor-attention);\n}\n\n.markdown-body .markdown-alert.markdown-alert-tip {\n  border-left-color: var(--borderColor-success-emphasis);\n}\n\n.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title {\n  color: var(--fgColor-success);\n}\n\n.markdown-body .markdown-alert.markdown-alert-caution {\n  border-left-color: var(--borderColor-danger-emphasis);\n}\n\n.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title {\n  color: var(--fgColor-danger);\n}\n\n.markdown-body>*:first-child>.heading-element:first-child {\n  margin-top: 0 !important;\n}\n\n.markdown-body .highlight pre:has(+.zeroclipboard-container) {\n  min-height: 52px;\n}\n"
  },
  {
    "path": "manager/app/src/main/cpp/CMakeLists.txt",
    "content": "\n# For more information about using CMake with Android Studio, read the\n# documentation: https://d.android.com/studio/projects/add-native-code.html\n\n# Sets the minimum version of CMake required to build the native library.\ncmake_minimum_required(VERSION 3.18.1)\n\nproject(\"kernelsu\")\n\nfind_package(cxx REQUIRED CONFIG)\nlink_libraries(cxx::cxx)\n\nadd_library(kernelsu\n        SHARED\n        jni.cc\n        ksu.cc\n        )\n\nfind_library(log-lib log)\n\ntarget_link_libraries(kernelsu ${log-lib})"
  },
  {
    "path": "manager/app/src/main/cpp/jni.cc",
    "content": "#include <jni.h>\n\n#include <sys/prctl.h>\n#include <linux/capability.h>\n#include <pwd.h>\n#include <unistd.h>\n#include <sys/wait.h>\n\n#include <android/log.h>\n#include <cstring>\n\n#include \"ksu.h\"\n#include \"logging.h\"\n\nextern \"C\"\nJNIEXPORT jint JNICALL\nJava_me_weishu_kernelsu_Natives_getVersion(JNIEnv *env, jobject) {\n    int version = get_version();\n    if (version > 0) {\n        return version;\n    }\n    // try legacy method as fallback\n    return legacy_get_info().first;\n}\n\nextern \"C\"\nJNIEXPORT jint JNICALL\nJava_me_weishu_kernelsu_Natives_getSuperuserCount(JNIEnv *env, jobject) {\n    struct ksu_new_get_allow_list_cmd cmd = {\n        .count = 0\n    };\n    bool result = get_allow_list(&cmd);\n    return result ? cmd.total_count : 0;\n}\n\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_isSafeMode(JNIEnv *env, jclass clazz) {\n    return is_safe_mode();\n}\n\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_isLkmMode(JNIEnv *env, jclass clazz) {\n    return is_lkm_mode();\n}\n\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_isLateLoadMode(JNIEnv *env, jclass clazz) {\n    return is_late_load_mode();\n}\n\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_isManager(JNIEnv *env, jclass clazz) {\n    return is_manager();\n}\n\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_isPrBuild(JNIEnv *env, jclass clazz) {\n    return is_pr_build();\n}\n\nstatic void fillIntArray(JNIEnv *env, jobject list, int *data, int count) {\n    auto cls = env->GetObjectClass(list);\n    auto add = env->GetMethodID(cls, \"add\", \"(Ljava/lang/Object;)Z\");\n    auto integerCls = env->FindClass(\"java/lang/Integer\");\n    auto constructor = env->GetMethodID(integerCls, \"<init>\", \"(I)V\");\n    for (int i = 0; i < count; ++i) {\n        auto integer = env->NewObject(integerCls, constructor, data[i]);\n        env->CallBooleanMethod(list, add, integer);\n    }\n}\n\nstatic void addIntToList(JNIEnv *env, jobject list, int ele) {\n    auto cls = env->GetObjectClass(list);\n    auto add = env->GetMethodID(cls, \"add\", \"(Ljava/lang/Object;)Z\");\n    auto integerCls = env->FindClass(\"java/lang/Integer\");\n    auto constructor = env->GetMethodID(integerCls, \"<init>\", \"(I)V\");\n    auto integer = env->NewObject(integerCls, constructor, ele);\n    env->CallBooleanMethod(list, add, integer);\n}\n\nstatic uint64_t capListToBits(JNIEnv *env, jobject list) {\n    auto cls = env->GetObjectClass(list);\n    auto get = env->GetMethodID(cls, \"get\", \"(I)Ljava/lang/Object;\");\n    auto size = env->GetMethodID(cls, \"size\", \"()I\");\n    auto listSize = env->CallIntMethod(list, size);\n    auto integerCls = env->FindClass(\"java/lang/Integer\");\n    auto intValue = env->GetMethodID(integerCls, \"intValue\", \"()I\");\n    uint64_t result = 0;\n    for (int i = 0; i < listSize; ++i) {\n        auto integer = env->CallObjectMethod(list, get, i);\n        int data = env->CallIntMethod(integer, intValue);\n\n        if (cap_valid(data)) {\n            result |= (1ULL << data);\n        }\n    }\n\n    return result;\n}\n\nstatic int getListSize(JNIEnv *env, jobject list) {\n    auto cls = env->GetObjectClass(list);\n    auto size = env->GetMethodID(cls, \"size\", \"()I\");\n    return env->CallIntMethod(list, size);\n}\n\nstatic void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) {\n    auto cls = env->GetObjectClass(list);\n    auto get = env->GetMethodID(cls, \"get\", \"(I)Ljava/lang/Object;\");\n    auto integerCls = env->FindClass(\"java/lang/Integer\");\n    auto intValue = env->GetMethodID(integerCls, \"intValue\", \"()I\");\n    for (int i = 0; i < count; ++i) {\n        auto integer = env->CallObjectMethod(list, get, i);\n        data[i] = env->CallIntMethod(integer, intValue);\n    }\n}\n\nextern \"C\"\nJNIEXPORT jobject JNICALL\nJava_me_weishu_kernelsu_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) {\n    if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) {\n        return nullptr;\n    }\n\n    p_key_t key = {};\n    auto cpkg = env->GetStringUTFChars(pkg, nullptr);\n    strcpy(key, cpkg);\n    env->ReleaseStringUTFChars(pkg, cpkg);\n\n    app_profile profile = {};\n    profile.version = KSU_APP_PROFILE_VER;\n\n    strcpy(profile.key, key);\n    profile.current_uid = uid;\n\n    bool useDefaultProfile = get_app_profile(&profile) != 0;\n\n    auto cls = env->FindClass(\"me/weishu/kernelsu/Natives$Profile\");\n    auto constructor = env->GetMethodID(cls, \"<init>\", \"()V\");\n    auto obj = env->NewObject(cls, constructor);\n    auto keyField = env->GetFieldID(cls, \"name\", \"Ljava/lang/String;\");\n    auto currentUidField = env->GetFieldID(cls, \"currentUid\", \"I\");\n    auto allowSuField = env->GetFieldID(cls, \"allowSu\", \"Z\");\n\n    auto rootUseDefaultField = env->GetFieldID(cls, \"rootUseDefault\", \"Z\");\n    auto rootTemplateField = env->GetFieldID(cls, \"rootTemplate\", \"Ljava/lang/String;\");\n\n    auto uidField = env->GetFieldID(cls, \"uid\", \"I\");\n    auto gidField = env->GetFieldID(cls, \"gid\", \"I\");\n    auto groupsField = env->GetFieldID(cls, \"groups\", \"Ljava/util/List;\");\n    auto capabilitiesField = env->GetFieldID(cls, \"capabilities\", \"Ljava/util/List;\");\n    auto domainField = env->GetFieldID(cls, \"context\", \"Ljava/lang/String;\");\n    auto namespacesField = env->GetFieldID(cls, \"namespace\", \"I\");\n\n    auto nonRootUseDefaultField = env->GetFieldID(cls, \"nonRootUseDefault\", \"Z\");\n    auto umountModulesField = env->GetFieldID(cls, \"umountModules\", \"Z\");\n\n    env->SetObjectField(obj, keyField, env->NewStringUTF(profile.key));\n    env->SetIntField(obj, currentUidField, profile.current_uid);\n\n    if (useDefaultProfile) {\n        // no profile found, so just use default profile:\n        // don't allow root and use default profile!\n        LOGD(\"use default profile for: %s, %d\", key, uid);\n\n        // allow_su = false\n        // non root use default = true\n        env->SetBooleanField(obj, allowSuField, false);\n        env->SetBooleanField(obj, nonRootUseDefaultField, true);\n\n        return obj;\n    }\n\n    auto allowSu = profile.allow_su;\n\n    if (allowSu) {\n        env->SetBooleanField(obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default);\n        if (strlen(profile.rp_config.template_name) > 0) {\n            env->SetObjectField(obj, rootTemplateField,\n                    env->NewStringUTF(profile.rp_config.template_name));\n        }\n\n        env->SetIntField(obj, uidField, profile.rp_config.profile.uid);\n        env->SetIntField(obj, gidField, profile.rp_config.profile.gid);\n\n        jobject groupList = env->GetObjectField(obj, groupsField);\n        int groupCount = profile.rp_config.profile.groups_count;\n        if (groupCount > KSU_MAX_GROUPS) {\n            LOGD(\"kernel group count too large: %d???\", groupCount);\n            groupCount = KSU_MAX_GROUPS;\n        }\n        fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount);\n\n        jobject capList = env->GetObjectField(obj, capabilitiesField);\n        for (int i = 0; i <= CAP_LAST_CAP; i++) {\n            if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) {\n                addIntToList(env, capList, i);\n            }\n        }\n\n        env->SetObjectField(obj, domainField,\n                env->NewStringUTF(profile.rp_config.profile.selinux_domain));\n        env->SetIntField(obj, namespacesField, profile.rp_config.profile.namespaces);\n        env->SetBooleanField(obj, allowSuField, profile.allow_su);\n    } else {\n        env->SetBooleanField(obj, nonRootUseDefaultField,\n                (jboolean) profile.nrp_config.use_default);\n        env->SetBooleanField(obj, umountModulesField, profile.nrp_config.profile.umount_modules);\n    }\n\n    return obj;\n}\n\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) {\n    auto cls = env->FindClass(\"me/weishu/kernelsu/Natives$Profile\");\n\n    auto keyField = env->GetFieldID(cls, \"name\", \"Ljava/lang/String;\");\n    auto currentUidField = env->GetFieldID(cls, \"currentUid\", \"I\");\n    auto allowSuField = env->GetFieldID(cls, \"allowSu\", \"Z\");\n\n    auto rootUseDefaultField = env->GetFieldID(cls, \"rootUseDefault\", \"Z\");\n    auto rootTemplateField = env->GetFieldID(cls, \"rootTemplate\", \"Ljava/lang/String;\");\n\n    auto uidField = env->GetFieldID(cls, \"uid\", \"I\");\n    auto gidField = env->GetFieldID(cls, \"gid\", \"I\");\n    auto groupsField = env->GetFieldID(cls, \"groups\", \"Ljava/util/List;\");\n    auto capabilitiesField = env->GetFieldID(cls, \"capabilities\", \"Ljava/util/List;\");\n    auto domainField = env->GetFieldID(cls, \"context\", \"Ljava/lang/String;\");\n    auto namespacesField = env->GetFieldID(cls, \"namespace\", \"I\");\n\n    auto nonRootUseDefaultField = env->GetFieldID(cls, \"nonRootUseDefault\", \"Z\");\n    auto umountModulesField = env->GetFieldID(cls, \"umountModules\", \"Z\");\n\n    auto key = env->GetObjectField(profile, keyField);\n    if (!key) {\n        return false;\n    }\n    if (env->GetStringLength((jstring) key) > KSU_MAX_PACKAGE_NAME) {\n        return false;\n    }\n\n    auto cpkg = env->GetStringUTFChars((jstring) key, nullptr);\n    p_key_t p_key = {};\n    strcpy(p_key, cpkg);\n    env->ReleaseStringUTFChars((jstring) key, cpkg);\n\n    auto currentUid = env->GetIntField(profile, currentUidField);\n\n    auto uid = env->GetIntField(profile, uidField);\n    auto gid = env->GetIntField(profile, gidField);\n    auto groups = env->GetObjectField(profile, groupsField);\n    auto capabilities = env->GetObjectField(profile, capabilitiesField);\n    auto domain = env->GetObjectField(profile, domainField);\n    auto allowSu = env->GetBooleanField(profile, allowSuField);\n    auto umountModules = env->GetBooleanField(profile, umountModulesField);\n\n    app_profile p = {};\n    p.version = KSU_APP_PROFILE_VER;\n\n    strcpy(p.key, p_key);\n    p.allow_su = allowSu;\n    p.current_uid = currentUid;\n\n    if (allowSu) {\n        p.rp_config.use_default = env->GetBooleanField(profile, rootUseDefaultField);\n        auto templateName = env->GetObjectField(profile, rootTemplateField);\n        if (templateName) {\n            auto ctemplateName = env->GetStringUTFChars((jstring) templateName, nullptr);\n            strcpy(p.rp_config.template_name, ctemplateName);\n            env->ReleaseStringUTFChars((jstring) templateName, ctemplateName);\n        }\n\n        p.rp_config.profile.uid = uid;\n        p.rp_config.profile.gid = gid;\n\n        int groups_count = getListSize(env, groups);\n        if (groups_count > KSU_MAX_GROUPS) {\n            LOGD(\"groups count too large: %d\", groups_count);\n            return false;\n        }\n        p.rp_config.profile.groups_count = groups_count;\n        fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count);\n\n        p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities);\n\n        auto cdomain = env->GetStringUTFChars((jstring) domain, nullptr);\n        strcpy(p.rp_config.profile.selinux_domain, cdomain);\n        env->ReleaseStringUTFChars((jstring) domain, cdomain);\n\n        p.rp_config.profile.namespaces = env->GetIntField(profile, namespacesField);\n    } else {\n        p.nrp_config.use_default = env->GetBooleanField(profile, nonRootUseDefaultField);\n        p.nrp_config.profile.umount_modules = umountModules;\n    }\n\n    return set_app_profile(&p);\n}\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) {\n    return uid_should_umount(uid);\n}\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_isSuEnabled(JNIEnv *env, jobject thiz) {\n    return is_su_enabled();\n}\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {\n    return set_su_enabled(enabled);\n}\n\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_isKernelUmountEnabled(JNIEnv *env, jobject thiz) {\n    return is_kernel_umount_enabled();\n}\n\nextern \"C\"\nJNIEXPORT jboolean JNICALL\nJava_me_weishu_kernelsu_Natives_setKernelUmountEnabled(JNIEnv *env, jobject thiz, jboolean enabled) {\n    return set_kernel_umount_enabled(enabled);\n}\n\nextern \"C\"\nJNIEXPORT jstring JNICALL\nJava_me_weishu_kernelsu_Natives_getUserName(JNIEnv *env, jobject thiz, jint uid) {\n    struct passwd *pw = getpwuid((uid_t) uid);\n    if (pw && pw->pw_name && pw->pw_name[0] != '\\0') {\n        return env->NewStringUTF(pw->pw_name);\n    }\n    return nullptr;\n}\n\nint fork_dont_care_and_exec_ksud(const char *path) {\n    int pid = fork();\n    if (pid < 0) {\n        PLOGE(\"fork\");\n        return pid;\n    } else if (pid > 0) {\n        int status = 0;\n        if (TEMP_FAILURE_RETRY(waitpid(pid, &status, 0)) < 0) {\n            PLOGE(\"waitpid\");\n            return -1;\n        }\n        if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {\n            LOGE(\"magica bootstrap child failed, status=%d\", status);\n        }\n        return pid;\n    }\n\n    if (setuid(0) != 0) {\n        PLOGE(\"setuid\");\n        _exit(1);\n    }\n\n    pid = fork();\n    if (pid < 0) {\n        PLOGE(\"fork 2\");\n        _exit(1);\n    } else if (pid > 0) {\n        _exit(0);\n    }\n\n    execl(path, \"ksud\", \"late-load\", \"--magica\", \"5555\", nullptr);\n    PLOGE(\"exec magica\");\n    _exit(1);\n}\n\nextern \"C\"\nJNIEXPORT void JNICALL\nJava_me_weishu_kernelsu_magica_AppZygotePreload_forkDontCareAndExecKsud(JNIEnv *env, jclass clazz,\n                                                                        jstring ksud_path) {\n    auto path = env->GetStringUTFChars(ksud_path, nullptr);\n    LOGD(\"executing magica %s\", path);\n    fork_dont_care_and_exec_ksud(path);\n    env->ReleaseStringUTFChars(ksud_path, path);\n}\n"
  },
  {
    "path": "manager/app/src/main/cpp/ksu.cc",
    "content": "//\n// Created by weishu on 2022/12/9.\n//\n\n#include <sys/prctl.h>\n#include <cstdint>\n#include <cstring>\n#include <cstdio>\n#include <unistd.h>\n#include <utility>\n#include <android/log.h>\n#include <dirent.h>\n#include <cstdlib>\n\n#include <unistd.h>\n#include <climits>\n#include <sys/syscall.h>\n#include \"ksu.h\"\n\nstatic int fd = -1;\n\nstatic inline int scan_driver_fd() {\n    const char *kName = \"[ksu_driver]\";\n    DIR *dir = opendir(\"/proc/self/fd\");\n    if (!dir) {\n        return -1;\n    }\n\n    int found = -1;\n    struct dirent *de;\n    char path[64];\n    char target[PATH_MAX];\n\n    while ((de = readdir(dir)) != NULL) {\n        if (de->d_name[0] == '.') {\n            continue;\n        }\n\n        char *endptr = NULL;\n        long fd_long = strtol(de->d_name, &endptr, 10);\n        if (!de->d_name[0] || *endptr != '\\0' || fd_long < 0 || fd_long > INT_MAX) {\n            continue;\n        }\n\n        snprintf(path, sizeof(path), \"/proc/self/fd/%s\", de->d_name);\n        ssize_t n = readlink(path, target, sizeof(target) - 1);\n        if (n < 0) {\n            continue;\n        }\n        target[n] = '\\0';\n\n        const char *base = strrchr(target, '/');\n        base = base ? base + 1 : target;\n\n        if (strstr(base, kName)) {\n            found = (int)fd_long;\n            break;\n        }\n    }\n\n    closedir(dir);\n    return found;\n}\n\ntemplate<typename... Args>\nstatic int ksuctl(unsigned long op, Args &&... args) {\n\n    if (fd < 0) {\n        fd = scan_driver_fd();\n    }\n\n    static_assert(sizeof...(Args) <= 1, \"ioctl expects at most one extra argument\");\n\n    return ioctl(fd, op, std::forward<Args>(args)...);\n}\n\nstatic struct ksu_get_info_cmd g_version {};\n\nstruct ksu_get_info_cmd get_info() {\n    if (!g_version.version) {\n        ksuctl(KSU_IOCTL_GET_INFO, &g_version);\n    }\n    return g_version;\n}\n\nuint32_t get_version() {\n    auto info = get_info();\n    return info.version;\n}\n\nbool get_allow_list(struct ksu_new_get_allow_list_cmd *cmd) {\n    return ksuctl(KSU_IOCTL_NEW_GET_ALLOW_LIST, cmd) == 0;\n}\n\nbool is_safe_mode() {\n    struct ksu_check_safemode_cmd cmd = {};\n    ksuctl(KSU_IOCTL_CHECK_SAFEMODE, &cmd);\n    return cmd.in_safe_mode;\n}\n\nbool is_lkm_mode() {\n    auto info = get_info();\n    if (info.version > 0) {\n        return (info.flags & KSU_GET_INFO_FLAG_LKM) != 0;\n    }\n    return (legacy_get_info().second & KSU_GET_INFO_FLAG_LKM) != 0;\n}\n\nbool is_late_load_mode() {\n    auto info = get_info();\n    if (info.version > 0) {\n        return (info.flags & KSU_GET_INFO_FLAG_LATE_LOAD) != 0;\n    }\n    return false;\n}\n\nbool is_manager() {\n    auto info = get_info();\n    if (info.version > 0) {\n        return (info.flags & KSU_GET_INFO_FLAG_MANAGER) != 0;\n    }\n    return legacy_get_info().first > 0;\n}\n\nbool is_pr_build() {\n    auto info = get_info();\n    if (info.version > 0) {\n        return (info.flags & KSU_GET_INFO_FLAG_PR_BUILD) != 0;\n    }\n    return false;\n}\n\nbool uid_should_umount(int uid) {\n    struct ksu_uid_should_umount_cmd cmd = {};\n    cmd.uid = uid;\n    ksuctl(KSU_IOCTL_UID_SHOULD_UMOUNT, &cmd);\n    return cmd.should_umount;\n}\n\nbool set_app_profile(const app_profile *profile) {\n    struct ksu_set_app_profile_cmd cmd = {};\n    cmd.profile = *profile;\n    return ksuctl(KSU_IOCTL_SET_APP_PROFILE, &cmd) == 0;\n}\n\nint get_app_profile(app_profile *profile) {\n    struct ksu_get_app_profile_cmd cmd = {.profile = *profile};\n    int ret = ksuctl(KSU_IOCTL_GET_APP_PROFILE, &cmd);\n    *profile = cmd.profile;\n    return ret;\n}\n\nbool set_su_enabled(bool enabled) {\n    struct ksu_set_feature_cmd cmd = {};\n    cmd.feature_id = KSU_FEATURE_SU_COMPAT;\n    cmd.value = enabled ? 1 : 0;\n    return ksuctl(KSU_IOCTL_SET_FEATURE, &cmd) == 0;\n}\n\nbool is_su_enabled() {\n    struct ksu_get_feature_cmd cmd = {};\n    cmd.feature_id = KSU_FEATURE_SU_COMPAT;\n    if (ksuctl(KSU_IOCTL_GET_FEATURE, &cmd) != 0) {\n        return false;\n    }\n    if (!cmd.supported) {\n        return false;\n    }\n    return cmd.value != 0;\n}\n\nstatic inline bool get_feature(uint32_t feature_id, uint64_t *out_value, bool *out_supported) {\n    struct ksu_get_feature_cmd cmd = {};\n    cmd.feature_id = feature_id;\n    if (ksuctl(KSU_IOCTL_GET_FEATURE, &cmd) != 0) {\n        return false;\n    }\n    if (out_value) *out_value = cmd.value;\n    if (out_supported) *out_supported = cmd.supported;\n    return true;\n}\n\nstatic inline bool set_feature(uint32_t feature_id, uint64_t value) {\n    struct ksu_set_feature_cmd cmd = {};\n    cmd.feature_id = feature_id;\n    cmd.value = value;\n    return ksuctl(KSU_IOCTL_SET_FEATURE, &cmd) == 0;\n}\n\nbool set_kernel_umount_enabled(bool enabled) {\n    return set_feature(KSU_FEATURE_KERNEL_UMOUNT, enabled ? 1 : 0);\n}\n\nbool is_kernel_umount_enabled() {\n    uint64_t value = 0;\n    bool supported = false;\n    if (!get_feature(KSU_FEATURE_KERNEL_UMOUNT, &value, &supported)) {\n        return false;\n    }\n    if (!supported) {\n        return false;\n    }\n    return value != 0;\n}\n"
  },
  {
    "path": "manager/app/src/main/cpp/ksu.h",
    "content": "//\n// Created by weishu on 2022/12/9.\n//\n\n#ifndef KERNELSU_KSU_H\n#define KERNELSU_KSU_H\n\n#include <cstdint>\n#include <sys/ioctl.h>\n#include <utility>\n\nuint32_t get_version();\n\nbool uid_should_umount(int uid);\n\nbool is_safe_mode();\n\nbool is_lkm_mode();\n\nbool is_late_load_mode();\n\nbool is_manager();\n\nbool is_pr_build();\n\n#define KSU_APP_PROFILE_VER 2\n#define KSU_MAX_PACKAGE_NAME 256\n// NGROUPS_MAX for Linux is 65535 generally, but we only supports 32 groups.\n#define KSU_MAX_GROUPS 32\n#define KSU_SELINUX_DOMAIN 64\n\nusing p_key_t = char[KSU_MAX_PACKAGE_NAME];\n\nstruct root_profile {\n    int32_t uid;\n    int32_t gid;\n\n    int32_t groups_count;\n    int32_t groups[KSU_MAX_GROUPS];\n\n    // kernel_cap_t is u32[2] for capabilities v3\n    struct {\n        uint64_t effective;\n        uint64_t permitted;\n        uint64_t inheritable;\n    } capabilities;\n\n    char selinux_domain[KSU_SELINUX_DOMAIN];\n\n    int32_t namespaces;\n};\n\nstruct non_root_profile {\n    bool umount_modules;\n};\n\nstruct app_profile {\n    // It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this.\n    uint32_t version;\n\n    // this is usually the package of the app, but can be other value for special apps\n    char key[KSU_MAX_PACKAGE_NAME];\n    int32_t current_uid;\n    bool allow_su;\n\n    union {\n        struct {\n            bool use_default;\n            char template_name[KSU_MAX_PACKAGE_NAME];\n\n            struct root_profile profile;\n        } rp_config;\n\n        struct {\n            bool use_default;\n\n            struct non_root_profile profile;\n        } nrp_config;\n    };\n};\n\nbool set_app_profile(const app_profile *profile);\n\nint get_app_profile(app_profile *profile);\n\n// Feature IDs\nenum ksu_feature_id {\n    KSU_FEATURE_SU_COMPAT = 0,\n    KSU_FEATURE_KERNEL_UMOUNT = 1,\n};\n\n// Generic feature API\nstruct ksu_get_feature_cmd {\n    uint32_t feature_id; // Input: feature ID\n    uint64_t value;      // Output: feature value/state\n    uint8_t supported;   // Output: whether the feature is supported\n};\n\nstruct ksu_set_feature_cmd {\n    uint32_t feature_id; // Input: feature ID\n    uint64_t value;      // Input: feature value/state to set\n};\n\nstruct ksu_become_daemon_cmd {\n    uint8_t token[65]; // Input: daemon token (null-terminated)\n};\n\nenum ksu_get_info_flag : uint32_t {\n    KSU_GET_INFO_FLAG_LKM = 1U << 0,\n    KSU_GET_INFO_FLAG_MANAGER = 1U << 1,\n    KSU_GET_INFO_FLAG_LATE_LOAD = 1U << 2,\n    KSU_GET_INFO_FLAG_PR_BUILD = 1U << 3,\n};\n\nstruct ksu_get_info_cmd {\n    uint32_t version; // Output: KERNEL_SU_VERSION\n    uint32_t flags;   // Output: KSU_GET_INFO_FLAG_* bits\n    uint32_t features; // Output: max feature ID supported (KSU_FEATURE_MAX)\n};\n\nstruct ksu_report_event_cmd {\n    uint32_t event; // Input: EVENT_POST_FS_DATA, EVENT_BOOT_COMPLETED, etc.\n};\n\nstruct ksu_set_sepolicy_cmd {\n    uint64_t cmd; // Input: sepolicy command\n    uint64_t arg; // Input: sepolicy argument pointer\n};\n\nstruct ksu_check_safemode_cmd {\n    uint8_t in_safe_mode; // Output: true if in safe mode, false otherwise\n};\n\nstruct ksu_new_get_allow_list_cmd {\n    uint16_t count; // Input / Output: number of UIDs in array\n    uint16_t total_count; // Output: total number of UIDs in requested list\n    uint32_t uids[0]; // Output: array of allowed/denied UIDs\n};\n\nstruct ksu_uid_granted_root_cmd {\n    uint32_t uid; // Input: target UID to check\n    uint8_t granted; // Output: true if granted, false otherwise\n};\n\nstruct ksu_uid_should_umount_cmd {\n    uint32_t uid; // Input: target UID to check\n    uint8_t should_umount; // Output: true if should umount, false otherwise\n};\n\nstruct ksu_get_manager_appid_cmd {\n    uint32_t appid; // Output: manager app id\n};\n\nstruct ksu_get_app_profile_cmd {\n    struct app_profile profile; // Input/Output: app profile structure\n};\n\nstruct ksu_set_app_profile_cmd {\n    struct app_profile profile; // Input: app profile structure\n};\n\n// Su compat\nbool set_su_enabled(bool enabled);\n\nbool is_su_enabled();\n\n// Kernel umount\nbool set_kernel_umount_enabled(bool enabled);\n\nbool is_kernel_umount_enabled();\n\n// IOCTL command definitions\n#define KSU_IOCTL_GRANT_ROOT _IOC(_IOC_NONE, 'K', 1, 0)\n#define KSU_IOCTL_GET_INFO _IOC(_IOC_READ, 'K', 2, 0)\n#define KSU_IOCTL_REPORT_EVENT _IOC(_IOC_WRITE, 'K', 3, 0)\n#define KSU_IOCTL_SET_SEPOLICY _IOC(_IOC_READ|_IOC_WRITE, 'K', 4, 0)\n#define KSU_IOCTL_CHECK_SAFEMODE _IOC(_IOC_READ, 'K', 5, 0)\n#define KSU_IOCTL_NEW_GET_ALLOW_LIST _IOWR('K', 6, struct ksu_new_get_allow_list_cmd)\n#define KSU_IOCTL_NEW_GET_DENY_LIST _IOWR('K', 7, struct ksu_new_get_allow_list_cmd)\n#define KSU_IOCTL_UID_GRANTED_ROOT _IOC(_IOC_READ|_IOC_WRITE, 'K', 8, 0)\n#define KSU_IOCTL_UID_SHOULD_UMOUNT _IOC(_IOC_READ|_IOC_WRITE, 'K', 9, 0)\n#define KSU_IOCTL_GET_MANAGER_APPID _IOC(_IOC_READ, 'K', 10, 0)\n#define KSU_IOCTL_GET_APP_PROFILE _IOC(_IOC_READ|_IOC_WRITE, 'K', 11, 0)\n#define KSU_IOCTL_SET_APP_PROFILE _IOC(_IOC_WRITE, 'K', 12, 0)\n#define KSU_IOCTL_GET_FEATURE _IOC(_IOC_READ|_IOC_WRITE, 'K', 13, 0)\n#define KSU_IOCTL_SET_FEATURE _IOC(_IOC_WRITE, 'K', 14, 0)\n\nbool get_allow_list(struct ksu_new_get_allow_list_cmd *);\n\ninline std::pair<int, int> legacy_get_info() {\n    int32_t version = -1;\n    int32_t flags = 0;\n    int32_t result = 0;\n    prctl(0xDEADBEEF, 2, &version, &flags, &result);\n    return {version, flags};\n}\n\n#endif //KERNELSU_KSU_H\n"
  },
  {
    "path": "manager/app/src/main/cpp/logging.h",
    "content": "#pragma once\n\n#include <android/log.h>\n#include <cerrno>\n#include <cstring>\n#include <string>\n\n#ifndef LOG_TAG\n# define LOG_TAG \"KernelSU\"\n#endif\n\n#ifndef NDEBUG\n#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)\n#define LOGV(...)  __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__)\n#else\n#define LOGD(...)\n#define LOGV(...)\n#endif\n#define LOGI(...)  __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)\n#define LOGW(...)  __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)\n#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)\n#define LOGF(...)  __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__)\n#define PLOGE(fmt, args...) LOGE(fmt \" failed with %d: %s\", ##args, errno, strerror(errno))\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/KernelSUApplication.kt",
    "content": "package me.weishu.kernelsu\n\nimport android.app.Application\nimport android.content.pm.ApplicationInfo\nimport android.os.Build\nimport android.system.Os\nimport androidx.lifecycle.ViewModelProvider\nimport androidx.lifecycle.ViewModelStore\nimport androidx.lifecycle.ViewModelStoreOwner\nimport me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel\nimport okhttp3.Cache\nimport okhttp3.OkHttpClient\nimport org.lsposed.hiddenapibypass.HiddenApiBypass\nimport java.io.File\nimport java.util.Locale\n\nlateinit var ksuApp: KernelSUApplication\n\nclass KernelSUApplication : Application(), ViewModelStoreOwner {\n\n    companion object {\n        fun setEnableOnBackInvokedCallback(appInfo: ApplicationInfo, enable: Boolean) {\n            runCatching {\n                val applicationInfoClass = ApplicationInfo::class.java\n                val method = applicationInfoClass.getDeclaredMethod(\"setEnableOnBackInvokedCallback\", Boolean::class.javaPrimitiveType)\n                method.isAccessible = true\n                method.invoke(appInfo, enable)\n            }\n        }\n    }\n\n    lateinit var okhttpClient: OkHttpClient\n    private val appViewModelStore by lazy { ViewModelStore() }\n\n    override fun onCreate() {\n        super.onCreate()\n        ksuApp = this\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {\n            val prefs = this.getSharedPreferences(\"settings\", MODE_PRIVATE)\n            val enable = prefs.getBoolean(\"enable_predictive_back\", false)\n            HiddenApiBypass.addHiddenApiExemptions(\"Landroid/content/pm/ApplicationInfo;->setEnableOnBackInvokedCallback\")\n            setEnableOnBackInvokedCallback(applicationInfo, enable)\n        }\n\n        val superUserViewModel = ViewModelProvider(this)[SuperUserViewModel::class.java]\n        superUserViewModel.loadAppList()\n\n        val webroot = File(dataDir, \"webroot\")\n        if (!webroot.exists()) {\n            webroot.mkdir()\n        }\n\n        // Provide working env for rust's temp_dir()\n        Os.setenv(\"TMPDIR\", cacheDir.absolutePath, true)\n\n        okhttpClient =\n            OkHttpClient.Builder().cache(Cache(File(cacheDir, \"okhttp\"), 10 * 1024 * 1024))\n                .addInterceptor { block ->\n                    block.proceed(\n                        block.request().newBuilder()\n                            .header(\"User-Agent\", \"KernelSU/${BuildConfig.VERSION_CODE}\")\n                            .header(\"Accept-Language\", Locale.getDefault().toLanguageTag()).build()\n                    )\n                }.build()\n    }\n\n    override val viewModelStore: ViewModelStore\n        get() = appViewModelStore\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/Kernels.kt",
    "content": "package me.weishu.kernelsu\n\nimport android.system.Os\n\n/**\n * @author weishu\n * @date 2022/12/10.\n */\n\ndata class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) {\n    override fun toString(): String {\n        return \"$major.$patchLevel.$subLevel\"\n    }\n\n    fun isGKI(): Boolean {\n\n        // kernel 6.x\n        if (major > 5) {\n            return true\n        }\n\n        // kernel 5.10.x\n        if (major == 5) {\n            return patchLevel >= 10\n        }\n\n        return false\n    }\n}\n\nfun parseKernelVersion(version: String): KernelVersion {\n    val find = \"(\\\\d+)\\\\.(\\\\d+)\\\\.(\\\\d+)\".toRegex().find(version)\n    return if (find != null) {\n        KernelVersion(find.groupValues[1].toInt(), find.groupValues[2].toInt(), find.groupValues[3].toInt())\n    } else {\n        KernelVersion(-1, -1, -1)\n    }\n}\n\nfun getKernelVersion(): KernelVersion {\n    Os.uname().release.let {\n        return parseKernelVersion(it)\n    }\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/Natives.kt",
    "content": "package me.weishu.kernelsu\n\nimport android.os.Parcelable\nimport androidx.annotation.Keep\nimport androidx.compose.runtime.Immutable\nimport kotlinx.parcelize.Parcelize\nimport kotlinx.serialization.Serializable\n\n/**\n * @author weishu\n * @date 2022/12/8.\n */\nobject Natives {\n    // minimal supported kernel version\n    // 10915: allowlist breaking change, add app profile\n    // 10931: app profile struct add 'version' field\n    // 10946: add capabilities\n    // 10977: change groups_count and groups to avoid overflow write\n    // 11071: Fix the issue of failing to set a custom SELinux type.\n    // 12143: breaking: new supercall impl\n    // 32310: new get_allow_list ioctl\n    // 32336: new set_sepolicy ioctl\n    const val MINIMAL_SUPPORTED_KERNEL = 32336\n\n    const val KERNEL_SU_DOMAIN = \"u:r:su:s0\"\n\n    const val ROOT_UID = 0\n    const val ROOT_GID = 0\n\n    init {\n        System.loadLibrary(\"kernelsu\")\n    }\n\n    val version: Int\n        external get\n\n    val isSafeMode: Boolean\n        external get\n\n    val isLkmMode: Boolean\n        external get\n\n    val isLateLoadMode: Boolean\n        external get\n\n    val isManager: Boolean\n        external get\n\n    val isPrBuild: Boolean\n        external get\n\n    external fun uidShouldUmount(uid: Int): Boolean\n\n    /**\n     * Get the profile of the given package.\n     * @param key usually the package name\n     * @return return null if failed.\n     */\n    external fun getAppProfile(key: String?, uid: Int): Profile\n    external fun setAppProfile(profile: Profile?): Boolean\n\n    /**\n     * `su` compat mode can be disabled temporarily.\n     *  0: disabled\n     *  1: enabled\n     *  negative : error\n     */\n    external fun isSuEnabled(): Boolean\n    external fun setSuEnabled(enabled: Boolean): Boolean\n\n    /**\n     * Kernel module umount can be disabled temporarily.\n     *  0: disabled\n     *  1: enabled\n     *  negative : error\n     */\n    external fun isKernelUmountEnabled(): Boolean\n    external fun setKernelUmountEnabled(enabled: Boolean): Boolean\n\n    /**\n     * Get the user name for the uid.\n     */\n    external fun getUserName(uid: Int): String?\n\n    external fun getSuperuserCount(): Int\n\n    private const val NON_ROOT_DEFAULT_PROFILE_KEY = \"$\"\n    private const val NOBODY_UID = 9999\n\n    fun setDefaultUmountModules(umountModules: Boolean): Boolean {\n        Profile(\n            NON_ROOT_DEFAULT_PROFILE_KEY,\n            NOBODY_UID,\n            false,\n            umountModules = umountModules\n        ).let {\n            return setAppProfile(it)\n        }\n    }\n\n    fun isDefaultUmountModules(): Boolean {\n        getAppProfile(NON_ROOT_DEFAULT_PROFILE_KEY, NOBODY_UID).let {\n            return it.umountModules\n        }\n    }\n\n    fun requireNewKernel(): Boolean {\n        return version != -1 && version < MINIMAL_SUPPORTED_KERNEL\n    }\n\n    @Keep\n    @Immutable\n    @Parcelize\n    @Serializable\n    data class Profile(\n        // and there is a default profile for root and non-root\n        val name: String,\n        // current uid for the package, this is convivent for kernel to check\n        // if the package name doesn't match uid, then it should be invalidated.\n        val currentUid: Int = 0,\n\n        // if this is true, kernel will grant root permission to this package\n        val allowSu: Boolean = false,\n\n        // these are used for root profile\n        val rootUseDefault: Boolean = true,\n        val rootTemplate: String? = null,\n        val uid: Int = ROOT_UID,\n        val gid: Int = ROOT_GID,\n        val groups: List<Int> = mutableListOf(),\n        val capabilities: List<Int> = mutableListOf(),\n        val context: String = KERNEL_SU_DOMAIN,\n        val namespace: Int = Namespace.INHERITED.ordinal,\n\n        val nonRootUseDefault: Boolean = true,\n        val umountModules: Boolean = true,\n        var rules: String = \"\", // this field is save in ksud!!\n    ) : Parcelable {\n        enum class Namespace {\n            INHERITED,\n            GLOBAL,\n            INDIVIDUAL,\n        }\n\n        constructor() : this(\"\")\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/model/AppInfo.kt",
    "content": "package me.weishu.kernelsu.data.model\n\nimport android.content.pm.PackageInfo\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\nimport me.weishu.kernelsu.Natives\n\n@Parcelize\ndata class AppInfo(\n    val label: String,\n    val packageInfo: PackageInfo,\n    val profile: Natives.Profile?,\n) : Parcelable {\n    val packageName: String\n        get() = packageInfo.packageName\n    val uid: Int\n        get() = packageInfo.applicationInfo!!.uid\n\n    val allowSu: Boolean\n        get() = profile != null && profile.allowSu\n    val hasCustomProfile: Boolean\n        get() {\n            if (profile == null) {\n                return false\n            }\n\n            return if (profile.allowSu) {\n                !profile.rootUseDefault\n            } else {\n                !profile.nonRootUseDefault\n            }\n        }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/model/Module.kt",
    "content": "package me.weishu.kernelsu.data.model\n\nimport androidx.compose.runtime.Immutable\n\n@Immutable\ndata class Module(\n    val id: String,\n    val name: String,\n    val author: String,\n    val version: String,\n    val versionCode: Int,\n    val description: String,\n    val enabled: Boolean,\n    val update: Boolean,\n    val remove: Boolean,\n    val updateJson: String,\n    val hasWebUi: Boolean,\n    val hasActionScript: Boolean,\n    val metamodule: Boolean,\n    val actionIconPath: String?,\n    val webUiIconPath: String?,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/model/ModuleUpdateInfo.kt",
    "content": "package me.weishu.kernelsu.data.model\n\nimport androidx.compose.runtime.Immutable\n\n@Immutable\ndata class ModuleUpdateInfo(\n    val downloadUrl: String,\n    val version: String,\n    val changelog: String\n) {\n    companion object {\n        val Empty = ModuleUpdateInfo(\"\", \"\", \"\")\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/model/RepoModule.kt",
    "content": "package me.weishu.kernelsu.data.model\n\nimport androidx.compose.runtime.Immutable\n\n@Immutable\ndata class Author(\n    val name: String,\n    val link: String,\n)\n\n@Immutable\ndata class ReleaseAsset(\n    val name: String,\n    val downloadUrl: String,\n    val size: Long\n)\n\n@Immutable\ndata class RepoModule(\n    val moduleId: String,\n    val moduleName: String,\n    val authors: String,\n    val authorList: List<Author>,\n    val summary: String,\n    val metamodule: Boolean,\n    val stargazerCount: Int,\n    val updatedAt: String,\n    val createdAt: String,\n    val latestRelease: String,\n    val latestReleaseTime: String,\n    val latestVersionCode: Int,\n    val latestAsset: ReleaseAsset?,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/model/TemplateInfo.kt",
    "content": "package me.weishu.kernelsu.data.model\n\nimport android.os.Parcelable\nimport android.util.Log\nimport kotlinx.parcelize.Parcelize\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.profile.Capabilities\nimport me.weishu.kernelsu.profile.Groups\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.util.Locale\n\n@Parcelize\ndata class TemplateInfo(\n    val id: String = \"\",\n    val name: String = \"\",\n    val description: String = \"\",\n    val author: String = \"\",\n    val local: Boolean = true,\n    val namespace: Int = Natives.Profile.Namespace.INHERITED.ordinal,\n    val uid: Int = Natives.ROOT_UID,\n    val gid: Int = Natives.ROOT_GID,\n    val groups: List<Int> = mutableListOf(),\n    val capabilities: List<Int> = mutableListOf(),\n    val context: String = Natives.KERNEL_SU_DOMAIN,\n    val rules: List<String> = mutableListOf(),\n) : Parcelable {\n    companion object {\n        private const val TAG = \"TemplateInfo\"\n\n        fun fromJSON(templateJson: JSONObject): TemplateInfo? {\n            return runCatching {\n                val groupsJsonArray = templateJson.optJSONArray(\"groups\")\n                val capabilitiesJsonArray = templateJson.optJSONArray(\"capabilities\")\n                val context = templateJson.optString(\"context\").takeIf { it.isNotEmpty() }\n                    ?: Natives.KERNEL_SU_DOMAIN\n                val namespace = templateJson.optString(\"namespace\").takeIf { it.isNotEmpty() }\n                    ?: Natives.Profile.Namespace.INHERITED.name\n\n                val rulesJsonArray = templateJson.optJSONArray(\"rules\")\n                val templateInfo = TemplateInfo(\n                    id = templateJson.getString(\"id\"),\n                    name = getLocaleString(templateJson, \"name\"),\n                    description = getLocaleString(templateJson, \"description\"),\n                    author = templateJson.optString(\"author\"),\n                    local = templateJson.optBoolean(\"local\"),\n                    namespace = Natives.Profile.Namespace.valueOf(\n                        namespace.uppercase()\n                    ).ordinal,\n                    uid = templateJson.optInt(\"uid\", Natives.ROOT_UID),\n                    gid = templateJson.optInt(\"gid\", Natives.ROOT_GID),\n                    groups = getEnumOrdinals(groupsJsonArray, Groups::class.java).map { it.gid },\n                    capabilities = getEnumOrdinals(\n                        capabilitiesJsonArray, Capabilities::class.java\n                    ).map { it.cap },\n                    context = context,\n                    rules = rulesJsonArray?.mapCatching<String, String>({ it }, {\n                        Log.e(TAG, \"ignore invalid rule: $it\", it)\n                    }).orEmpty()\n                )\n                templateInfo\n            }.onFailure {\n                Log.e(TAG, \"ignore invalid template: $it\", it)\n            }.getOrNull()\n        }\n\n        private fun getLocaleString(json: JSONObject, key: String): String {\n            val fallback = json.getString(key)\n            val locale = Locale.getDefault()\n            val localeKey = \"${locale.language}_${locale.country}\"\n            json.optJSONObject(\"locales\")?.let {\n                // check locale first\n                it.optJSONObject(localeKey)?.let { json ->\n                    return json.optString(key, fallback)\n                }\n                // fallback to language\n                it.optJSONObject(locale.language)?.let { json ->\n                    return json.optString(key, fallback)\n                }\n            }\n            return fallback\n        }\n\n        @Suppress(\"UNCHECKED_CAST\")\n        private fun <T, R> JSONArray.mapCatching(\n            transform: (T) -> R, onFail: (Throwable) -> Unit\n        ): List<R> {\n            return List(length()) { i -> get(i) as T }.mapNotNull { element ->\n                runCatching {\n                    transform(element)\n                }.onFailure(onFail).getOrNull()\n            }\n        }\n\n        private inline fun <reified T : Enum<T>> getEnumOrdinals(\n            jsonArray: JSONArray?, enumClass: Class<T>\n        ): List<T> {\n            return jsonArray?.mapCatching<String, T>({ name ->\n                enumValueOf(name.uppercase())\n            }, {\n                Log.e(TAG, \"ignore invalid enum ${enumClass.simpleName}: $it\", it)\n            }).orEmpty()\n        }\n    }\n\n    fun toJSON(): JSONObject {\n        val template = this\n        return JSONObject().apply {\n\n            put(\"id\", template.id)\n            put(\"name\", template.name.ifBlank { template.id })\n            put(\"description\", template.description.ifBlank { template.id })\n            if (template.author.isNotEmpty()) {\n                put(\"author\", template.author)\n            }\n            put(\"namespace\", Natives.Profile.Namespace.entries[template.namespace].name)\n            put(\"uid\", template.uid)\n            put(\"gid\", template.gid)\n\n            if (template.groups.isNotEmpty()) {\n                put(\n                    \"groups\", JSONArray(\n                        Groups.entries.filter {\n                            template.groups.contains(it.gid)\n                        }.map {\n                            it.name\n                        }\n                    ))\n            }\n\n            if (template.capabilities.isNotEmpty()) {\n                put(\n                    \"capabilities\", JSONArray(\n                        Capabilities.entries.filter {\n                            template.capabilities.contains(it.cap)\n                        }.map {\n                            it.name\n                        }\n                    ))\n            }\n\n            if (template.context.isNotEmpty()) {\n                put(\"context\", template.context)\n            }\n\n            if (template.rules.isNotEmpty()) {\n                put(\"rules\", JSONArray(template.rules))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/repository/ModuleRepoRepository.kt",
    "content": "package me.weishu.kernelsu.data.repository\n\nimport me.weishu.kernelsu.data.model.RepoModule\n\ninterface ModuleRepoRepository {\n    suspend fun fetchModules(): Result<List<RepoModule>>\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/repository/ModuleRepoRepositoryImpl.kt",
    "content": "package me.weishu.kernelsu.data.repository\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.data.model.Author\nimport me.weishu.kernelsu.data.model.ReleaseAsset\nimport me.weishu.kernelsu.data.model.RepoModule\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.util.isNetworkAvailable\nimport okhttp3.Request\nimport org.json.JSONArray\nimport org.json.JSONObject\n\nclass ModuleRepoRepositoryImpl : ModuleRepoRepository {\n\n    companion object {\n        private const val MODULES_URL = \"https://modules.kernelsu.org/modules.json\"\n    }\n\n    override suspend fun fetchModules(): Result<List<RepoModule>> = withContext(Dispatchers.IO) {\n        runCatching {\n            if (!isNetworkAvailable(ksuApp)) {\n                throw Exception(\"Network unavailable\")\n            }\n\n            val request = Request.Builder().url(MODULES_URL).build()\n            ksuApp.okhttpClient.newCall(request).execute().use { response ->\n                if (!response.isSuccessful) {\n                    throw Exception(\"Fetch failed: ${response.code}\")\n                }\n\n                val body = response.body.string()\n                val json = JSONArray(body)\n                (0 until json.length()).mapNotNull { idx ->\n                    val item = json.optJSONObject(idx) ?: return@mapNotNull null\n                    parseRepoModule(item)\n                }\n            }\n        }\n    }\n\n    private fun parseRepoModule(item: JSONObject): RepoModule? {\n        val moduleId = item.optString(\"moduleId\", \"\")\n        if (moduleId.isEmpty()) return null\n        val moduleName = item.optString(\"moduleName\", \"\")\n        val authorsArray = item.optJSONArray(\"authors\")\n        val authorList = if (authorsArray != null) {\n            (0 until authorsArray.length())\n                .mapNotNull { idx ->\n                    val authorObj = authorsArray.optJSONObject(idx) ?: return@mapNotNull null\n                    val name = authorObj.optString(\"name\", \"\").trim()\n                    var link = authorObj.optString(\"link\", \"\").trim()\n                    if (link.startsWith(\"`\") && link.endsWith(\"`\") && link.length >= 2) {\n                        link = link.substring(1, link.length - 1)\n                    }\n                    if (name.isEmpty()) null else Author(name = name, link = link)\n                }\n        } else {\n            emptyList()\n        }\n        val authors = if (authorList.isNotEmpty()) authorList.joinToString(\", \") { it.name } else item.optString(\"authors\", \"\")\n        val summary = item.optString(\"summary\", \"\")\n        val metamodule = item.optBoolean(\"metamodule\", false)\n        val stargazerCount = item.optInt(\"stargazerCount\", 0)\n        val updatedAt = item.optString(\"updatedAt\", \"\")\n        val createdAt = item.optString(\"createdAt\", \"\")\n\n        var latestRelease = \"\"\n        var latestReleaseTime = \"\"\n        var latestVersionCode = 0\n        var latestAsset: ReleaseAsset? = null\n        val lr = item.optJSONObject(\"latestRelease\")\n        if (lr != null) {\n            val lrName = lr.optString(\"name\", lr.optString(\"version\", \"\"))\n            val lrTime = lr.optString(\"time\", \"\")\n            var lrUrl = lr.optString(\"downloadUrl\", \"\")\n            lrUrl = lrUrl.trim().let {\n                var s = it\n                if (s.startsWith(\"`\") && s.endsWith(\"`\") && s.length >= 2) {\n                    s = s.substring(1, s.length - 1)\n                }\n                s\n            }\n            val vcAny = lr.opt(\"versionCode\")\n            latestVersionCode = when (vcAny) {\n                is Number -> vcAny.toInt()\n                is String -> vcAny.toIntOrNull() ?: 0\n                else -> 0\n            }\n            latestRelease = lrName\n            latestReleaseTime = lrTime\n            if (lrUrl.isNotEmpty()) {\n                val fileName = lrUrl.substringAfterLast('/')\n                latestAsset = ReleaseAsset(name = fileName, downloadUrl = lrUrl, size = 0L)\n            }\n        }\n\n        return RepoModule(\n            moduleId = moduleId,\n            moduleName = moduleName,\n            authors = authors,\n            authorList = authorList,\n            summary = summary,\n            metamodule = metamodule,\n            stargazerCount = stargazerCount,\n            updatedAt = updatedAt,\n            createdAt = createdAt,\n            latestRelease = latestRelease,\n            latestReleaseTime = latestReleaseTime,\n            latestVersionCode = latestVersionCode,\n            latestAsset = latestAsset,\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/repository/ModuleRepository.kt",
    "content": "package me.weishu.kernelsu.data.repository\n\nimport me.weishu.kernelsu.data.model.Module\nimport me.weishu.kernelsu.data.model.ModuleUpdateInfo\n\ninterface ModuleRepository {\n    suspend fun getModules(): Result<List<Module>>\n    suspend fun checkUpdate(module: Module): Result<ModuleUpdateInfo>\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/repository/ModuleRepositoryImpl.kt",
    "content": "package me.weishu.kernelsu.data.repository\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.data.model.Module\nimport me.weishu.kernelsu.data.model.ModuleUpdateInfo\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.util.isNetworkAvailable\nimport me.weishu.kernelsu.ui.util.listModules\nimport me.weishu.kernelsu.ui.util.module.sanitizeVersionString\nimport okhttp3.Request\nimport org.json.JSONArray\nimport org.json.JSONObject\n\nclass ModuleRepositoryImpl : ModuleRepository {\n\n    companion object {\n        private const val TAG = \"ModuleRepository\"\n    }\n\n    override suspend fun getModules(): Result<List<Module>> = withContext(Dispatchers.IO) {\n        runCatching {\n            val result = listModules()\n            val array = JSONArray(result)\n            (0 until array.length())\n                .asSequence()\n                .map { array.getJSONObject(it) }\n                .map { obj ->\n                    Module(\n                        id = obj.getString(\"id\"),\n                        name = obj.optString(\"name\"),\n                        author = obj.optString(\"author\", \"Unknown\"),\n                        version = obj.optString(\"version\", \"Unknown\"),\n                        versionCode = obj.optInt(\"versionCode\", 0),\n                        description = obj.optString(\"description\"),\n                        enabled = obj.getBoolean(\"enabled\"),\n                        update = obj.optBoolean(\"update\"),\n                        remove = obj.getBoolean(\"remove\"),\n                        updateJson = obj.optString(\"updateJson\"),\n                        hasWebUi = obj.optBoolean(\"web\"),\n                        hasActionScript = obj.optBoolean(\"action\"),\n                        metamodule = (obj.optInt(\"metamodule\") != 0) || obj.optBoolean(\"metamodule\"),\n                        actionIconPath = obj.optString(\"actionIcon\").takeIf { it.isNotBlank() },\n                        webUiIconPath = obj.optString(\"webuiIcon\").takeIf { it.isNotBlank() }\n                    )\n                }.toList()\n        }\n    }\n\n    override suspend fun checkUpdate(module: Module): Result<ModuleUpdateInfo> = withContext(Dispatchers.IO) {\n        runCatching {\n            if (!isNetworkAvailable(ksuApp)) {\n                return@runCatching ModuleUpdateInfo.Empty\n            }\n            if (module.updateJson.isEmpty() || module.remove || module.update || !module.enabled) {\n                return@runCatching ModuleUpdateInfo.Empty\n            }\n\n            val url = module.updateJson\n            val response = ksuApp.okhttpClient.newCall(\n                Request.Builder().url(url).build()\n            ).execute()\n\n            val result = if (response.isSuccessful) {\n                response.body.string()\n            } else {\n                \"\"\n            }\n\n            if (result.isEmpty()) {\n                return@runCatching ModuleUpdateInfo.Empty\n            }\n\n            val updateJson = JSONObject(result)\n            var version = updateJson.optString(\"version\", \"\")\n            version = sanitizeVersionString(version)\n            val versionCode = updateJson.optInt(\"versionCode\", 0)\n            val zipUrl = updateJson.optString(\"zipUrl\", \"\")\n            val changelog = updateJson.optString(\"changelog\", \"\")\n\n            if (versionCode <= module.versionCode || zipUrl.isEmpty()) {\n                ModuleUpdateInfo.Empty\n            } else {\n                ModuleUpdateInfo(zipUrl, version, changelog)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/repository/SettingsRepository.kt",
    "content": "package me.weishu.kernelsu.data.repository\n\ninterface SettingsRepository {\n    var uiMode: String\n    var checkUpdate: Boolean\n    var checkModuleUpdate: Boolean\n    var themeMode: Int\n    var miuixMonet: Boolean\n    var keyColor: Int\n    var colorStyle: String\n    var colorSpec: String\n    var enablePredictiveBack: Boolean\n    var enableBlur: Boolean\n    var enableFloatingBottomBar: Boolean\n    var enableFloatingBottomBarBlur: Boolean\n    var pageScale: Float\n    var enableWebDebugging: Boolean\n    var autoJailbreak: Boolean\n\n    suspend fun getSuCompatStatus(): String\n    suspend fun getSuCompatPersistValue(): Long?\n    fun isSuEnabled(): Boolean\n    fun setSuEnabled(enabled: Boolean): Boolean\n    fun setSuCompatModePref(mode: Int)\n    fun getSuCompatModePref(): Int\n\n    suspend fun getKernelUmountStatus(): String\n    fun isKernelUmountEnabled(): Boolean\n    fun setKernelUmountEnabled(enabled: Boolean): Boolean\n\n    fun isDefaultUmountModules(): Boolean\n    fun setDefaultUmountModules(enabled: Boolean): Boolean\n\n    fun isLkmMode(): Boolean\n\n    fun execKsudFeatureSave()\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/repository/SettingsRepositoryImpl.kt",
    "content": "package me.weishu.kernelsu.data.repository\n\nimport android.content.ComponentName\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport android.util.Log\nimport androidx.core.content.edit\nimport com.materialkolor.PaletteStyle\nimport com.materialkolor.dynamiccolor.ColorSpec\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.magica.BootCompletedReceiver\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.util.execKsud\nimport me.weishu.kernelsu.ui.util.getFeaturePersistValue\nimport me.weishu.kernelsu.ui.util.getFeatureStatus\n\nclass SettingsRepositoryImpl : SettingsRepository {\n\n    private val prefs by lazy {\n        ksuApp.getSharedPreferences(\"settings\", Context.MODE_PRIVATE)\n    }\n\n    override var uiMode: String\n        get() = prefs.getString(\"ui_mode\", UiMode.DEFAULT_VALUE) ?: UiMode.DEFAULT_VALUE\n        set(value) = prefs.edit { putString(\"ui_mode\", value) }\n\n    override var checkUpdate: Boolean\n        get() = prefs.getBoolean(\"check_update\", true)\n        set(value) = prefs.edit { putBoolean(\"check_update\", value) }\n\n    override var checkModuleUpdate: Boolean\n        get() = prefs.getBoolean(\"module_check_update\", true)\n        set(value) = prefs.edit { putBoolean(\"module_check_update\", value) }\n\n    override var themeMode: Int\n        get() = prefs.getInt(\"color_mode\", 0)\n        set(value) = prefs.edit { putInt(\"color_mode\", value) }\n\n    override var miuixMonet: Boolean\n        get() = prefs.getBoolean(\"miuix_monet\", false)\n        set(value) = prefs.edit { putBoolean(\"miuix_monet\", value) }\n\n    override var keyColor: Int\n        get() = prefs.getInt(\"key_color\", 0)\n        set(value) = prefs.edit { putInt(\"key_color\", value) }\n\n    override var colorStyle: String\n        get() = prefs.getString(\"color_style\", PaletteStyle.TonalSpot.name) ?: PaletteStyle.TonalSpot.name\n        set(value) = prefs.edit { putString(\"color_style\", value) }\n\n    override var colorSpec: String\n        get() = prefs.getString(\"color_spec\", ColorSpec.SpecVersion.Default.name) ?: ColorSpec.SpecVersion.Default.name\n        set(value) = prefs.edit { putString(\"color_spec\", value) }\n\n    override var enablePredictiveBack: Boolean\n        get() = prefs.getBoolean(\"enable_predictive_back\", false)\n        set(value) = prefs.edit { putBoolean(\"enable_predictive_back\", value) }\n\n    override var enableBlur: Boolean\n        get() = prefs.getBoolean(\"enable_blur\", false)\n        set(value) = prefs.edit { putBoolean(\"enable_blur\", value) }\n\n    override var enableFloatingBottomBar: Boolean\n        get() = prefs.getBoolean(\"enable_floating_bottom_bar\", false)\n        set(value) = prefs.edit { putBoolean(\"enable_floating_bottom_bar\", value) }\n\n    override var enableFloatingBottomBarBlur: Boolean\n        get() = prefs.getBoolean(\"enable_floating_bottom_bar_blur\", false)\n        set(value) = prefs.edit { putBoolean(\"enable_floating_bottom_bar_blur\", value) }\n\n    override var pageScale: Float\n        get() = prefs.getFloat(\"page_scale\", 1.0f)\n        set(value) = prefs.edit { putFloat(\"page_scale\", value) }\n\n    override var enableWebDebugging: Boolean\n        get() = prefs.getBoolean(\"enable_web_debugging\", false)\n        set(value) = prefs.edit { putBoolean(\"enable_web_debugging\", value) }\n\n    override var autoJailbreak: Boolean\n        get() = prefs.getBoolean(\"auto_jailbreak\", false)\n        set(value) {\n            runCatching {\n                ksuApp.packageManager.setComponentEnabledSetting(\n                    ComponentName(ksuApp, BootCompletedReceiver::class.java),\n                    if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED,\n                    PackageManager.DONT_KILL_APP\n                )\n            }.onFailure {\n                Log.e(\"Settings\", \"failed to change boot receiver state to $value\", it)\n            }\n            prefs.edit {\n                putBoolean(\"auto_jailbreak\", value)\n            }\n        }\n\n    override suspend fun getSuCompatStatus(): String = getFeatureStatus(\"su_compat\")\n\n    override suspend fun getSuCompatPersistValue(): Long? = getFeaturePersistValue(\"su_compat\")\n\n    override fun isSuEnabled(): Boolean = Natives.isSuEnabled()\n\n    override fun setSuEnabled(enabled: Boolean): Boolean = Natives.setSuEnabled(enabled)\n\n    override fun setSuCompatModePref(mode: Int) = prefs.edit { putInt(\"su_compat_mode\", mode) }\n\n    override fun getSuCompatModePref(): Int = prefs.getInt(\"su_compat_mode\", 0)\n\n    override suspend fun getKernelUmountStatus(): String = getFeatureStatus(\"kernel_umount\")\n\n    override fun isKernelUmountEnabled(): Boolean = Natives.isKernelUmountEnabled()\n\n    override fun setKernelUmountEnabled(enabled: Boolean): Boolean = Natives.setKernelUmountEnabled(enabled)\n\n    override fun isDefaultUmountModules(): Boolean = Natives.isDefaultUmountModules()\n\n    override fun setDefaultUmountModules(enabled: Boolean): Boolean = Natives.setDefaultUmountModules(enabled)\n\n    override fun isLkmMode(): Boolean = Natives.isLkmMode\n\n    override fun execKsudFeatureSave() {\n        execKsud(\"feature save\", true)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/repository/SuperUserRepository.kt",
    "content": "package me.weishu.kernelsu.data.repository\n\nimport me.weishu.kernelsu.data.model.AppInfo\n\ninterface SuperUserRepository {\n    suspend fun getAppList(): Result<Pair<List<AppInfo>, List<Int>>>\n    suspend fun refreshProfiles(currentApps: List<AppInfo>): Result<List<AppInfo>>\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/repository/SuperUserRepositoryImpl.kt",
    "content": "package me.weishu.kernelsu.data.repository\n\nimport android.content.ComponentName\nimport android.content.Intent\nimport android.content.ServiceConnection\nimport android.os.Handler\nimport android.os.IBinder\nimport android.os.Looper\nimport android.os.SystemClock\nimport android.util.Log\nimport com.topjohnwu.superuser.Shell\nimport com.topjohnwu.superuser.ipc.RootService\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.IKsuInterface\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.data.model.AppInfo\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.KsuService\nimport me.weishu.kernelsu.ui.util.KsuCli\nimport kotlin.coroutines.resume\n\nclass SuperUserRepositoryImpl : SuperUserRepository {\n\n    companion object {\n        private const val TAG = \"SuperUserRepository\"\n    }\n\n    override suspend fun getAppList(): Result<Pair<List<AppInfo>, List<Int>>> = withContext(Dispatchers.IO) {\n        runCatching {\n            val result = connectKsuService {\n                Log.w(TAG, \"KsuService disconnected\")\n            }\n\n            var currentBinder = result.first\n            var currentConnection = result.second\n\n            try {\n                suspend fun reconnect(): IKsuInterface {\n                    withContext(Dispatchers.Main) {\n                        RootService.unbind(currentConnection)\n                    }\n                    val retry = connectKsuService { Log.w(TAG, \"KsuService disconnected\") }\n                    currentBinder = retry.first\n                    currentConnection = retry.second\n                    return IKsuInterface.Stub.asInterface(currentBinder)\n                }\n\n                val pm = ksuApp.packageManager\n                val start = SystemClock.elapsedRealtime()\n\n                var iface = IKsuInterface.Stub.asInterface(currentBinder)\n                val idsArray = try {\n                    iface.userIds\n                } catch (_: Exception) {\n                    iface = reconnect()\n                    iface.userIds\n                }\n\n                val slice = try {\n                    iface.getPackages(0)\n                } catch (_: Exception) {\n                    iface = reconnect()\n                    iface.getPackages(0)\n                }\n\n                val packages = slice.list\n                val newApps = packages.map {\n                    val appInfo = it.applicationInfo\n                    val uid = appInfo!!.uid\n                    val profile = Natives.getAppProfile(it.packageName, uid)\n                    AppInfo(\n                        label = appInfo.loadLabel(pm).toString(),\n                        packageInfo = it,\n                        profile = profile,\n                    )\n                }.filter {\n                    val ai = it.packageInfo.applicationInfo!!\n                    !ai.isResourceOverlay\n                }\n\n                Log.i(TAG, \"load cost: ${SystemClock.elapsedRealtime() - start}\")\n                Pair(newApps, idsArray.toList())\n            } finally {\n                withContext(Dispatchers.Main) {\n                    RootService.unbind(currentConnection)\n                }\n            }\n        }\n    }\n\n    override suspend fun refreshProfiles(currentApps: List<AppInfo>): Result<List<AppInfo>> = withContext(Dispatchers.IO) {\n        runCatching {\n            if (currentApps.isEmpty()) return@runCatching emptyList()\n\n            currentApps.map {\n                val profile = Natives.getAppProfile(it.packageName, it.uid)\n                it.copy(profile = profile)\n            }\n        }\n    }\n\n    private suspend inline fun connectKsuService(\n        crossinline onDisconnect: () -> Unit = {}\n    ): Pair<IBinder, ServiceConnection> = withContext(Dispatchers.Main) {\n        suspendCancellableCoroutine { cont ->\n            val connection = object : ServiceConnection {\n                override fun onServiceDisconnected(name: ComponentName?) {\n                    onDisconnect()\n                }\n\n                override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {\n                    if (cont.isActive) {\n                        cont.resume(binder as IBinder to this)\n                    }\n                }\n            }\n\n            cont.invokeOnCancellation {\n                if (Looper.myLooper() == Looper.getMainLooper()) {\n                    RootService.unbind(connection)\n                } else {\n                    Handler(Looper.getMainLooper()).post {\n                        RootService.unbind(connection)\n                    }\n                }\n            }\n\n            val intent = Intent(ksuApp, KsuService::class.java)\n\n            val task = RootService.bindOrTask(\n                intent,\n                Shell.EXECUTOR,\n                connection,\n            )\n            val shell = KsuCli.SHELL\n            task?.let { shell.execTask(it) }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/repository/TemplateRepository.kt",
    "content": "package me.weishu.kernelsu.data.repository\n\nimport me.weishu.kernelsu.data.model.TemplateInfo\n\ninterface TemplateRepository {\n    suspend fun getTemplates(sync: Boolean): Result<List<TemplateInfo>>\n    suspend fun importTemplates(jsonString: String): Result<Unit>\n    suspend fun exportTemplates(): Result<String>\n    suspend fun getTemplate(id: String): Result<TemplateInfo>\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/data/repository/TemplateRepositoryImpl.kt",
    "content": "package me.weishu.kernelsu.data.repository\n\nimport android.util.Log\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.data.model.TemplateInfo\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.util.getAppProfileTemplate\nimport me.weishu.kernelsu.ui.util.listAppProfileTemplates\nimport me.weishu.kernelsu.ui.util.setAppProfileTemplate\nimport okhttp3.Request\nimport org.json.JSONArray\nimport org.json.JSONObject\n\nclass TemplateRepositoryImpl : TemplateRepository {\n\n    companion object {\n        private const val TAG = \"TemplateRepository\"\n        private const val TEMPLATE_INDEX_URL = \"https://kernelsu.org/templates/index.json\"\n        private const val TEMPLATE_URL = \"https://kernelsu.org/templates/%s\"\n    }\n\n    override suspend fun getTemplates(sync: Boolean): Result<List<TemplateInfo>> = withContext(Dispatchers.IO) {\n        runCatching {\n            val localTemplateIds = listAppProfileTemplates()\n            Log.i(TAG, \"localTemplateIds: $localTemplateIds\")\n            if (localTemplateIds.isEmpty() || sync) {\n                fetchRemoteTemplates()\n            }\n            listAppProfileTemplates().mapNotNull { getTemplateInfoById(it) }\n        }\n    }\n\n    override suspend fun importTemplates(jsonString: String): Result<Unit> = withContext(Dispatchers.IO) {\n        runCatching {\n            val array = try {\n                JSONArray(jsonString)\n            } catch (e: Exception) {\n                try {\n                    val json = JSONObject(jsonString)\n                    JSONArray().apply { put(json) }\n                } catch (e: Exception) {\n                    throw Exception(\"invalid templates: $jsonString\")\n                }\n            }\n\n            (0 until array.length()).forEach { i ->\n                runCatching {\n                    val template = array.getJSONObject(i)\n                    val id = template.getString(\"id\")\n                    template.put(\"local\", true)\n                    setAppProfileTemplate(id, template.toString())\n                }.onFailure { e ->\n                    Log.e(TAG, \"ignore invalid template: $array\", e)\n                }\n            }\n        }\n    }\n\n    override suspend fun exportTemplates(): Result<String> = withContext(Dispatchers.IO) {\n        runCatching {\n            val templates = listAppProfileTemplates()\n                .mapNotNull { getTemplateInfoById(it) }\n                .filter { it.local }\n            if (templates.isEmpty()) {\n                throw Exception(\"No templates to export\")\n            }\n            JSONArray(templates.map { it.toJSON() }).toString()\n        }\n    }\n\n    override suspend fun getTemplate(id: String): Result<TemplateInfo> = withContext(Dispatchers.IO) {\n        runCatching {\n            getTemplateInfoById(id) ?: throw Exception(\"Template not found: $id\")\n        }\n    }\n\n    private fun fetchRemoteTemplates() {\n        runCatching {\n            ksuApp.okhttpClient.newCall(\n                Request.Builder().url(TEMPLATE_INDEX_URL).build()\n            ).execute().use { response ->\n                if (!response.isSuccessful) {\n                    return\n                }\n                val remoteTemplateIds = JSONArray(response.body.string())\n                Log.i(TAG, \"fetchRemoteTemplates: $remoteTemplateIds\")\n                (0 until remoteTemplateIds.length()).forEach { i ->\n                    val id = remoteTemplateIds.getString(i)\n                    Log.i(TAG, \"fetch template: $id\")\n                    val templateJson = ksuApp.okhttpClient.newCall(\n                        Request.Builder().url(TEMPLATE_URL.format(id)).build()\n                    ).runCatching {\n                        execute().use { response ->\n                            if (!response.isSuccessful) {\n                                return@forEach\n                            }\n                            response.body.string()\n                        }\n                    }.getOrNull() ?: return@forEach\n                    Log.i(TAG, \"template: $templateJson\")\n\n                    // validate remote template\n                    runCatching {\n                        val json = JSONObject(templateJson)\n                        TemplateInfo.fromJSON(json)?.let {\n                            // force local template\n                            json.put(\"local\", false)\n                            setAppProfileTemplate(id, json.toString())\n                        }\n                    }.onFailure {\n                        Log.e(TAG, \"ignore invalid template: $it\", it)\n                        return@forEach\n                    }\n                }\n            }\n        }.onFailure { Log.e(TAG, \"fetchRemoteTemplates: $it\", it) }\n    }\n\n    private fun getTemplateInfoById(id: String): TemplateInfo? {\n        return runCatching {\n            TemplateInfo.fromJSON(JSONObject(getAppProfileTemplate(id)))\n        }.onFailure {\n            Log.e(TAG, \"ignore invalid template: $it\", it)\n        }.getOrNull()\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/magica/AppZygotePreload.java",
    "content": "package me.weishu.kernelsu.magica;\n\nimport android.app.ZygotePreload;\nimport android.content.pm.ApplicationInfo;\nimport android.util.Log;\n\nimport androidx.annotation.NonNull;\n\nimport java.io.File;\n\npublic class AppZygotePreload implements ZygotePreload {\n    public static final String TAG = \"KernelSUMagica\";\n\n    private static native void forkDontCareAndExecKsud(String ksudPath);\n\n    @Override\n    public void doPreload(@NonNull ApplicationInfo appInfo) {\n        File f = new File(appInfo.nativeLibraryDir, \"libksud.so\");\n        try {\n            System.loadLibrary(\"kernelsu\");\n            Log.d(TAG, \"executing magica ...\");\n            forkDontCareAndExecKsud(f.getAbsolutePath());\n        } catch (Throwable t) {\n            Log.e(TAG, \"failed to late load\", t);\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/magica/BootCompletedReceiver.java",
    "content": "package me.weishu.kernelsu.magica;\n\nimport static me.weishu.kernelsu.magica.AppZygotePreload.TAG;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.util.Log;\n\npublic class BootCompletedReceiver extends BroadcastReceiver {\n\n    @Override\n    public void onReceive(Context context, Intent intent) {\n        if (intent == null) {\n            return;\n        }\n        var action = intent.getAction();\n        if (!Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action)\n                && !Intent.ACTION_BOOT_COMPLETED.equals(action)\n                && !\"me.weishu.kernelsu.magica.LAUNCH\".equals(action)) {\n            return;\n        }\n        try {\n            context.startService(new Intent(context, MagicaService.class));\n            Log.i(TAG, \"MagicaService started from boot action: \" + action);\n        } catch (Throwable e) {\n\n            Log.e(TAG, \"Failed to start MagicaService from boot action: \" + action, e);\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/magica/MagicaService.java",
    "content": "package me.weishu.kernelsu.magica;\n\nimport android.app.Service;\nimport android.content.Intent;\nimport android.os.Binder;\nimport android.os.IBinder;\n\nimport androidx.annotation.Nullable;\n\npublic class MagicaService extends Service {\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        return new Binder();\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/profile/Capabilities.kt",
    "content": "package me.weishu.kernelsu.profile\n\n/**\n * @author weishu\n * @date 2023/6/3.\n */\nenum class Capabilities(val cap: Int, val display: String, val desc: String) {\n    CAP_CHOWN(0, \"CHOWN\", \"Make arbitrary changes to file UIDs and GIDs (see chown(2))\"),\n    CAP_DAC_OVERRIDE(1, \"DAC_OVERRIDE\", \"Bypass file read, write, and execute permission checks\"),\n    CAP_DAC_READ_SEARCH(2, \"DAC_READ_SEARCH\", \"Bypass file read permission checks and directory read and execute permission checks\"),\n    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\"),\n    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\"),\n    CAP_KILL(5, \"KILL\", \"Bypass permission checks for sending signals (see kill(2)).\"),\n    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\"),\n    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\"),\n    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.)\"),\n    CAP_LINUX_IMMUTABLE(9, \"LINUX_IMMUTABLE\", \"Set the FS_APPEND_FL and FS_IMMUTABLE_FL inode flags (see chattr(1)).\"),\n    CAP_NET_BIND_SERVICE(10, \"NET_BIND_SERVICE\", \"Bind a socket to Internet domain\"),\n    CAP_NET_BROADCAST(11, \"NET_BROADCAST\", \"Make socket broadcasts, and listen to multicasts\"),\n    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\"),\n    CAP_NET_RAW(13, \"NET_RAW\", \"Use RAW and PACKET sockets\"),\n    CAP_IPC_LOCK(14, \"IPC_LOCK\", \"Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2))\"),\n    CAP_IPC_OWNER(15, \"IPC_OWNER\", \"Bypass permission checks for operations on System V IPC objects\"),\n    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\"),\n    CAP_SYS_RAWIO(17, \"SYS_RAWIO\", \"Perform I/O port operations (iopl(2) and ioperm(2)); access /proc/kcore\"),\n    CAP_SYS_CHROOT(18, \"SYS_CHROOT\", \"Use chroot(2)\"),\n    CAP_SYS_PTRACE(19, \"SYS_PTRACE\", \"Trace arbitrary processes using ptrace(2)\"),\n    CAP_SYS_PACCT(20, \"SYS_PACCT\", \"Use acct(2)\"),\n    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\"),\n    CAP_SYS_BOOT(22, \"SYS_BOOT\", \"Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution\"),\n    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)\"),\n    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\"),\n    CAP_SYS_TIME(25, \"SYS_TIME\", \"Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock\"),\n    CAP_SYS_TTY_CONFIG(26, \"SYS_TTY_CONFIG\", \"Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals\"),\n    CAP_MKNOD(27, \"MKNOD\", \"Create special files using mknod(2)\"),\n    CAP_LEASE(28, \"LEASE\", \"Establish leases on arbitrary files (see fcntl(2))\"),\n    CAP_AUDIT_WRITE(29, \"AUDIT_WRITE\", \"Write records to kernel auditing log\"),\n    CAP_AUDIT_CONTROL(30, \"AUDIT_CONTROL\", \"Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules\"),\n    CAP_SETFCAP(31, \"SETFCAP\", \"If file capabilities are supported: grant or remove any capability in any capability set to any file\"),\n    CAP_MAC_OVERRIDE(32, \"MAC_OVERRIDE\", \"Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM)\"),\n    CAP_MAC_ADMIN(33, \"MAC_ADMIN\", \"Allow MAC configuration or state changes. Implemented for the Smack LSM\"),\n    CAP_SYSLOG(34, \"SYSLOG\", \"Perform privileged syslog(2) operations. See syslog(2) for information on which operations require privilege\"),\n    CAP_WAKE_ALARM(35, \"WAKE_ALARM\", \"Trigger something that will wake up the system\"),\n    CAP_BLOCK_SUSPEND(36, \"BLOCK_SUSPEND\", \"Employ features that can block system suspend\"),\n    CAP_AUDIT_READ(37, \"AUDIT_READ\", \"Allow reading the audit log via a multicast netlink socket\"),\n    CAP_PERFMON(38, \"PERFMON\", \"Allow performance monitoring via perf_event_open(2)\"),\n    CAP_BPF(39, \"BPF\", \"Allow BPF operations via bpf(2)\"),\n    CAP_CHECKPOINT_RESTORE(40, \"CHECKPOINT_RESTORE\", \"Allow processes to be checkpointed via checkpoint/restore in user namespace(2)\"),\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/profile/Groups.kt",
    "content": "package me.weishu.kernelsu.profile\n\n/**\n * https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h\n * @author weishu\n * @date 2023/6/3.\n */\nenum class Groups(val gid: Int, val display: String, val desc: String) {\n    ROOT(0, \"root\", \"traditional unix root user\"),\n    DAEMON(1, \"daemon\", \"Traditional unix daemon owner.\"),\n    BIN(2, \"bin\", \"Traditional unix binaries owner.\"),\n    SYS(3, \"sys\", \"A group with the same gid on Linux/macOS/Android.\"),\n    SYSTEM(1000, \"system\", \"system server\"),\n    RADIO(1001, \"radio\", \"telephony subsystem, RIL\"),\n    BLUETOOTH(1002, \"bluetooth\", \"bluetooth subsystem\"),\n    GRAPHICS(1003, \"graphics\", \"graphics devices\"),\n    INPUT(1004, \"input\", \"input devices\"),\n    AUDIO(1005, \"audio\", \"audio devices\"),\n    CAMERA(1006, \"camera\", \"camera devices\"),\n    LOG(1007, \"log\", \"log devices\"),\n    COMPASS(1008, \"compass\", \"compass device\"),\n    MOUNT(1009, \"mount\", \"mountd socket\"),\n    WIFI(1010, \"wifi\", \"wifi subsystem\"),\n    ADB(1011, \"adb\", \"android debug bridge (adbd)\"),\n    INSTALL(1012, \"install\", \"group for installing packages\"),\n    MEDIA(1013, \"media\", \"mediaserver process\"),\n    DHCP(1014, \"dhcp\", \"dhcp client\"),\n    SDCARD_RW(1015, \"sdcard_rw\", \"external storage write access\"),\n    VPN(1016, \"vpn\", \"vpn system\"),\n    KEYSTORE(1017, \"keystore\", \"keystore subsystem\"),\n    USB(1018, \"usb\", \"USB devices\"),\n    DRM(1019, \"drm\", \"DRM server\"),\n    MDNSR(1020, \"mdnsr\", \"MulticastDNSResponder (service discovery)\"),\n    GPS(1021, \"gps\", \"GPS daemon\"),\n    UNUSED1(1022, \"unused1\", \"deprecated, DO NOT USE\"),\n    MEDIA_RW(1023, \"media_rw\", \"internal media storage write access\"),\n    MTP(1024, \"mtp\", \"MTP USB driver access\"),\n    UNUSED2(1025, \"unused2\", \"deprecated, DO NOT USE\"),\n    DRMRPC(1026, \"drmrpc\", \"group for drm rpc\"),\n    NFC(1027, \"nfc\", \"nfc subsystem\"),\n    SDCARD_R(1028, \"sdcard_r\", \"external storage read access\"),\n    CLAT(1029, \"clat\", \"clat part of nat464\"),\n    LOOP_RADIO(1030, \"loop_radio\", \"loop radio devices\"),\n    MEDIA_DRM(1031, \"media_drm\", \"MediaDrm plugins\"),\n    PACKAGE_INFO(1032, \"package_info\", \"access to installed package details\"),\n    SDCARD_PICS(1033, \"sdcard_pics\", \"external storage photos access\"),\n    SDCARD_AV(1034, \"sdcard_av\", \"external storage audio/video access\"),\n    SDCARD_ALL(1035, \"sdcard_all\", \"access all users external storage\"),\n    LOGD(1036, \"logd\", \"log daemon\"),\n    SHARED_RELRO(1037, \"shared_relro\", \"creator of shared GNU RELRO files\"),\n    DBUS(1038, \"dbus\", \"dbus-daemon IPC broker process\"),\n    TLSDATE(1039, \"tlsdate\", \"tlsdate unprivileged user\"),\n    MEDIA_EX(1040, \"media_ex\", \"mediaextractor process\"),\n    AUDIOSERVER(1041, \"audioserver\", \"audioserver process\"),\n    METRICS_COLL(1042, \"metrics_coll\", \"metrics_collector process\"),\n    METRICSD(1043, \"metricsd\", \"metricsd process\"),\n    WEBSERV(1044, \"webserv\", \"webservd process\"),\n    DEBUGGERD(1045, \"debuggerd\", \"debuggerd unprivileged user\"),\n    MEDIA_CODEC(1046, \"media_codec\", \"media_codec process\"),\n    CAMERASERVER(1047, \"cameraserver\", \"cameraserver process\"),\n    FIREWALL(1048, \"firewall\", \"firewall process\"),\n    TRUNKS(1049, \"trunks\", \"trunksd process\"),\n    NVRAM(1050, \"nvram\", \"nvram daemon\"),\n    DNS(1051, \"dns\", \"DNS resolution daemon (system: netd)\"),\n    DNS_TETHER(1052, \"dns_tether\", \"DNS resolution daemon (tether: dnsmasq)\"),\n    WEBVIEW_ZYGOTE(1053, \"webview_zygote\", \"WebView zygote process\"),\n    VEHICLE_NETWORK(1054, \"vehicle_network\", \"Vehicle network service\"),\n    MEDIA_AUDIO(1055, \"media_audio\", \"GID for audio files on internal media storage\"),\n    MEDIA_VIDEO(1056, \"media_video\", \"GID for video files on internal media storage\"),\n    MEDIA_IMAGE(1057, \"media_image\", \"GID for image files on internal media storage\"),\n    TOMBSTONED(1058, \"tombstoned\", \"tombstoned user\"),\n    MEDIA_OBB(1059, \"media_obb\", \"GID for OBB files on internal media storage\"),\n    ESE(1060, \"ese\", \"embedded secure element (eSE) subsystem\"),\n    OTA_UPDATE(1061, \"ota_update\", \"resource tracking UID for OTA updates\"),\n    AUTOMOTIVE_EVS(1062, \"automotive_evs\", \"Automotive rear and surround view system\"),\n    LOWPAN(1063, \"lowpan\", \"LoWPAN subsystem\"),\n    HSM(1064, \"lowpan\", \"hardware security module subsystem\"),\n    RESERVED_DISK(1065, \"reserved_disk\", \"GID that has access to reserved disk space\"),\n    STATSD(1066, \"statsd\", \"statsd daemon\"),\n    INCIDENTD(1067, \"incidentd\", \"incidentd daemon\"),\n    SECURE_ELEMENT(1068, \"secure_element\", \"secure element subsystem\"),\n    LMKD(1069, \"lmkd\", \"low memory killer daemon\"),\n    LLKD(1070, \"llkd\", \"live lock daemon\"),\n    IORAPD(1071, \"iorapd\", \"input/output readahead and pin daemon\"),\n    GPU_SERVICE(1072, \"gpu_service\", \"GPU service daemon\"),\n    NETWORK_STACK(1073, \"network_stack\", \"network stack service\"),\n    GSID(1074, \"GSID\", \"GSI service daemon\"),\n    FSVERITY_CERT(1075, \"fsverity_cert\", \"fs-verity key ownership in keystore\"),\n    CREDSTORE(1076, \"credstore\", \"identity credential manager service\"),\n    EXTERNAL_STORAGE(1077, \"external_storage\", \"Full external storage access including USB OTG volumes\"),\n    EXT_DATA_RW(1078, \"ext_data_rw\", \"GID for app-private data directories on external storage\"),\n    EXT_OBB_RW(1079, \"ext_obb_rw\", \"GID for OBB directories on external storage\"),\n    CONTEXT_HUB(1080, \"context_hub\", \"GID for access to the Context Hub\"),\n    VIRTUALIZATIONSERVICE(1081, \"virtualizationservice\", \"VirtualizationService daemon\"),\n    ARTD(1082, \"artd\", \"ART Service daemon\"),\n    UWB(1083, \"uwb\", \"UWB subsystem\"),\n    THREAD_NETWORK(1084, \"thread_network\", \"Thread Network subsystem\"),\n    DICED(1085, \"diced\", \"Android's DICE daemon\"),\n    DMESGD(1086, \"dmesgd\", \"dmesg parsing daemon for kernel report collection\"),\n    JC_WEAVER(1087, \"jc_weaver\", \"Javacard Weaver HAL - to manage omapi ARA rules\"),\n    JC_STRONGBOX(1088, \"jc_strongbox\", \"Javacard Strongbox HAL - to manage omapi ARA rules\"),\n    JC_IDENTITYCRED(1089, \"jc_identitycred\", \"Javacard Identity Cred HAL - to manage omapi ARA rules\"),\n    SDK_SANDBOX(1090, \"sdk_sandbox\", \"SDK sandbox virtual UID\"),\n    SECURITY_LOG_WRITER(1091, \"security_log_writer\", \"write to security log\"),\n    PRNG_SEEDER(1092, \"prng_seeder\", \"PRNG seeder daemon\"),\n    UPROBESTATS(1093, \"uprobestats\", \"uid for uprobestats\"),\n    CROS_EC(1094, \"cros_ec\", \"uid for accessing ChromeOS EC (cros_ec)\"),\n    MMD(1095, \"mmd\", \"uid for memory management daemon\"),\n\n    SHELL(2000, \"shell\", \"adb and debug shell user\"),\n    CACHE(2001, \"cache\", \"cache access\"),\n    DIAG(2002, \"diag\", \"access to diagnostic resources\"),\n\n    /* The 3000 series are intended for use as supplemental group id's only.\n     * They indicate special Android capabilities that the kernel is aware of. */\n    NET_BT_ADMIN(3001, \"net_bt_admin\", \"bluetooth: create any socket\"),\n    NET_BT(3002, \"net_bt\", \"bluetooth: create sco, rfcomm or l2cap sockets\"),\n    INET(3003, \"inet\", \"can create AF_INET and AF_INET6 sockets\"),\n    NET_RAW(3004, \"net_raw\", \"can create raw INET sockets\"),\n    NET_ADMIN(3005, \"net_admin\", \"can configure interfaces and routing tables.\"),\n    NET_BW_STATS(3006, \"net_bw_stats\", \"read bandwidth statistics\"),\n    NET_BW_ACCT(3007, \"net_bw_acct\", \"change bandwidth statistics accounting\"),\n    NET_BT_STACK(3008, \"net_bt_stack\", \"access to various bluetooth management functions\"),\n    READPROC(3009, \"readproc\", \"Allow /proc read access\"),\n    WAKELOCK(3010, \"wakelock\", \"Allow system wakelock read/write access\"),\n    UHID(3011, \"uhid\", \"Allow read/write to /dev/uhid node\"),\n    READTRACEFS(3012, \"readtracefs\", \"Allow tracefs read\"),\n    VIRTUALMACHINE(3013, \"virtualmachine\", \"Allows VMs to tune for performance\"),\n\n    EVERYBODY(9997, \"everybody\", \"Shared external storage read/write\"),\n    MISC(9998, \"misc\", \"Access to misc storage\"),\n    NOBODY(9999, \"nobody\", \"Reserved\"),\n    APP(10000, \"app\", \"Access to app data\"),\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/KsuService.kt",
    "content": "package me.weishu.kernelsu.ui\n\nimport android.content.Intent\nimport android.content.pm.PackageInfo\nimport android.content.pm.PackageManager\nimport android.os.IBinder\nimport android.os.UserManager\nimport android.util.Log\nimport com.topjohnwu.superuser.ipc.RootService\nimport me.weishu.kernelsu.IKsuInterface\nimport rikka.parcelablelist.ParcelableListSlice\n\n/**\n * @author weishu\n * @date 2023/4/18.\n */\n\nclass KsuService : RootService() {\n\n    companion object {\n        private const val TAG = \"KsuService\"\n    }\n\n    override fun onBind(intent: Intent): IBinder {\n        return Stub()\n    }\n\n    private fun getAllUserIds(): IntArray {\n        val um = getSystemService(USER_SERVICE) as UserManager\n        // getUsers(boolean excludeDying) was added in API 17, but marked as deprecated\n        try {\n            val method = um.javaClass.getMethod(\"getUsers\", Boolean::class.javaPrimitiveType)\n            val users = method.invoke(um, true) as List<*>\n            return extractUserIds(users)\n        } catch (e: Exception) {\n            Log.w(TAG, \"getUsers reflection failed\", e)\n        }\n        // getAliveUsers() was added in API 31\n        try {\n            val method = um.javaClass.getMethod(\"getAliveUsers\")\n            val users = method.invoke(um) as List<*>\n            return extractUserIds(users)\n        } catch (e: Exception) {\n            Log.e(TAG, \"getAliveUsers reflection failed\", e)\n        }\n\n        return intArrayOf(0)\n    }\n\n    private fun extractUserIds(users: List<*>?): IntArray {\n        if (users.isNullOrEmpty()) return intArrayOf(0)\n\n        return try {\n            users.map { user ->\n                user!!.javaClass.getField(\"id\").getInt(user)\n            }.toIntArray()\n        } catch (e: Exception) {\n            Log.e(TAG, \"Error extracting ID from UserInfo\", e)\n            intArrayOf(0)\n        }\n    }\n\n    private fun getInstalledPackagesAll(flags: Int): ArrayList<PackageInfo> {\n        val packages = ArrayList<PackageInfo>()\n        for (userId in getAllUserIds()) {\n            Log.i(TAG, \"getInstalledPackagesAll: $userId\")\n            packages.addAll(getInstalledPackagesAsUser(flags, userId))\n        }\n        return packages\n    }\n\n    @Suppress(\"UNCHECKED_CAST\")\n    private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List<PackageInfo> {\n        return try {\n            val pm: PackageManager = packageManager\n            val method = pm.javaClass.getDeclaredMethod(\n                \"getInstalledPackagesAsUser\",\n                Int::class.javaPrimitiveType,\n                Int::class.javaPrimitiveType\n            )\n            method.invoke(pm, flags, userId) as List<PackageInfo>\n        } catch (e: Throwable) {\n            Log.e(TAG, \"err\", e)\n            ArrayList()\n        }\n    }\n\n    private inner class Stub : IKsuInterface.Stub() {\n        override fun getPackages(flags: Int): ParcelableListSlice<PackageInfo> {\n            val list = getInstalledPackagesAll(flags)\n            Log.i(TAG, \"getPackages: ${list.size}\")\n            return ParcelableListSlice(list)\n        }\n\n        override fun getUserIds(): IntArray {\n            val ids = getAllUserIds()\n            Log.i(TAG, \"getUserIds: ${ids.contentToString()}\")\n            return ids\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt",
    "content": "package me.weishu.kernelsu.ui\n\nimport android.annotation.SuppressLint\nimport android.content.Intent\nimport android.content.res.Configuration\nimport android.net.Uri\nimport android.os.Bundle\nimport android.widget.Toast\nimport androidx.activity.ComponentActivity\nimport androidx.activity.SystemBarStyle\nimport androidx.activity.compose.LocalActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.consumeWindowInsets\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.union\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.Density\nimport androidx.compose.ui.unit.Dp\nimport androidx.core.net.toUri\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator\nimport androidx.navigation3.runtime.entryProvider\nimport androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator\nimport androidx.navigation3.ui.NavDisplay\nimport androidx.navigationevent.NavigationEventInfo\nimport androidx.navigationevent.compose.NavigationBackHandler\nimport androidx.navigationevent.compose.rememberNavigationEventState\nimport com.kyant.backdrop.backdrops.layerBackdrop\nimport com.kyant.backdrop.backdrops.rememberLayerBackdrop\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.bottombar.BottomBar\nimport me.weishu.kernelsu.ui.component.bottombar.MainPagerState\nimport me.weishu.kernelsu.ui.component.bottombar.SideRail\nimport me.weishu.kernelsu.ui.component.bottombar.rememberMainPagerState\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.navigation3.HandleDeepLink\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.navigation3.Navigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.navigation3.rememberNavigator\nimport me.weishu.kernelsu.ui.screen.about.AboutScreen\nimport me.weishu.kernelsu.ui.screen.appprofile.AppProfileScreen\nimport me.weishu.kernelsu.ui.screen.colorpalette.ColorPaletteScreen\nimport me.weishu.kernelsu.ui.screen.executemoduleaction.ExecuteModuleActionScreen\nimport me.weishu.kernelsu.ui.screen.flash.FlashIt\nimport me.weishu.kernelsu.ui.screen.flash.FlashScreen\nimport me.weishu.kernelsu.ui.screen.home.HomePager\nimport me.weishu.kernelsu.ui.screen.install.InstallScreen\nimport me.weishu.kernelsu.ui.screen.module.ModulePager\nimport me.weishu.kernelsu.ui.screen.modulerepo.ModuleRepoDetailScreen\nimport me.weishu.kernelsu.ui.screen.modulerepo.ModuleRepoScreen\nimport me.weishu.kernelsu.ui.screen.settings.SettingPager\nimport me.weishu.kernelsu.ui.screen.superuser.SuperUserPager\nimport me.weishu.kernelsu.ui.screen.template.AppProfileTemplateScreen\nimport me.weishu.kernelsu.ui.screen.templateeditor.TemplateEditorScreen\nimport me.weishu.kernelsu.ui.theme.KernelSUTheme\nimport me.weishu.kernelsu.ui.theme.LocalColorMode\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.theme.LocalEnableFloatingBottomBar\nimport me.weishu.kernelsu.ui.theme.LocalEnableFloatingBottomBarBlur\nimport me.weishu.kernelsu.ui.util.LocalSnackbarHost\nimport me.weishu.kernelsu.ui.util.getFileName\nimport me.weishu.kernelsu.ui.util.install\nimport me.weishu.kernelsu.ui.util.rootAvailable\nimport me.weishu.kernelsu.ui.viewmodel.MainActivityViewModel\nimport me.weishu.kernelsu.ui.webui.WebUIActivity\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\n\nclass MainActivity : ComponentActivity() {\n\n    private val intentState = MutableStateFlow(0)\n\n    @SuppressLint(\"UnusedMaterial3ScaffoldPaddingParameter\")\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        val isManager = Natives.isManager\n        if (isManager && !Natives.requireNewKernel()) install()\n\n        setContent {\n            val viewModel = viewModel<MainActivityViewModel>()\n            val uiState by viewModel.uiState.collectAsState()\n            val appSettings = uiState.appSettings\n            val uiMode = uiState.uiMode\n            val darkMode = appSettings.colorMode.isDark || (appSettings.colorMode.isSystem && isSystemInDarkTheme())\n\n            DisposableEffect(darkMode) {\n                enableEdgeToEdge(\n                    statusBarStyle = SystemBarStyle.auto(\n                        android.graphics.Color.TRANSPARENT,\n                        android.graphics.Color.TRANSPARENT\n                    ) { darkMode },\n                    navigationBarStyle = SystemBarStyle.auto(\n                        android.graphics.Color.TRANSPARENT,\n                        android.graphics.Color.TRANSPARENT\n                    ) { darkMode },\n                )\n                window.isNavigationBarContrastEnforced = false\n                onDispose { }\n            }\n\n            val navigator = rememberNavigator(Route.Main)\n            val snackBarHostState = remember { SnackbarHostState() }\n            val systemDensity = LocalDensity.current\n            val density = remember(systemDensity, uiState.pageScale) {\n                Density(systemDensity.density * uiState.pageScale, systemDensity.fontScale)\n            }\n\n            CompositionLocalProvider(\n                LocalNavigator provides navigator,\n                LocalDensity provides density,\n                LocalColorMode provides appSettings.colorMode.value,\n                LocalEnableBlur provides uiState.enableBlur,\n                LocalEnableFloatingBottomBar provides uiState.enableFloatingBottomBar,\n                LocalEnableFloatingBottomBarBlur provides uiState.enableFloatingBottomBarBlur,\n                LocalUiMode provides uiMode,\n                LocalSnackbarHost provides snackBarHostState\n            ) {\n                KernelSUTheme(appSettings = appSettings, uiMode = uiMode) {\n                    HandleDeepLink(intentState = intentState.collectAsState())\n                    ZipFileIntentHandler(intentState = intentState, isManager = isManager)\n                    ShortcutIntentHandler(intentState = intentState)\n\n                    val navDisplay = @Composable {\n                        NavDisplay(\n                            backStack = navigator.backStack,\n                            entryDecorators = listOf(\n                                rememberSaveableStateHolderNavEntryDecorator(),\n                                rememberViewModelStoreNavEntryDecorator()\n                            ),\n                            onBack = {\n                                when (val top = navigator.current()) {\n                                    is Route.TemplateEditor -> {\n                                        if (!top.readOnly) {\n                                            navigator.setResult(\"template_edit\", true)\n                                        } else {\n                                            navigator.pop()\n                                        }\n                                    }\n\n                                    else -> navigator.pop()\n                                }\n                            },\n                            entryProvider = entryProvider {\n                                entry<Route.Main> { MainScreen() }\n                                entry<Route.About> { AboutScreen() }\n                                entry<Route.ColorPalette> { ColorPaletteScreen() }\n                                entry<Route.AppProfileTemplate> { AppProfileTemplateScreen() }\n                                entry<Route.TemplateEditor> { key -> TemplateEditorScreen(key.template, key.readOnly) }\n                                entry<Route.AppProfile> { key -> AppProfileScreen(key.uid) }\n                                entry<Route.ModuleRepo> { ModuleRepoScreen() }\n                                entry<Route.ModuleRepoDetail> { key -> ModuleRepoDetailScreen(key.module) }\n                                entry<Route.Install> { InstallScreen() }\n                                entry<Route.Flash> { key -> FlashScreen(key.flashIt) }\n                                entry<Route.ExecuteModuleAction> { key -> ExecuteModuleActionScreen(key.moduleId, key.fromShortcut) }\n                                entry<Route.Home> { MainScreen() }\n                                entry<Route.SuperUser> { MainScreen() }\n                                entry<Route.Module> { MainScreen() }\n                                entry<Route.Settings> { MainScreen() }\n                            }\n                        )\n                    }\n\n                    when (uiMode) {\n                        UiMode.Material -> androidx.compose.material3.Scaffold { navDisplay() }\n                        UiMode.Miuix -> Scaffold { navDisplay() }\n                    }\n                }\n            }\n        }\n    }\n\n    override fun onNewIntent(intent: Intent) {\n        super.onNewIntent(intent)\n        setIntent(intent)\n        // Increment intentState to trigger LaunchedEffect re-execution\n        intentState.value += 1\n    }\n}\n\nval LocalMainPagerState = staticCompositionLocalOf<MainPagerState> { error(\"LocalMainPagerState not provided\") }\n\n@SuppressLint(\"UnusedMaterial3ScaffoldPaddingParameter\")\n@Composable\nfun MainScreen() {\n    val navController = LocalNavigator.current\n    val enableBlur = LocalEnableBlur.current\n    val enableFloatingBottomBar = LocalEnableFloatingBottomBar.current\n    val enableFloatingBottomBarBlur = LocalEnableFloatingBottomBarBlur.current\n    val pagerState = rememberPagerState(pageCount = { 4 })\n    val mainPagerState = rememberMainPagerState(pagerState)\n    val isManager = Natives.isManager\n    val isFullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()\n    var userScrollEnabled by remember(isFullFeatured) { mutableStateOf(isFullFeatured) }\n    val uiMode = LocalUiMode.current\n    val surfaceColor = when (uiMode) {\n        UiMode.Material -> MaterialTheme.colorScheme.surface // Haze is not used in Material, this is just a placeholder\n        UiMode.Miuix -> MiuixTheme.colorScheme.surface\n    }\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = surfaceColor,\n            tint = HazeTint(surfaceColor.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n\n    val backdrop = rememberLayerBackdrop {\n        drawRect(surfaceColor)\n        drawContent()\n    }\n\n    LaunchedEffect(mainPagerState.pagerState.currentPage) {\n        mainPagerState.syncPage()\n    }\n\n    MainScreenBackHandler(mainPagerState, navController)\n\n    val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE\n    val useNavigationRail = isLandscape && !(uiMode == UiMode.Miuix && enableFloatingBottomBar)\n\n    CompositionLocalProvider(\n        LocalMainPagerState provides mainPagerState\n    ) {\n        val pagerContent = @Composable { bottomInnerPadding: Dp ->\n            HorizontalPager(\n                modifier = Modifier\n                    .then(if (enableBlur) Modifier.hazeSource(state = hazeState) else Modifier)\n                    .then(if (enableFloatingBottomBar && enableFloatingBottomBarBlur) Modifier.layerBackdrop(backdrop) else Modifier),\n                state = mainPagerState.pagerState,\n                beyondViewportPageCount = 3,\n                userScrollEnabled = userScrollEnabled,\n            ) {\n                when (it) {\n                    0 -> HomePager(navController, bottomInnerPadding)\n                    1 -> SuperUserPager(navController, bottomInnerPadding)\n                    2 -> ModulePager(bottomInnerPadding)\n                    3 -> SettingPager(navController, bottomInnerPadding)\n                }\n            }\n        }\n\n        if (useNavigationRail) {\n            val startInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout)\n                .only(WindowInsetsSides.Start)\n            val navBarBottomPadding = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()\n\n            when (uiMode) {\n                UiMode.Material -> androidx.compose.material3.Scaffold {\n                    Row {\n                        SideRail(\n                            hazeState = hazeState,\n                            hazeStyle = hazeStyle,\n                        )\n                        Box(\n                            modifier = Modifier\n                                .weight(1f)\n                                .consumeWindowInsets(startInsets)\n                        ) {\n                            pagerContent(navBarBottomPadding)\n                        }\n                    }\n                }\n\n                UiMode.Miuix -> Scaffold { _ ->\n                    Row {\n                        SideRail(\n                            hazeState = hazeState,\n                            hazeStyle = hazeStyle,\n                        )\n                        Box(\n                            modifier = Modifier\n                                .weight(1f)\n                                .consumeWindowInsets(startInsets)\n                        ) {\n                            pagerContent(navBarBottomPadding)\n                        }\n                    }\n                }\n            }\n        } else {\n            val bottomBar = @Composable {\n                Box(\n                    modifier = Modifier.fillMaxWidth()\n                ) {\n                    BottomBar(\n                        hazeState = hazeState,\n                        hazeStyle = hazeStyle,\n                        backdrop = backdrop,\n                        modifier = Modifier.align(Alignment.BottomCenter),\n                    )\n                }\n            }\n\n            when (uiMode) {\n                UiMode.Material -> androidx.compose.material3.Scaffold(bottomBar = bottomBar) { innerPadding ->\n                    pagerContent(innerPadding.calculateBottomPadding())\n                }\n\n                UiMode.Miuix -> Scaffold(bottomBar = bottomBar) { innerPadding ->\n                    pagerContent(innerPadding.calculateBottomPadding())\n                }\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun MainScreenBackHandler(\n    mainState: MainPagerState,\n    navController: Navigator,\n) {\n    val isPagerBackHandlerEnabled by remember {\n        derivedStateOf {\n            navController.current() is Route.Main && navController.backStackSize() == 1 && mainState.selectedPage != 0\n        }\n    }\n\n    val navEventState = rememberNavigationEventState(NavigationEventInfo.None)\n\n    NavigationBackHandler(\n        state = navEventState,\n        isBackEnabled = isPagerBackHandlerEnabled,\n        onBackCompleted = {\n            mainState.animateToPage(0)\n        }\n    )\n}\n\n/**\n * Handles ZIP file installation from external apps (e.g., file managers).\n * - In normal mode: Shows a confirmation dialog before installation\n * - In safe mode: Shows a Toast notification and prevents installation\n */\n@SuppressLint(\"StringFormatInvalid\", \"LocalContextGetResourceValueCall\")\n@Composable\nprivate fun ZipFileIntentHandler(\n    intentState: MutableStateFlow<Int>,\n    isManager: Boolean,\n) {\n    val activity = LocalActivity.current ?: return\n    val context = LocalContext.current\n    var zipUri by remember { mutableStateOf<Uri?>(null) }\n    val isSafeMode = Natives.isSafeMode\n    val clearZipUri = { zipUri = null }\n    val navigator = LocalNavigator.current\n\n    val installDialog = rememberConfirmDialog(\n        onConfirm = {\n            zipUri?.let { uri -> navigator.push(Route.Flash(FlashIt.FlashModules(listOf(uri)))) }\n            clearZipUri()\n        },\n        onDismiss = clearZipUri\n    )\n\n    fun getDisplayName(uri: Uri): String {\n        return uri.getFileName(context) ?: uri.lastPathSegment ?: \"Unknown\"\n    }\n\n    val intentStateValue by intentState.collectAsState()\n    LaunchedEffect(intentStateValue) {\n        val currentIntent = activity.intent\n        val uri = currentIntent?.data ?: return@LaunchedEffect\n\n        if (!isManager || uri.scheme != \"content\" || currentIntent.type != \"application/zip\") {\n            return@LaunchedEffect\n        }\n\n        activity.intent.data = null\n        activity.intent.type = null\n\n        if (isSafeMode) {\n            Toast.makeText(context, context.getString(R.string.safe_mode_module_disabled), Toast.LENGTH_SHORT).show()\n        } else {\n            zipUri = uri\n            installDialog.showConfirm(\n                title = context.getString(R.string.module),\n                content = context.getString(\n                    R.string.module_install_prompt_with_name,\n                    \"\\n${getDisplayName(uri)}\"\n                )\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun ShortcutIntentHandler(\n    intentState: MutableStateFlow<Int>,\n) {\n    val activity = LocalActivity.current ?: return\n    val context = LocalContext.current\n    val intentStateValue by intentState.collectAsState()\n    val navigator = LocalNavigator.current\n    LaunchedEffect(intentStateValue) {\n        val intent = activity.intent\n        val type = intent?.getStringExtra(\"shortcut_type\") ?: return@LaunchedEffect\n\n        when (type) {\n            \"module_action\" -> {\n                val moduleId = intent.getStringExtra(\"module_id\") ?: return@LaunchedEffect\n                navigator.push(Route.ExecuteModuleAction(moduleId, fromShortcut = true))\n                intent.removeExtra(\"shortcut_type\")\n                intent.removeExtra(\"module_id\")\n            }\n\n            \"module_webui\" -> {\n                val moduleId = intent.getStringExtra(\"module_id\") ?: return@LaunchedEffect\n                val webIntent = Intent(context, WebUIActivity::class.java)\n                    .setData(\"kernelsu://webui/$moduleId\".toUri())\n                context.startActivity(webIntent)\n            }\n\n            else -> return@LaunchedEffect\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/UiMode.kt",
    "content": "package me.weishu.kernelsu.ui\n\nimport androidx.compose.runtime.staticCompositionLocalOf\n\nenum class UiMode(val value: String) {\n    Miuix(\"miuix\"),\n    Material(\"material\");\n\n    companion object {\n        fun fromValue(value: String): UiMode = when (value) {\n            Material.value -> Material\n            else -> Miuix\n        }\n\n        val DEFAULT_VALUE = Miuix.value\n    }\n}\n\nval LocalUiMode = staticCompositionLocalOf { UiMode.Miuix }\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/animation/DampedDragAnimation.kt",
    "content": "package me.weishu.kernelsu.ui.animation\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.spring\nimport androidx.compose.foundation.MutatorMutex\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.input.pointer.util.VelocityTracker\nimport androidx.compose.ui.unit.IntSize\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.android.awaitFrame\nimport kotlinx.coroutines.flow.filter\nimport kotlinx.coroutines.flow.first\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.ui.modifier.inspectDragGestures\nimport kotlin.math.abs\n\nclass DampedDragAnimation(\n    private val animationScope: CoroutineScope,\n    val initialValue: Float,\n    val valueRange: ClosedRange<Float>,\n    val visibilityThreshold: Float,\n    val initialScale: Float,\n    val pressedScale: Float,\n    val canDrag: (Offset) -> Boolean = { true },\n    val onDragStarted: DampedDragAnimation.(position: Offset) -> Unit,\n    val onDragStopped: DampedDragAnimation.() -> Unit,\n    val onDrag: DampedDragAnimation.(size: IntSize, dragAmount: Offset) -> Unit,\n) {\n\n    private val valueAnimationSpec =\n        spring(1f, 1000f, visibilityThreshold)\n    private val velocityAnimationSpec =\n        spring(0.5f, 300f, visibilityThreshold * 10f)\n    private val pressProgressAnimationSpec =\n        spring(1f, 1000f, 0.001f)\n    private val scaleXAnimationSpec =\n        spring(0.6f, 250f, 0.001f)\n    private val scaleYAnimationSpec =\n        spring(0.7f, 250f, 0.001f)\n\n    private val valueAnimation =\n        Animatable(initialValue, visibilityThreshold)\n    private val velocityAnimation =\n        Animatable(0f, 5f)\n    private val pressProgressAnimation =\n        Animatable(0f, 0.001f)\n    private val scaleXAnimation =\n        Animatable(initialScale, 0.001f)\n    private val scaleYAnimation =\n        Animatable(initialScale, 0.001f)\n\n    private val mutatorMutex = MutatorMutex()\n\n    private val velocityTracker = VelocityTracker()\n\n    val value: Float get() = valueAnimation.value\n    val targetValue: Float get() = valueAnimation.targetValue\n    val pressProgress: Float get() = pressProgressAnimation.value\n    val scaleX: Float get() = scaleXAnimation.value\n    val scaleY: Float get() = scaleYAnimation.value\n    val velocity: Float get() = velocityAnimation.value\n\n    val modifier: Modifier = Modifier.pointerInput(Unit) {\n        inspectDragGestures(\n            onDragStart = { down ->\n                onDragStarted(down.position)\n                press()\n            },\n            onDragEnd = {\n                onDragStopped()\n                release()\n            },\n            onDragCancel = {\n                onDragStopped()\n                release()\n            }\n        ) { change, dragAmount ->\n            val position = change.position\n            val previousPosition = change.previousPosition\n\n            val isInside = canDrag(position)\n            val wasInside = canDrag(previousPosition)\n\n            if (isInside && wasInside) {\n                onDrag(size, dragAmount)\n            }\n        }\n    }\n\n    fun press() {\n        velocityTracker.resetTracking()\n        animationScope.launch {\n            launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) }\n            launch { scaleXAnimation.animateTo(pressedScale, scaleXAnimationSpec) }\n            launch { scaleYAnimation.animateTo(pressedScale, scaleYAnimationSpec) }\n        }\n    }\n\n    fun release() {\n        animationScope.launch {\n            awaitFrame()\n            if (value != targetValue) {\n                val threshold = (valueRange.endInclusive - valueRange.start) * 0.025f\n                snapshotFlow { valueAnimation.value }\n                    .filter { abs(it - valueAnimation.targetValue) < threshold }\n                    .first()\n            }\n            launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }\n            launch { scaleXAnimation.animateTo(initialScale, scaleXAnimationSpec) }\n            launch { scaleYAnimation.animateTo(initialScale, scaleYAnimationSpec) }\n        }\n    }\n\n    fun updateValue(value: Float) {\n        val targetValue = value.coerceIn(valueRange)\n        animationScope.launch {\n            launch { valueAnimation.animateTo(targetValue, valueAnimationSpec) { updateVelocity() } }\n        }\n    }\n\n    fun animateToValue(value: Float) {\n        animationScope.launch {\n            mutatorMutex.mutate {\n                press()\n                val targetValue = value.coerceIn(valueRange)\n                launch { valueAnimation.animateTo(targetValue, valueAnimationSpec) }\n                if (velocity != 0f) {\n                    launch { velocityAnimation.animateTo(0f, velocityAnimationSpec) }\n                }\n                release()\n            }\n        }\n    }\n\n    private fun updateVelocity() {\n        velocityTracker.addPosition(\n            System.currentTimeMillis(),\n            Offset(value, 0f)\n        )\n        val targetVelocity = velocityTracker.calculateVelocity().x / (valueRange.endInclusive - valueRange.start)\n        animationScope.launch { velocityAnimation.animateTo(targetVelocity, velocityAnimationSpec) }\n    }\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/animation/InteractiveHighlight.kt",
    "content": "package me.weishu.kernelsu.ui.animation\n\nimport android.annotation.SuppressLint\nimport android.graphics.RuntimeShader\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.VectorConverter\nimport androidx.compose.animation.core.VisibilityThreshold\nimport androidx.compose.animation.core.spring\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.drawWithContent\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.geometry.Size\nimport androidx.compose.ui.graphics.BlendMode\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ShaderBrush\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.util.fastCoerceIn\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.ui.modifier.inspectDragGestures\nimport org.intellij.lang.annotations.Language\n\n@SuppressLint(\"NewApi\")\nclass InteractiveHighlight(\n    val animationScope: CoroutineScope,\n    val position: (size: Size, offset: Offset) -> Offset = { _, offset -> offset }\n) {\n\n    private val pressProgressAnimationSpec =\n        spring(0.5f, 300f, 0.001f)\n    private val positionAnimationSpec =\n        spring(0.5f, 300f, Offset.VisibilityThreshold)\n\n    private val pressProgressAnimation =\n        Animatable(0f, 0.001f)\n    private val positionAnimation =\n        Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold)\n\n    private var startPosition = Offset.Zero\n    val offset: Offset get() = positionAnimation.value - startPosition\n\n    @Language(\"AGSL\")\n    private val shader =\n        RuntimeShader(\n            \"\"\"\n    uniform float2 size;\n    layout(color) uniform half4 color;\n    uniform float radius;\n    uniform float2 position;\n    \n    half4 main(float2 coord) {\n        float dist = distance(coord, position);\n        float intensity = smoothstep(radius, radius * 0.5, dist);\n        return color * intensity;\n    }\"\"\"\n        )\n\n    val modifier: Modifier =\n        Modifier.drawWithContent {\n            val progress = pressProgressAnimation.value\n            if (progress > 0f) {\n                drawRect(\n                    Color.White.copy(0.06f * progress),\n                    blendMode = BlendMode.Plus\n                )\n                shader.apply {\n                    val position = position(size, positionAnimation.value)\n                    setFloatUniform(\"size\", size.width, size.height)\n                    setColorUniform(\"color\", Color.White.copy(0.12f * progress).toArgb())\n                    setFloatUniform(\"radius\", size.minDimension * 1.2f)\n                    setFloatUniform(\n                        \"position\",\n                        position.x.fastCoerceIn(0f, size.width),\n                        position.y.fastCoerceIn(0f, size.height)\n                    )\n                }\n                drawRect(\n                    ShaderBrush(shader),\n                    blendMode = BlendMode.Plus\n                )\n            }\n\n            drawContent()\n        }\n\n    val gestureModifier: Modifier =\n        Modifier.pointerInput(animationScope) {\n            inspectDragGestures(\n                onDragStart = { down ->\n                    startPosition = down.position\n                    animationScope.launch {\n                        launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) }\n                        launch { positionAnimation.snapTo(startPosition) }\n                    }\n                },\n                onDragEnd = {\n                    animationScope.launch {\n                        launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }\n                        launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }\n                    }\n                },\n                onDragCancel = {\n                    animationScope.launch {\n                        launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) }\n                        launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) }\n                    }\n                }\n            ) { change, _ ->\n                animationScope.launch { positionAnimation.snapTo(change.position) }\n            }\n        }\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/AppIconImage.kt",
    "content": "package me.weishu.kernelsu.ui.component\n\nimport android.content.pm.ApplicationInfo\nimport android.content.pm.PackageInfo\nimport androidx.compose.animation.Crossfade\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asImageBitmap\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.unit.dp\nimport com.kyant.capsule.ContinuousRoundedRectangle\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.ui.util.AppIconCache\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\n\n@Composable\nfun AppIconImage(\n    modifier: Modifier = Modifier,\n    applicationInfo: ApplicationInfo,\n    label: String? = null\n) {\n    val density = LocalDensity.current\n    val context = LocalContext.current\n    val targetSizePx = with(density) { 48.dp.roundToPx() }\n\n    Box(modifier = modifier) {\n        var appIcon by remember { mutableStateOf<ImageBitmap?>(null) }\n\n        LaunchedEffect(applicationInfo) {\n            val loadedIcon = AppIconCache.loadIcon(context, applicationInfo, targetSizePx)\n            appIcon = loadedIcon.asImageBitmap()\n        }\n\n        val appLabel by produceState(initialValue = label, key1 = applicationInfo) {\n            if (label != null) {\n                value = label\n            } else {\n                withContext(Dispatchers.IO) {\n                    val pm = context.packageManager\n                    val appLabel = pm.getApplicationLabel(applicationInfo).toString()\n                    value = appLabel\n                }\n            }\n        }\n\n        Crossfade(\n            targetState = appIcon, animationSpec = tween(durationMillis = 150), label = \"IconFade\"\n        ) { icon ->\n            if (icon == null) {\n                PlaceHolderBox(Modifier.fillMaxSize())\n            } else {\n                Image(\n                    bitmap = icon,\n                    contentDescription = appLabel,\n                    modifier = Modifier.fillMaxSize()\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun AppIconImage(modifier: Modifier = Modifier, packageInfo: PackageInfo, label: String? = null) {\n    val appInfo = packageInfo.applicationInfo\n    if (appInfo == null) {\n        PlaceHolderBox(modifier)\n        return\n    }\n    AppIconImage(modifier, appInfo, label)\n}\n\n@Composable\nprivate fun PlaceHolderBox(modifier: Modifier = Modifier) {\n    Box(\n        modifier = modifier\n            .clip(ContinuousRoundedRectangle(12.dp))\n            .background(colorScheme.secondaryContainer)\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/FloatingBottomBar.kt",
    "content": "package me.weishu.kernelsu.ui.component\n\nimport androidx.compose.animation.core.Animatable\nimport androidx.compose.animation.core.EaseOut\nimport androidx.compose.animation.core.spring\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.ColorFilter\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.clearAndSetSemantics\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.util.fastCoerceIn\nimport androidx.compose.ui.util.fastRoundToInt\nimport androidx.compose.ui.util.lerp\nimport com.kyant.backdrop.Backdrop\nimport com.kyant.backdrop.backdrops.layerBackdrop\nimport com.kyant.backdrop.backdrops.rememberCombinedBackdrop\nimport com.kyant.backdrop.backdrops.rememberLayerBackdrop\nimport com.kyant.backdrop.drawBackdrop\nimport com.kyant.backdrop.effects.blur\nimport com.kyant.backdrop.effects.lens\nimport com.kyant.backdrop.effects.vibrancy\nimport com.kyant.backdrop.highlight.Highlight\nimport com.kyant.backdrop.shadow.InnerShadow\nimport com.kyant.backdrop.shadow.Shadow\nimport com.kyant.capsule.ContinuousCapsule\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.flow.drop\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.ui.animation.DampedDragAnimation\nimport me.weishu.kernelsu.ui.animation.InteractiveHighlight\nimport me.weishu.kernelsu.ui.theme.isInDarkTheme\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\nimport kotlin.math.abs\nimport kotlin.math.sign\n\nval LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } }\n\n@Composable\nfun RowScope.FloatingBottomBarItem(\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    content: @Composable ColumnScope.() -> Unit\n) {\n    val scale = LocalFloatingBottomBarTabScale.current\n    Column(\n        modifier\n            .clip(ContinuousCapsule)\n            .clickable(\n                interactionSource = null,\n                indication = null,\n                role = Role.Tab,\n                onClick = onClick\n            )\n            .fillMaxHeight()\n            .weight(1f)\n            .graphicsLayer {\n                val scale = scale()\n                scaleX = scale\n                scaleY = scale\n            },\n        verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically),\n        horizontalAlignment = Alignment.CenterHorizontally,\n        content = content\n    )\n}\n\n@Composable\nfun FloatingBottomBar(\n    modifier: Modifier = Modifier,\n    selectedIndex: () -> Int,\n    onSelected: (index: Int) -> Unit,\n    backdrop: Backdrop,\n    tabsCount: Int,\n    isBlurEnabled: Boolean = true,\n    content: @Composable RowScope.() -> Unit\n) {\n    val isInLightTheme = !isInDarkTheme()\n    val accentColor = MiuixTheme.colorScheme.primary\n    val containerColor = if (isBlurEnabled) {\n        MiuixTheme.colorScheme.surfaceContainer.copy(0.4f)\n    } else {\n        MiuixTheme.colorScheme.surfaceContainer\n    }\n\n    val tabsBackdrop = rememberLayerBackdrop()\n    val density = LocalDensity.current\n    val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr\n    val animationScope = rememberCoroutineScope()\n\n    var tabWidthPx by remember { mutableFloatStateOf(0f) }\n    var totalWidthPx by remember { mutableFloatStateOf(0f) }\n\n    val offsetAnimation = remember { Animatable(0f) }\n    val panelOffset by remember(density) {\n        derivedStateOf {\n            if (totalWidthPx == 0f) 0f else {\n                val fraction = (offsetAnimation.value / totalWidthPx).fastCoerceIn(-1f, 1f)\n                with(density) {\n                    4f.dp.toPx() * fraction.sign * EaseOut.transform(abs(fraction))\n                }\n            }\n        }\n    }\n\n    var currentIndex by remember(selectedIndex) { mutableIntStateOf(selectedIndex()) }\n\n    class DampedDragAnimationHolder {\n        var instance: DampedDragAnimation? = null\n    }\n\n    val holder = remember { DampedDragAnimationHolder() }\n\n    val dampedDragAnimation = remember(animationScope, tabsCount, density, isLtr) {\n        DampedDragAnimation(\n            animationScope = animationScope,\n            initialValue = selectedIndex().toFloat(),\n            valueRange = 0f..(tabsCount - 1).toFloat(),\n            visibilityThreshold = 0.001f,\n            initialScale = 1f,\n            pressedScale = 78f / 56f,\n            canDrag = { offset ->\n                val anim = holder.instance ?: return@DampedDragAnimation true\n                if (tabWidthPx == 0f) return@DampedDragAnimation false\n\n                val currentValue = anim.value\n                val indicatorX = currentValue * tabWidthPx\n                val padding = with(density) { 4.dp.toPx() }\n                val globalTouchX = if (isLtr) {\n                    val touchX = indicatorX + offset.x\n                    padding + touchX\n                } else {\n                    val touchX = totalWidthPx - padding - tabWidthPx - indicatorX + offset.x\n                    touchX\n                }\n                globalTouchX in 0f..totalWidthPx\n            },\n            onDragStarted = {},\n            onDragStopped = {\n                val targetIndex = targetValue.fastRoundToInt().fastCoerceIn(0, tabsCount - 1)\n                currentIndex = targetIndex\n                animateToValue(targetIndex.toFloat())\n                animationScope.launch {\n                    offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f))\n                }\n            },\n            onDrag = { _, dragAmount ->\n                if (tabWidthPx > 0) {\n                    updateValue(\n                        (targetValue + dragAmount.x / tabWidthPx * if (isLtr) 1f else -1f)\n                            .fastCoerceIn(0f, (tabsCount - 1).toFloat())\n                    )\n                    animationScope.launch {\n                        offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x)\n                    }\n                }\n            }\n        ).also { holder.instance = it }\n    }\n\n    LaunchedEffect(selectedIndex) {\n        snapshotFlow { selectedIndex() }.collectLatest { currentIndex = it }\n    }\n    LaunchedEffect(dampedDragAnimation) {\n        snapshotFlow { currentIndex }.drop(1).collectLatest { index ->\n            dampedDragAnimation.animateToValue(index.toFloat())\n            onSelected(index)\n        }\n    }\n\n    val interactiveHighlight = remember(animationScope, tabWidthPx) {\n        InteractiveHighlight(\n            animationScope = animationScope,\n            position = { size, _ ->\n                Offset(\n                    if (isLtr) (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset\n                    else size.width - (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset,\n                    size.height / 2f\n                )\n            }\n        )\n    }\n\n    Box(\n        modifier = modifier.width(IntrinsicSize.Min),\n        contentAlignment = Alignment.CenterStart\n    ) {\n        Row(\n            Modifier\n                .onGloballyPositioned { coords ->\n                    totalWidthPx = coords.size.width.toFloat()\n                    val contentWidthPx = totalWidthPx - with(density) { 8.dp.toPx() }\n                    tabWidthPx = contentWidthPx / tabsCount\n                }\n                .graphicsLayer { translationX = panelOffset }\n                .clickable(\n                    interactionSource = remember { MutableInteractionSource() },\n                    indication = null,\n                    onClick = {}\n                )\n                .drawBackdrop(\n                    backdrop = backdrop,\n                    shape = { ContinuousCapsule },\n                    effects = {\n                        if (isBlurEnabled) {\n                            vibrancy()\n                            blur(8f.dp.toPx())\n                            lens(24f.dp.toPx(), 24f.dp.toPx())\n                        }\n                    },\n                    highlight = {\n                        Highlight.Default.copy(alpha = if (isBlurEnabled) 1f else 0f)\n                    },\n                    shadow = {\n                        Shadow.Default.copy(\n                            color = Color.Black.copy(if (isInLightTheme) 0.1f else 0.2f),\n                        )\n                    },\n                    layerBlock = {\n                        if (isBlurEnabled) {\n                            val progress = dampedDragAnimation.pressProgress\n                            val scale = lerp(1f, 1f + 16f.dp.toPx() / size.width, progress)\n                            scaleX = scale\n                            scaleY = scale\n                        }\n                    },\n                    onDrawSurface = { drawRect(containerColor) }\n                )\n                .then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier)\n                .height(64.dp)\n                .padding(4.dp),\n            verticalAlignment = Alignment.CenterVertically,\n            content = content\n        )\n\n        CompositionLocalProvider(\n            LocalFloatingBottomBarTabScale provides {\n                if (isBlurEnabled) lerp(1f, 1.2f, dampedDragAnimation.pressProgress)\n                else 1f\n            }\n        ) {\n            Row(\n                Modifier\n                    .clearAndSetSemantics {}\n                    .alpha(0f)\n                    .layerBackdrop(tabsBackdrop)\n                    .graphicsLayer { translationX = panelOffset }\n                    .drawBackdrop(\n                        backdrop = backdrop,\n                        shape = { ContinuousCapsule },\n                        effects = {\n                            if (isBlurEnabled) {\n                                val progress = dampedDragAnimation.pressProgress\n                                vibrancy()\n                                blur(8f.dp.toPx())\n                                lens(24f.dp.toPx() * progress, 24f.dp.toPx() * progress)\n                            }\n                        },\n                        highlight = {\n                            Highlight.Default.copy(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f)\n                        },\n                        onDrawSurface = { drawRect(containerColor) }\n                    )\n                    .then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier)\n                    .height(56.dp)\n                    .padding(horizontal = 4.dp)\n                    .graphicsLayer(colorFilter = ColorFilter.tint(accentColor)),\n                verticalAlignment = Alignment.CenterVertically,\n                content = content\n            )\n        }\n\n        if (tabWidthPx > 0f) {\n            Box(\n                Modifier\n                    .padding(horizontal = 4.dp)\n                    .graphicsLayer {\n                        val contentWidth = totalWidthPx - with(density) { 8.dp.toPx() }\n                        val singleTabWidth = contentWidth / tabsCount\n\n                        val progressOffset = dampedDragAnimation.value * singleTabWidth\n\n                        translationX = if (isLtr) {\n                            progressOffset + panelOffset\n                        } else {\n                            -progressOffset + panelOffset\n                        }\n                    }\n                    .then(if (isBlurEnabled) interactiveHighlight.gestureModifier else Modifier)\n                    .then(dampedDragAnimation.modifier)\n                    .drawBackdrop(\n                        backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop),\n                        shape = { ContinuousCapsule },\n                        effects = {\n                            if (isBlurEnabled) {\n                                val progress = dampedDragAnimation.pressProgress\n                                lens(10f.dp.toPx() * progress, 14f.dp.toPx() * progress, true)\n                            }\n                        },\n                        highlight = {\n                            Highlight.Default.copy(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f)\n                        },\n                        shadow = { Shadow(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f) },\n                        innerShadow = {\n                            InnerShadow(\n                                radius = 8f.dp * dampedDragAnimation.pressProgress,\n                                alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f\n                            )\n                        },\n                        layerBlock = {\n                            if (isBlurEnabled) {\n                                scaleX = dampedDragAnimation.scaleX\n                                scaleY = dampedDragAnimation.scaleY\n                                val velocity = dampedDragAnimation.velocity / 10f\n                                scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f)\n                                scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f)\n                            }\n                        },\n                        onDrawSurface = {\n                            val progress = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f\n                            drawRect(\n                                color = if (isInLightTheme) {\n                                    Color.Black.copy(0.1f)\n                                } else {\n                                    Color.White.copy(0.1f)\n                                },\n                                alpha = 1f - progress\n                            )\n                            drawRect(\n                                Color.Black.copy(alpha = 0.03f * progress)\n                            )\n                        }\n                    )\n                    .height(56.dp)\n                    .width(with(density) { ((totalWidthPx - 8.dp.toPx()) / tabsCount).toDp() })\n            )\n        }\n    }\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/GithubMarkdown.kt",
    "content": "package me.weishu.kernelsu.ui.component\n\nimport android.annotation.SuppressLint\nimport android.content.ActivityNotFoundException\nimport android.content.Intent\nimport android.graphics.Color\nimport android.util.Log\nimport android.view.MotionEvent\nimport android.view.View\nimport android.webkit.JavascriptInterface\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebResourceResponse\nimport android.webkit.WebSettings\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport android.widget.FrameLayout\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.surfaceColorAtElevation\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.platform.LocalResources\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.webkit.WebViewAssetLoader\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.theme.isInDarkTheme\nimport me.weishu.kernelsu.ui.util.adjustLightnessArgb\nimport me.weishu.kernelsu.ui.util.cssColorFromArgb\nimport me.weishu.kernelsu.ui.util.ensureVisibleByMix\nimport me.weishu.kernelsu.ui.util.relativeLuminance\nimport okhttp3.Headers.Companion.toHeaders\nimport okhttp3.OkHttpClient\nimport okhttp3.Request\nimport okhttp3.Response\nimport okio.IOException\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\nimport java.io.ByteArrayInputStream\nimport java.nio.charset.StandardCharsets\nimport kotlin.math.abs\n\nprivate val SEMICOLON_SPLIT = \";\\\\s*\".toRegex()\nprivate val EQUALS_SPLIT = \"=\\\\s*\".toRegex()\n\n@SuppressLint(\"JavascriptInterface\", \"SetJavaScriptEnabled\", \"ClickableViewAccessibility\")\n@Composable\nfun GithubMarkdown(\n    content: String,\n    onLoadingChange: (Boolean) -> Unit = {},\n    containerColor: androidx.compose.ui.graphics.Color? = null,\n) {\n    val density = LocalDensity.current\n    val systemDensity = LocalResources.current.displayMetrics.density\n    val fontScale = density.fontScale\n    val pageScale = density.density / systemDensity\n    val newTextZoom = (90 * pageScale * fontScale).toInt()\n\n    val scrollInterface = remember { MarkdownScrollInterface() }\n    val isDark = isInDarkTheme()\n    val dir = if (LocalLayoutDirection.current == LayoutDirection.Rtl) \"rtl\" else \"ltr\"\n\n    val colors = getMarkdownColors(containerColor)\n    val bgDefault = colors.bgDefault\n    val bgMuted = colors.bgMuted\n    val bgNeutralMuted = colors.bgNeutralMuted\n    val bgAttentionMuted = colors.bgAttentionMuted\n    val fgLink = colors.fgLink\n\n    val cssHref = \"https://appassets.androidplatform.net/assets/github-markdown.css\"\n    val html = \"\"\"\n        <!DOCTYPE html>\n        <html>\n        <head>\n          <meta charset='utf-8'/>\n          <meta name='viewport' content='width=device-width, initial-scale=1'/>\n          <link rel=\"stylesheet\" href=\"$cssHref\" />\n          <style>\n            html, body { margin:0; padding:0; }\n            img, video { max-width:100%; height:auto; }\n            .markdown-body {\n              padding: 16px;\n              --bgColor-default: $bgDefault;\n              --bgColor-muted: $bgMuted;\n              --bgColor-neutral-muted: $bgNeutralMuted;\n              --bgColor-attention-muted: $bgAttentionMuted;\n              --fgColor-accent: $fgLink;\n            }\n          </style>\n        </head>\n        <body dir='${dir}'>\n          <article class='markdown-body' data-theme='${if (isDark) \"dark\" else \"light\"}'>${content}</article>\n        </body>\n        </html>\n    \"\"\".trimIndent()\n\n    AndroidView(\n        factory = { context ->\n            val frameLayout = FrameLayout(context)\n            val webView = WebView(context).apply {\n                try {\n                    setBackgroundColor(Color.TRANSPARENT)\n                    isVerticalScrollBarEnabled = false\n                    isHorizontalScrollBarEnabled = false\n\n                    settings.apply {\n                        offscreenPreRaster = true\n                        javaScriptEnabled = true\n                        domStorageEnabled = true\n                        mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW\n                        allowContentAccess = false\n                        allowFileAccess = false\n                        cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK\n                        textZoom = newTextZoom\n                        setSupportZoom(false)\n                        setGeolocationEnabled(false)\n                    }\n                    layoutParams = FrameLayout.LayoutParams(\n                        FrameLayout.LayoutParams.MATCH_PARENT,\n                        FrameLayout.LayoutParams.WRAP_CONTENT\n                    )\n                    addJavascriptInterface(scrollInterface, \"AndroidScroll\")\n                    webViewClient = object : WebViewClient() {\n                        private val assetLoader = WebViewAssetLoader.Builder()\n                            .addPathHandler(\"/assets/\", WebViewAssetLoader.AssetsPathHandler(context))\n                            .build()\n\n                        override fun onPageFinished(view: WebView, url: String) {\n                            super.onPageFinished(view, url)\n\n                            val js = \"\"\"\n                                (function() {\n                                    if (window.androidScrollInjected) return;\n                                    window.androidScrollInjected = true;\n                                \n                                    function checkScroll(target) {\n                                        if (!target || target === document.body || target === document.documentElement) return {l: false, r: false};\n                                        var style = window.getComputedStyle(target);\n                                        if (style.overflowX !== 'auto' && style.overflowX !== 'scroll') return {l: false, r: false};\n                                        if (target.scrollWidth <= target.clientWidth) return {l: false, r: false};\n                                        \n                                        var atLeft = target.scrollLeft <= 0;\n                                        var atRight = Math.ceil(target.scrollLeft + target.clientWidth) >= target.scrollWidth;\n                                        \n                                        return {l: !atLeft, r: !atRight};\n                                    }\n                                \n                                    var lastTarget = null;\n                                    var lastState = {l: false, r: false};\n                                    \n                                    function update(l, r) {\n                                        if (lastState.l !== l || lastState.r !== r) {\n                                            lastState = {l: l, r: r};\n                                            AndroidScroll.updateScrollState(l, r);\n                                        }\n                                    }\n                                \n                                    document.addEventListener('touchstart', function(e) {\n                                        var t = e.target;\n                                        var found = false;\n                                        while(t && t !== document.body) {\n                                            var s = checkScroll(t);\n                                            if (s.l || s.r) { \n                                                 lastTarget = t;\n                                                 update(s.l, s.r);\n                                                 found = true;\n                                                 break;\n                                            }\n                                            t = t.parentElement;\n                                        }\n                                        if (!found) {\n                                            lastTarget = null;\n                                            update(false, false);\n                                        }\n                                    }, {passive: true});\n                                \n                                    document.addEventListener('touchmove', function(e) {\n                                        if (lastTarget) {\n                                             var s = checkScroll(lastTarget);\n                                             update(s.l, s.r);\n                                        }\n                                    }, {passive: true});\n                                    \n                                    document.addEventListener('scroll', function(e) {\n                                        if (lastTarget && (e.target === lastTarget || e.target.contains(lastTarget))) {\n                                              var s = checkScroll(lastTarget);\n                                              update(s.l, s.r);\n                                        }\n                                    }, {passive: true, capture: true});\n                                })();\n                            \"\"\".trimIndent()\n                            view.evaluateJavascript(js, null)\n                        }\n\n                        override fun shouldOverrideUrlLoading(\n                            view: WebView, request: WebResourceRequest\n                        ): Boolean {\n                            try {\n                                val intent = Intent(Intent.ACTION_VIEW, request.url)\n                                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n                                context.startActivity(intent)\n                            } catch (_: ActivityNotFoundException) {\n                                Log.w(\"GithubMarkdown\", \"No activity to handle: ${request.url}\")\n                            }\n                            return true\n                        }\n\n                        override fun onPageCommitVisible(view: WebView?, url: String?) {\n                            onLoadingChange(false)\n                        }\n\n                        override fun shouldInterceptRequest(\n                            view: WebView, request: WebResourceRequest\n                        ): WebResourceResponse? {\n                            assetLoader.shouldInterceptRequest(request.url)?.let { return it }\n                            val scheme = request.url.scheme ?: return null\n                            if (!scheme.startsWith(\"http\")) return null\n                            val client: OkHttpClient = ksuApp.okhttpClient\n                            val call = client.newCall(\n                                Request.Builder()\n                                    .url(request.url.toString())\n                                    .method(request.method, null)\n                                    .headers(request.requestHeaders.toHeaders())\n                                    .build()\n                            )\n                            return try {\n                                val reply: Response = call.execute()\n                                val header = reply.header(\"content-type\", \"text/plain; charset=utf-8\")\n                                val contentTypes = header?.split(SEMICOLON_SPLIT) ?: emptyList()\n                                val mimeType = contentTypes.firstOrNull() ?: \"image/*\"\n                                val charset = contentTypes.getOrNull(1)?.split(EQUALS_SPLIT)?.getOrNull(1) ?: \"utf-8\"\n                                val bytes = reply.body.bytes()\n                                WebResourceResponse(mimeType, charset, ByteArrayInputStream(bytes))\n                            } catch (e: IOException) {\n                                Log.e(\"GithubMarkdown\", \"Resource load failed\", e)\n                                WebResourceResponse(\n                                    \"text/html\", \"utf-8\",\n                                    ByteArrayInputStream(ByteArray(0))\n                                )\n                            }\n                        }\n                    }\n                    setOnTouchListener(object : View.OnTouchListener {\n                        private var isHorizontalScrollLocked = false\n                        private var initialDownX = 0f\n                        private var initialDownY = 0f\n\n                        @SuppressLint(\"ClickableViewAccessibility\")\n                        override fun onTouch(v: View, event: MotionEvent): Boolean {\n                            when (event.action) {\n                                MotionEvent.ACTION_DOWN -> {\n                                    initialDownX = event.x\n                                    initialDownY = event.y\n                                    isHorizontalScrollLocked = false\n                                    v.parent.requestDisallowInterceptTouchEvent(true)\n                                }\n\n                                MotionEvent.ACTION_MOVE -> {\n                                    if (isHorizontalScrollLocked) {\n                                        v.parent.requestDisallowInterceptTouchEvent(true)\n                                    } else {\n                                        val dx = event.x - initialDownX\n                                        val dy = event.y - initialDownY\n                                        if (abs(dx) > abs(dy)) {\n                                            val canScroll = if (dx < 0) scrollInterface.canScrollRight else scrollInterface.canScrollLeft\n                                            if (canScroll) {\n                                                isHorizontalScrollLocked = true\n                                                v.parent.requestDisallowInterceptTouchEvent(true)\n                                            } else {\n                                                v.parent.requestDisallowInterceptTouchEvent(false)\n                                            }\n                                        } else {\n                                            v.parent.requestDisallowInterceptTouchEvent(false)\n                                            return true\n                                        }\n                                    }\n                                }\n\n                                MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {\n                                    v.parent.requestDisallowInterceptTouchEvent(false)\n                                    isHorizontalScrollLocked = false\n                                }\n                            }\n                            return false\n                        }\n                    })\n                    loadDataWithBaseURL(\n                        \"https://appassets.androidplatform.net\", html,\n                        \"text/html\", StandardCharsets.UTF_8.name(), null\n                    )\n                } catch (e: Throwable) {\n                    Log.e(\"GithubMarkdown\", \"WebView setup failed\", e)\n                }\n            }\n            frameLayout.addView(webView)\n            frameLayout\n        },\n        update = { frameLayout ->\n            val webView = frameLayout.getChildAt(0) as? WebView ?: return@AndroidView\n            webView.settings.textZoom = newTextZoom\n            onLoadingChange(true)\n            webView.loadDataWithBaseURL(\n                \"https://appassets.androidplatform.net\", html,\n                \"text/html\", StandardCharsets.UTF_8.name(), null\n            )\n        },\n        onRelease = { frameLayout ->\n            val webView = frameLayout.getChildAt(0) as? WebView\n            frameLayout.removeAllViews()\n            webView?.apply {\n                stopLoading()\n                destroy()\n            }\n        },\n        modifier = Modifier\n            .fillMaxWidth()\n            .wrapContentHeight()\n            .clipToBounds(),\n    )\n}\n\nclass MarkdownScrollInterface {\n    @Volatile\n    var canScrollLeft = false\n\n    @Volatile\n    var canScrollRight = false\n\n    @JavascriptInterface\n    fun updateScrollState(left: Boolean, right: Boolean) {\n        canScrollLeft = left\n        canScrollRight = right\n    }\n}\n\nprivate data class MarkdownColors(\n    val bgDefault: String,\n    val bgMuted: String,\n    val bgNeutralMuted: String,\n    val bgAttentionMuted: String,\n    val fgLink: String\n)\n\n@Composable\nprivate fun getMarkdownColors(containerColor: androidx.compose.ui.graphics.Color?): MarkdownColors {\n    val uiMode = LocalUiMode.current\n\n    return when (uiMode) {\n        UiMode.Material -> {\n            val bgArgb = containerColor?.toArgb() ?: MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp).toArgb()\n\n            MarkdownColors(\n                cssColorFromArgb(bgArgb),\n                cssColorFromArgb(MaterialTheme.colorScheme.surfaceContainerHigh.toArgb()),\n                cssColorFromArgb(MaterialTheme.colorScheme.surfaceDim.toArgb()),\n                cssColorFromArgb(MaterialTheme.colorScheme.surfaceBright.toArgb()),\n                cssColorFromArgb(MaterialTheme.colorScheme.primary.toArgb())\n            )\n        }\n\n        UiMode.Miuix -> {\n            val bgArgb = containerColor?.toArgb() ?: MiuixTheme.colorScheme.surfaceContainer.toArgb()\n            val bgLuminance = relativeLuminance(bgArgb)\n\n            fun makeVariant(delta: Float): Int {\n                val candidate = adjustLightnessArgb(bgArgb, delta)\n                val madeLighter = delta > 0f\n                return ensureVisibleByMix(bgArgb, candidate, 1.15, madeLighter)\n            }\n\n            MarkdownColors(\n                cssColorFromArgb(bgArgb),\n                cssColorFromArgb(makeVariant(if (bgLuminance > 0.6) -0.06f else 0.06f)),\n                cssColorFromArgb(makeVariant(if (bgLuminance > 0.6) -0.12f else 0.12f)),\n                cssColorFromArgb(makeVariant(-0.12f)),\n                cssColorFromArgb(MiuixTheme.colorScheme.primary.toArgb())\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/KeyEventBlocker.kt",
    "content": "package me.weishu.kernelsu.ui.component\n\nimport androidx.compose.foundation.focusable\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.input.key.KeyEvent\nimport androidx.compose.ui.input.key.onKeyEvent\n\n@Composable\nfun KeyEventBlocker(predicate: (KeyEvent) -> Boolean) {\n    val requester = remember { FocusRequester() }\n    Box(\n        Modifier\n            .onKeyEvent {\n                predicate(it)\n            }\n            .focusRequester(requester)\n            .focusable()\n    )\n    LaunchedEffect(Unit) {\n        requester.requestFocus()\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/KsuValidCheck.kt",
    "content": "package me.weishu.kernelsu.ui.component\n\nimport androidx.compose.runtime.Composable\nimport me.weishu.kernelsu.Natives\n\n@Composable\nfun KsuIsValid(\n    content: @Composable () -> Unit\n) {\n    val isManager = Natives.isManager\n    val ksuVersion = if (isManager) Natives.version else null\n\n    if (ksuVersion != null) {\n        content()\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/Markdown.kt",
    "content": "package me.weishu.kernelsu.ui.component\n\nimport android.graphics.text.LineBreaker\nimport android.text.Layout\nimport android.text.method.LinkMovementMethod\nimport android.view.ViewGroup\nimport android.widget.FrameLayout\nimport android.widget.ScrollView\nimport android.widget.TextView\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.wrapContentHeight\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clipToBounds\nimport androidx.compose.ui.graphics.toArgb\nimport androidx.compose.ui.viewinterop.AndroidView\nimport io.noties.markwon.Markwon\nimport io.noties.markwon.utils.NoCopySpannableFactory\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\n\nprivate const val TEXTVIEW_TAG = \"markdownTextView\"\n\n@Composable\nfun Markdown(content: String) {\n    val contentColor = when (LocalUiMode.current) {\n        UiMode.Material -> MaterialTheme.colorScheme.onBackground.toArgb()\n        UiMode.Miuix -> MiuixTheme.colorScheme.onBackground.toArgb()\n    }\n\n    AndroidView(\n        factory = { context ->\n            val frameLayout = FrameLayout(context)\n            val scrollView = ScrollView(context)\n            val textView = TextView(context).apply {\n                tag = TEXTVIEW_TAG\n                movementMethod = LinkMovementMethod.getInstance()\n                setSpannableFactory(NoCopySpannableFactory.getInstance())\n                breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE\n                hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE\n                layoutParams = ViewGroup.LayoutParams(\n                    ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT\n                )\n            }\n            scrollView.addView(textView)\n            frameLayout.addView(scrollView)\n            frameLayout\n        },\n        modifier = Modifier\n            .fillMaxWidth()\n            .wrapContentHeight()\n            .clipToBounds(),\n        update = { frameLayout ->\n            frameLayout.findViewWithTag<TextView>(TEXTVIEW_TAG)?.let { textView ->\n                Markwon.create(textView.context).setMarkdown(textView, content)\n                textView.setTextColor(contentColor)\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/MenuPositionProvider.kt",
    "content": "package me.weishu.kernelsu.ui.component\n\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.IntRect\nimport androidx.compose.ui.unit.IntSize\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport top.yukonga.miuix.kmp.basic.PopupPositionProvider\n\nobject ListPopupDefaults {\n    val MenuPositionProvider = object : PopupPositionProvider {\n        override fun calculatePosition(\n            anchorBounds: IntRect,\n            windowBounds: IntRect,\n            layoutDirection: LayoutDirection,\n            popupContentSize: IntSize,\n            popupMargin: IntRect,\n            alignment: PopupPositionProvider.Align,\n        ): IntOffset {\n            val offsetX: Int\n            val offsetY: Int\n            when (alignment.resolve(layoutDirection)) {\n                PopupPositionProvider.Align.TopStart -> {\n                    offsetX = anchorBounds.left + popupMargin.left\n                    offsetY = anchorBounds.bottom + popupMargin.top\n                }\n\n                PopupPositionProvider.Align.TopEnd -> {\n                    offsetX = anchorBounds.right - popupContentSize.width - popupMargin.right\n                    offsetY = anchorBounds.bottom + popupMargin.top\n                }\n\n                PopupPositionProvider.Align.BottomStart -> {\n                    offsetX = anchorBounds.left + popupMargin.left\n                    offsetY = anchorBounds.top - popupContentSize.height - popupMargin.bottom\n                }\n\n                PopupPositionProvider.Align.BottomEnd -> {\n                    offsetX = anchorBounds.right - popupContentSize.width - popupMargin.right\n                    offsetY = anchorBounds.top - popupContentSize.height - popupMargin.bottom\n                }\n\n                else -> {\n                    // Fallback\n                    offsetX = if (alignment.resolve(layoutDirection) == PopupPositionProvider.Align.End) {\n                        anchorBounds.right - popupContentSize.width - popupMargin.right\n                    } else {\n                        anchorBounds.left + popupMargin.left\n                    }\n                    offsetY = if (windowBounds.bottom - anchorBounds.bottom > popupContentSize.height) {\n                        // Show below\n                        anchorBounds.bottom + popupMargin.bottom\n                    } else if (anchorBounds.top - windowBounds.top > popupContentSize.height) {\n                        // Show above\n                        anchorBounds.top - popupContentSize.height - popupMargin.top\n                    } else {\n                        // Middle\n                        anchorBounds.top + anchorBounds.height / 2 - popupContentSize.height / 2\n                    }\n                }\n            }\n            return IntOffset(\n                x = offsetX.coerceIn(\n                    windowBounds.left,\n                    (windowBounds.right - popupContentSize.width - popupMargin.right).coerceAtLeast(windowBounds.left),\n                ),\n                y = offsetY.coerceIn(\n                    (windowBounds.top + popupMargin.top).coerceAtMost(windowBounds.bottom - popupContentSize.height - popupMargin.bottom),\n                    windowBounds.bottom - popupContentSize.height - popupMargin.bottom,\n                ),\n            )\n        }\n\n        override fun getMargins(): PaddingValues = PaddingValues(start = 20.dp)\n    }\n}\n\n\nprivate fun PopupPositionProvider.Align.resolve(layoutDirection: LayoutDirection): PopupPositionProvider.Align {\n    if (layoutDirection == LayoutDirection.Ltr) return this\n    return when (this) {\n        PopupPositionProvider.Align.Start -> PopupPositionProvider.Align.End\n        PopupPositionProvider.Align.End -> PopupPositionProvider.Align.Start\n        PopupPositionProvider.Align.TopStart -> PopupPositionProvider.Align.TopEnd\n        PopupPositionProvider.Align.TopEnd -> PopupPositionProvider.Align.TopStart\n        PopupPositionProvider.Align.BottomStart -> PopupPositionProvider.Align.BottomEnd\n        PopupPositionProvider.Align.BottomEnd -> PopupPositionProvider.Align.BottomStart\n    }\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchStatus.kt",
    "content": "package me.weishu.kernelsu.ui.component\n\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Stable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\n\n@Stable\ndata class SearchStatus(\n    val label: String,\n    val searchText: String = \"\",\n    val current: Status = Status.COLLAPSED,\n    val offsetY: Dp = 0.dp,\n    val resultStatus: ResultStatus = ResultStatus.DEFAULT\n) {\n    fun isExpand() = current == Status.EXPANDED\n    fun isCollapsed() = current == Status.COLLAPSED\n    fun shouldExpand() = current == Status.EXPANDED || current == Status.EXPANDING\n    fun shouldCollapsed() = current == Status.COLLAPSED || current == Status.COLLAPSING\n    fun isAnimatingExpand() = current == Status.EXPANDING\n\n    fun onAnimationComplete(): SearchStatus {\n        return when (current) {\n            Status.EXPANDING -> copy(current = Status.EXPANDED)\n            Status.COLLAPSING -> copy(searchText = \"\", current = Status.COLLAPSED)\n            else -> this\n        }\n    }\n\n    @Composable\n    fun TopAppBarAnim(\n        modifier: Modifier = Modifier,\n        visible: Boolean = shouldCollapsed(),\n        hazeState: HazeState? = null,\n        hazeStyle: HazeStyle? = null,\n        content: @Composable () -> Unit\n    ) {\n        val topAppBarAlpha = animateFloatAsState(\n            if (visible) 1f else 0f,\n            animationSpec = tween(if (visible) 550 else 0, easing = FastOutSlowInEasing),\n        )\n        Box(modifier = modifier) {\n            Box(\n                modifier = Modifier\n                    .matchParentSize()\n                    .then(\n                        if (hazeState != null && hazeStyle != null) {\n                            Modifier.defaultHazeEffect(hazeState, hazeStyle)\n                        } else {\n                            Modifier.background(colorScheme.surface)\n                        }\n                    )\n            )\n            Box(\n                modifier = Modifier\n                    .graphicsLayer { alpha = topAppBarAlpha.value }\n            ) { content() }\n        }\n    }\n\n    enum class Status { EXPANDED, EXPANDING, COLLAPSED, COLLAPSING }\n    enum class ResultStatus { DEFAULT, EMPTY, LOAD, SHOW }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/bottombar/BottomBar.kt",
    "content": "package me.weishu.kernelsu.ui.component.bottombar\n\nimport androidx.compose.animation.core.EaseInOut\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.gestures.animateScrollBy\nimport androidx.compose.foundation.pager.PagerState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport com.kyant.backdrop.Backdrop\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.job\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport kotlin.math.abs\n\nclass MainPagerState(\n    val pagerState: PagerState,\n    private val coroutineScope: CoroutineScope\n) {\n    var selectedPage by mutableIntStateOf(pagerState.currentPage)\n        private set\n\n    var isNavigating by mutableStateOf(false)\n        private set\n\n    private var navJob: Job? = null\n\n    fun animateToPage(targetIndex: Int) {\n        if (targetIndex == selectedPage) return\n\n        navJob?.cancel()\n\n        selectedPage = targetIndex\n        isNavigating = true\n\n        val distance = abs(targetIndex - pagerState.currentPage).coerceAtLeast(2)\n        val duration = 100 * distance + 100\n        val layoutInfo = pagerState.layoutInfo\n        val pageSize = layoutInfo.pageSize + layoutInfo.pageSpacing\n        val currentDistanceInPages = targetIndex - pagerState.currentPage - pagerState.currentPageOffsetFraction\n        val scrollPixels = currentDistanceInPages * pageSize\n\n        navJob = coroutineScope.launch {\n            val myJob = coroutineContext.job\n            try {\n                pagerState.animateScrollBy(\n                    value = scrollPixels,\n                    animationSpec = tween(easing = EaseInOut, durationMillis = duration)\n                )\n            } finally {\n                if (navJob == myJob) {\n                    isNavigating = false\n                    if (pagerState.currentPage != targetIndex) {\n                        selectedPage = pagerState.currentPage\n                    }\n                }\n            }\n        }\n    }\n\n    fun syncPage() {\n        if (!isNavigating && selectedPage != pagerState.currentPage) {\n            selectedPage = pagerState.currentPage\n        }\n    }\n}\n\n@Composable\nfun rememberMainPagerState(\n    pagerState: PagerState,\n    coroutineScope: CoroutineScope = rememberCoroutineScope()\n): MainPagerState {\n    return remember(pagerState, coroutineScope) {\n        MainPagerState(pagerState, coroutineScope)\n    }\n}\n\n@Composable\nfun BottomBar(\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n    backdrop: Backdrop,\n    modifier: Modifier = Modifier,\n) {\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> BottomBarMiuix(hazeState, hazeStyle, backdrop, modifier)\n        UiMode.Material -> BottomBarMaterial()\n    }\n}\n\n@Composable\nfun SideRail(\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n    modifier: Modifier = Modifier,\n) {\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> NavigationRailMiuix(hazeState, hazeStyle, modifier)\n        UiMode.Material -> NavigationRailMaterial(modifier)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/bottombar/BottomBarMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.component.bottombar\n\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.union\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Extension\nimport androidx.compose.material.icons.filled.Home\nimport androidx.compose.material.icons.filled.Settings\nimport androidx.compose.material.icons.filled.Shield\nimport androidx.compose.material.icons.outlined.Extension\nimport androidx.compose.material.icons.outlined.Home\nimport androidx.compose.material.icons.outlined.Settings\nimport androidx.compose.material.icons.outlined.Shield\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FlexibleBottomAppBar\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.NavigationBarItem\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.LocalMainPagerState\nimport me.weishu.kernelsu.ui.util.rootAvailable\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun BottomBarMaterial() {\n    val isManager = Natives.isManager\n    val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()\n    val mainPagerState = LocalMainPagerState.current\n\n    if (!fullFeatured) return\n\n    val items = listOf(\n        Triple(R.string.home, Icons.Filled.Home, Icons.Outlined.Home),\n        Triple(R.string.superuser, Icons.Filled.Shield, Icons.Outlined.Shield),\n        Triple(R.string.module, Icons.Filled.Extension, Icons.Outlined.Extension),\n        Triple(R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings)\n    )\n\n    FlexibleBottomAppBar(\n        windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(\n            WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom\n        )\n    ) {\n        items.forEachIndexed { index, (label, selectedIcon, unselectedIcon) ->\n            val selected = mainPagerState.selectedPage == index\n            NavigationBarItem(\n                selected = selected,\n                onClick = {\n                    if (!selected) {\n                        mainPagerState.animateToPage(index)\n                    }\n                },\n                icon = {\n                    Icon(\n                        if (selected) selectedIcon else unselectedIcon,\n                        stringResource(label)\n                    )\n                },\n                label = {\n                    Text(\n                        stringResource(label),\n                        maxLines = 1,\n                        overflow = TextOverflow.Ellipsis\n                    )\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/bottombar/BottomBarMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.component.bottombar\n\nimport androidx.annotation.StringRes\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.defaultMinSize\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Cottage\nimport androidx.compose.material.icons.rounded.Extension\nimport androidx.compose.material.icons.rounded.Security\nimport androidx.compose.material.icons.rounded.Settings\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.kyant.backdrop.Backdrop\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.LocalMainPagerState\nimport me.weishu.kernelsu.ui.component.FloatingBottomBar\nimport me.weishu.kernelsu.ui.component.FloatingBottomBarItem\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.theme.LocalEnableFloatingBottomBar\nimport me.weishu.kernelsu.ui.theme.LocalEnableFloatingBottomBarBlur\nimport me.weishu.kernelsu.ui.util.rootAvailable\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.NavigationBar\nimport top.yukonga.miuix.kmp.basic.NavigationBarItem\nimport top.yukonga.miuix.kmp.basic.NavigationItem\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\n\n@Composable\nfun BottomBarMiuix(\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n    backdrop: Backdrop,\n    modifier: Modifier,\n) {\n    val isManager = Natives.isManager\n    val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()\n    if (!fullFeatured) return\n\n    val mainState = LocalMainPagerState.current\n    val enableBlur = LocalEnableBlur.current\n    val enableFloatingBottomBar = LocalEnableFloatingBottomBar.current\n    val enableFloatingBottomBarBlur = LocalEnableFloatingBottomBarBlur.current\n\n    val items = BottomBarDestination.entries.map { destination ->\n        NavigationItem(\n            label = stringResource(destination.label),\n            icon = destination.icon,\n        )\n    }\n    if (!enableFloatingBottomBar) {\n        NavigationBar(\n            modifier = modifier\n                .then(\n                    if (enableBlur) {\n                        Modifier.defaultHazeEffect(hazeState, hazeStyle)\n                    } else Modifier\n                ),\n            color = if (enableBlur) Color.Transparent else MiuixTheme.colorScheme.surface,\n            content = {\n                items.forEachIndexed { index, item ->\n                    NavigationBarItem(\n                        modifier = Modifier.weight(1f),\n                        icon = item.icon,\n                        label = item.label,\n                        selected = mainState.selectedPage == index,\n                        onClick = {\n                            mainState.animateToPage(index)\n                        }\n                    )\n                }\n            }\n        )\n    } else {\n        FloatingBottomBar(\n            modifier = modifier\n                .clickable(\n                    interactionSource = remember { MutableInteractionSource() },\n                    indication = null,\n                    onClick = {},\n                )\n                .padding(bottom = 12.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()),\n            selectedIndex = { mainState.selectedPage },\n            onSelected = { mainState.animateToPage(it) },\n            backdrop = backdrop,\n            tabsCount = items.size,\n            isBlurEnabled = enableFloatingBottomBarBlur,\n        ) {\n            items.forEachIndexed { index, item ->\n                FloatingBottomBarItem(\n                    onClick = {\n                        mainState.animateToPage(index)\n                    },\n                    modifier = Modifier.defaultMinSize(minWidth = 76.dp)\n                ) {\n                    Icon(\n                        imageVector = item.icon,\n                        contentDescription = item.label,\n                        tint = MiuixTheme.colorScheme.onSurface\n                    )\n                    Text(\n                        text = item.label,\n                        fontSize = 11.sp,\n                        lineHeight = 14.sp,\n                        color = MiuixTheme.colorScheme.onSurface,\n                        maxLines = 1,\n                        softWrap = false,\n                        overflow = TextOverflow.Visible\n                    )\n                }\n            }\n        }\n    }\n}\n\nenum class BottomBarDestination(\n    @get:StringRes val label: Int,\n    val icon: ImageVector,\n) {\n    Home(R.string.home, Icons.Rounded.Cottage),\n    SuperUser(R.string.superuser, Icons.Rounded.Security),\n    Module(R.string.module, Icons.Rounded.Extension),\n    Setting(R.string.settings, Icons.Rounded.Settings)\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/bottombar/NavigationRailMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.component.bottombar\n\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.union\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Extension\nimport androidx.compose.material.icons.filled.Home\nimport androidx.compose.material.icons.filled.Settings\nimport androidx.compose.material.icons.filled.Shield\nimport androidx.compose.material.icons.outlined.Extension\nimport androidx.compose.material.icons.outlined.Home\nimport androidx.compose.material.icons.outlined.Settings\nimport androidx.compose.material.icons.outlined.Shield\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.NavigationRail\nimport androidx.compose.material3.NavigationRailItem\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.LocalMainPagerState\nimport me.weishu.kernelsu.ui.util.rootAvailable\n\n@Composable\nfun NavigationRailMaterial(\n    modifier: Modifier = Modifier,\n) {\n    val isManager = Natives.isManager\n    val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()\n    val mainPagerState = LocalMainPagerState.current\n\n    if (!fullFeatured) return\n\n    val items = listOf(\n        Triple(R.string.home, Icons.Filled.Home, Icons.Outlined.Home),\n        Triple(R.string.superuser, Icons.Filled.Shield, Icons.Outlined.Shield),\n        Triple(R.string.module, Icons.Filled.Extension, Icons.Outlined.Extension),\n        Triple(R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings)\n    )\n\n    NavigationRail(\n        modifier = modifier.fillMaxHeight(),\n        windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(\n            WindowInsetsSides.Start + WindowInsetsSides.Vertical\n        )\n    ) {\n        Spacer(Modifier.weight(1f))\n        items.forEachIndexed { index, (label, selectedIcon, unselectedIcon) ->\n            val selected = mainPagerState.selectedPage == index\n            NavigationRailItem(\n                selected = selected,\n                onClick = {\n                    if (!selected) {\n                        mainPagerState.animateToPage(index)\n                    }\n                },\n                icon = {\n                    Icon(\n                        if (selected) selectedIcon else unselectedIcon,\n                        stringResource(label)\n                    )\n                },\n                label = { Text(stringResource(label)) }\n            )\n        }\n        Spacer(Modifier.weight(1f))\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/bottombar/NavigationRailMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.component.bottombar\n\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport me.weishu.kernelsu.ui.LocalMainPagerState\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.util.rootAvailable\nimport top.yukonga.miuix.kmp.basic.NavigationRail\nimport top.yukonga.miuix.kmp.basic.NavigationRailItem\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\n\n@Composable\nfun NavigationRailMiuix(\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n    modifier: Modifier = Modifier,\n) {\n    val isManager = Natives.isManager\n    val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable()\n    if (!fullFeatured) return\n\n    val mainState = LocalMainPagerState.current\n    val enableBlur = LocalEnableBlur.current\n\n    val items = BottomBarDestination.entries.map { destination ->\n        Pair(stringResource(destination.label), destination.icon)\n    }\n\n    NavigationRail(\n        modifier = modifier\n            .fillMaxHeight()\n            .then(\n                if (enableBlur) {\n                    Modifier.defaultHazeEffect(hazeState, hazeStyle)\n                } else Modifier\n            ),\n        color = if (enableBlur) Color.Transparent else MiuixTheme.colorScheme.surface,\n    ) {\n        Spacer(modifier = Modifier.weight(1f))\n        items.forEachIndexed { index, (label, icon) ->\n            NavigationRailItem(\n                icon = icon,\n                label = label,\n                selected = mainState.selectedPage == index,\n                onClick = {\n                    mainState.animateToPage(index)\n                },\n                modifier = Modifier.padding(vertical = 4.dp)\n            )\n        }\n        Spacer(modifier = Modifier.weight(1f))\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/choosekmidialog/ChooseKmiDialog.kt",
    "content": "package me.weishu.kernelsu.ui.component.choosekmidialog\n\nimport androidx.compose.runtime.Composable\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\n\n@Composable\nfun ChooseKmiDialog(\n    show: Boolean,\n    onDismissRequest: () -> Unit,\n    onSelected: (String?) -> Unit\n) {\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> ChooseKmiDialogMiuix(show, onDismissRequest, onSelected)\n        UiMode.Material -> ChooseKmiDialogMaterial(show, onDismissRequest, onSelected)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/choosekmidialog/ChooseKmiDialogMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.component.choosekmidialog\n\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedRadioItem\nimport me.weishu.kernelsu.ui.util.getCurrentKmi\nimport me.weishu.kernelsu.ui.util.getSupportedKmis\n\n@Composable\nfun ChooseKmiDialogMaterial(\n    show: Boolean,\n    onDismissRequest: () -> Unit,\n    onSelected: (String?) -> Unit\n) {\n    if (!show) return\n\n    val supportedKMIs by produceState(initialValue = emptyList()) {\n        value = getSupportedKmis()\n    }\n\n    val currentKmi by produceState(initialValue = \"\") {\n        value = getCurrentKmi()\n    }\n\n    val selectedKmi = remember(currentKmi) { mutableStateOf(currentKmi) }\n\n    AlertDialog(\n        onDismissRequest = {\n            onDismissRequest()\n            selectedKmi.value = currentKmi\n        },\n        confirmButton = {\n            TextButton(\n                onClick = {\n                    onSelected(selectedKmi.value)\n                    onDismissRequest()\n                },\n                enabled = supportedKMIs.contains(selectedKmi.value)\n            ) {\n                Text(stringResource(id = R.string.confirm))\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = {\n                onDismissRequest()\n                selectedKmi.value = currentKmi\n            }) {\n                Text(stringResource(id = android.R.string.cancel))\n            }\n        },\n        title = {\n            Text(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(bottom = 16.dp),\n                text = stringResource(R.string.select_kmi),\n                textAlign = TextAlign.Center\n            )\n        },\n        text = {\n            SegmentedColumn(\n                content = supportedKMIs.map { kmi ->\n                    {\n                        SegmentedRadioItem(\n                            title = kmi,\n                            summary = if (kmi == currentKmi) stringResource(R.string.current_device_kmi) else null,\n                            selected = selectedKmi.value == kmi,\n                            onClick = { selectedKmi.value = kmi }\n                        )\n                    }\n                }\n            )\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/choosekmidialog/ChooseKmiDialogMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.component.choosekmidialog\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.util.getCurrentKmi\nimport me.weishu.kernelsu.ui.util.getSupportedKmis\nimport top.yukonga.miuix.kmp.basic.ButtonDefaults\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.extra.CheckboxLocation\nimport top.yukonga.miuix.kmp.extra.SuperCheckbox\nimport top.yukonga.miuix.kmp.extra.SuperDialog\n\n@Composable\nfun ChooseKmiDialogMiuix(\n    show: Boolean,\n    onDismissRequest: () -> Unit,\n    onSelected: (String?) -> Unit\n) {\n    val supportedKMIs by produceState(initialValue = emptyList()) {\n        value = getSupportedKmis()\n    }\n    val currentKmi by produceState(initialValue = \"\") {\n        value = getCurrentKmi()\n    }\n    val currentSelection = rememberSaveable(currentKmi) { mutableStateOf(currentKmi) }\n    SuperDialog(\n        show = show,\n        title = stringResource(R.string.select_kmi),\n        summary = stringResource(R.string.current_kmi, currentKmi.let { it.ifBlank { \"Unknown\" } }),\n        onDismissRequest = {\n            onDismissRequest()\n            currentSelection.value = currentKmi\n        },\n        insideMargin = DpSize(0.dp, 24.dp),\n        content = {\n            Column(modifier = Modifier.heightIn(max = 500.dp)) {\n                LazyColumn(modifier = Modifier.weight(1f, fill = false)) {\n                    items(supportedKMIs) { kmi ->\n                        SuperCheckbox(\n                            title = kmi,\n                            summary = if (kmi == currentKmi) stringResource(R.string.current_device_kmi) else null,\n                            insideMargin = PaddingValues(horizontal = 30.dp, vertical = 16.dp),\n                            checkboxLocation = CheckboxLocation.End,\n                            checked = currentSelection.value == kmi,\n                            holdDownState = currentSelection.value == kmi,\n                            onCheckedChange = { _ ->\n                                currentSelection.value = kmi\n                            }\n                        )\n                    }\n                }\n                Spacer(Modifier.height(12.dp))\n                Row(\n                    modifier = Modifier.padding(horizontal = 24.dp),\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    TextButton(\n                        onClick = {\n                            onDismissRequest()\n                            currentSelection.value = currentKmi\n                        },\n                        text = stringResource(android.R.string.cancel),\n                        modifier = Modifier.weight(1f),\n                    )\n                    Spacer(modifier = Modifier.width(20.dp))\n                    TextButton(\n                        enabled = supportedKMIs.contains(currentSelection.value),\n                        onClick = {\n                            onSelected(currentSelection.value)\n                            onDismissRequest()\n                        },\n                        text = stringResource(R.string.confirm),\n                        modifier = Modifier.weight(1f),\n                        colors = ButtonDefaults.textButtonColorsPrimary()\n                    )\n                }\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/dialog/Dialog.kt",
    "content": "package me.weishu.kernelsu.ui.component.dialog\n\nimport android.os.Parcelable\nimport android.util.Log\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport kotlinx.coroutines.CancellableContinuation\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.channels.Channel\nimport kotlinx.coroutines.channels.ReceiveChannel\nimport kotlinx.coroutines.flow.FlowCollector\nimport kotlinx.coroutines.flow.consumeAsFlow\nimport kotlinx.coroutines.flow.onEach\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.suspendCancellableCoroutine\nimport kotlinx.parcelize.Parcelize\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport kotlin.coroutines.resume\n\nprivate const val TAG = \"DialogComponent\"\n\ninterface ConfirmDialogVisuals : Parcelable {\n    val title: String\n    val content: String?\n    val isMarkdown: Boolean\n    val isHtml: Boolean\n    val confirm: String?\n    val dismiss: String?\n}\n\n@Parcelize\nprivate data class ConfirmDialogVisualsImpl(\n    override val title: String,\n    override val content: String?,\n    override val isMarkdown: Boolean,\n    override val isHtml: Boolean,\n    override val confirm: String?,\n    override val dismiss: String?,\n) : ConfirmDialogVisuals {\n    companion object {\n        val Empty: ConfirmDialogVisuals = ConfirmDialogVisualsImpl(\"\", \"\", isMarkdown = false, isHtml = false, confirm = null, dismiss = null)\n    }\n}\n\ninterface DialogHandle {\n    val isShown: Boolean\n    val dialogType: String\n    fun show()\n    fun hide()\n}\n\ninterface LoadingDialogHandle : DialogHandle {\n    suspend fun <R> withLoading(block: suspend () -> R): R\n    fun showLoading()\n}\n\nsealed interface ConfirmResult {\n    object Confirmed : ConfirmResult\n    object Canceled : ConfirmResult\n}\n\ninterface ConfirmDialogHandle : DialogHandle {\n    val visuals: ConfirmDialogVisuals\n\n    fun showConfirm(\n        title: String,\n        content: String? = null,\n        markdown: Boolean = false,\n        html: Boolean = false,\n        confirm: String? = null,\n        dismiss: String? = null\n    )\n\n    suspend fun awaitConfirm(\n        title: String,\n        content: String? = null,\n        markdown: Boolean = false,\n        html: Boolean = false,\n        confirm: String? = null,\n        dismiss: String? = null\n    ): ConfirmResult\n}\n\nprivate abstract class DialogHandleBase(\n    val visible: MutableState<Boolean>,\n    val coroutineScope: CoroutineScope\n) : DialogHandle {\n    override val isShown: Boolean\n        get() = visible.value\n\n    override fun show() {\n        coroutineScope.launch {\n            visible.value = true\n        }\n    }\n\n    final override fun hide() {\n        coroutineScope.launch {\n            visible.value = false\n        }\n    }\n\n    override fun toString(): String {\n        return dialogType\n    }\n}\n\nprivate class LoadingDialogHandleImpl(\n    visible: MutableState<Boolean>,\n    coroutineScope: CoroutineScope\n) : LoadingDialogHandle, DialogHandleBase(visible, coroutineScope) {\n    override suspend fun <R> withLoading(block: suspend () -> R): R {\n        return coroutineScope.async {\n            try {\n                visible.value = true\n                block()\n            } finally {\n                visible.value = false\n            }\n        }.await()\n    }\n\n    override fun showLoading() {\n        show()\n    }\n\n    override val dialogType: String get() = \"LoadingDialog\"\n}\n\ntypealias NullableCallback = (() -> Unit)?\n\ninterface ConfirmCallback {\n\n    val onConfirm: NullableCallback\n\n    val onDismiss: NullableCallback\n\n    val isEmpty: Boolean get() = onConfirm == null && onDismiss == null\n\n    companion object {\n        operator fun invoke(\n            onConfirmProvider: () -> NullableCallback,\n            onDismissProvider: () -> NullableCallback\n        ): ConfirmCallback {\n            return object : ConfirmCallback {\n                override val onConfirm: NullableCallback\n                    get() = onConfirmProvider()\n                override val onDismiss: NullableCallback\n                    get() = onDismissProvider()\n            }\n        }\n    }\n}\n\nprivate class ConfirmDialogHandleImpl(\n    visible: MutableState<Boolean>,\n    coroutineScope: CoroutineScope,\n    callback: ConfirmCallback,\n    override var visuals: ConfirmDialogVisuals = ConfirmDialogVisualsImpl.Empty,\n    private val resultFlow: ReceiveChannel<ConfirmResult>\n) : ConfirmDialogHandle, DialogHandleBase(visible, coroutineScope) {\n    private class ResultCollector(\n        private val callback: ConfirmCallback\n    ) : FlowCollector<ConfirmResult> {\n        fun handleResult(result: ConfirmResult) {\n            Log.d(TAG, \"handleResult: ${result.javaClass.simpleName}\")\n            when (result) {\n                ConfirmResult.Confirmed -> onConfirm()\n                ConfirmResult.Canceled -> onDismiss()\n            }\n        }\n\n        fun onConfirm() {\n            callback.onConfirm?.invoke()\n        }\n\n        fun onDismiss() {\n            callback.onDismiss?.invoke()\n        }\n\n        override suspend fun emit(value: ConfirmResult) {\n            handleResult(value)\n        }\n    }\n\n    private val resultCollector = ResultCollector(callback)\n\n    private var awaitContinuation: CancellableContinuation<ConfirmResult>? = null\n\n    private val isCallbackEmpty = callback.isEmpty\n\n    init {\n        coroutineScope.launch {\n            resultFlow\n                .consumeAsFlow()\n                .onEach { result ->\n                    awaitContinuation?.let {\n                        awaitContinuation = null\n                        if (it.isActive) {\n                            it.resume(result)\n                        }\n                    }\n                }\n                .onEach { hide() }\n                .collect(resultCollector)\n        }\n    }\n\n    private suspend fun awaitResult(): ConfirmResult {\n        return suspendCancellableCoroutine {\n            awaitContinuation = it.apply {\n                if (isCallbackEmpty) {\n                    invokeOnCancellation {\n                        visible.value = false\n                    }\n                }\n            }\n        }\n    }\n\n    fun updateVisuals(visuals: ConfirmDialogVisuals) {\n        this.visuals = visuals\n    }\n\n    override fun show() {\n        if (visuals !== ConfirmDialogVisualsImpl.Empty) {\n            super.show()\n        } else {\n            throw UnsupportedOperationException(\"can't show confirm dialog with the Empty visuals\")\n        }\n    }\n\n    override fun showConfirm(\n        title: String,\n        content: String?,\n        markdown: Boolean,\n        html: Boolean,\n        confirm: String?,\n        dismiss: String?\n    ) {\n        coroutineScope.launch {\n            updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, html, confirm, dismiss))\n            show()\n        }\n    }\n\n    override suspend fun awaitConfirm(\n        title: String,\n        content: String?,\n        markdown: Boolean,\n        html: Boolean,\n        confirm: String?,\n        dismiss: String?\n    ): ConfirmResult {\n        coroutineScope.launch {\n            updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, html, confirm, dismiss))\n            show()\n        }\n        return awaitResult()\n    }\n\n    override val dialogType: String get() = \"ConfirmDialog\"\n\n    override fun toString(): String {\n        return \"${super.toString()}(visuals: $visuals)\"\n    }\n\n    companion object {\n        fun Saver(\n            visible: MutableState<Boolean>,\n            coroutineScope: CoroutineScope,\n            callback: ConfirmCallback,\n            resultChannel: ReceiveChannel<ConfirmResult>\n        ) = Saver<ConfirmDialogHandle, ConfirmDialogVisuals>(\n            save = {\n                it.visuals\n            },\n            restore = {\n                Log.d(TAG, \"ConfirmDialog restore, visuals: $it\")\n                ConfirmDialogHandleImpl(visible, coroutineScope, callback, it, resultChannel)\n            }\n        )\n    }\n}\n\n@Composable\nfun rememberLoadingDialog(): LoadingDialogHandle {\n    val visible = remember { mutableStateOf(false) }\n    val coroutineScope = rememberCoroutineScope()\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> LoadingDialogMiuix(visible)\n        UiMode.Material -> LoadingDialogMaterial(visible)\n    }\n\n    return remember {\n        LoadingDialogHandleImpl(visible, coroutineScope)\n    }\n}\n\n@Composable\nprivate fun rememberConfirmDialog(visuals: ConfirmDialogVisuals, callback: ConfirmCallback): ConfirmDialogHandle {\n    val visible = rememberSaveable {\n        mutableStateOf(false)\n    }\n    val coroutineScope = rememberCoroutineScope()\n    val resultChannel = remember {\n        Channel<ConfirmResult>()\n    }\n\n    val handle = rememberSaveable(\n        saver = ConfirmDialogHandleImpl.Saver(visible, coroutineScope, callback, resultChannel),\n        init = {\n            ConfirmDialogHandleImpl(visible, coroutineScope, callback, visuals, resultChannel)\n        }\n    )\n\n    if (visible.value) {\n        when (LocalUiMode.current) {\n            UiMode.Miuix -> ConfirmDialogMiuix(\n                handle.visuals,\n                confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } },\n                dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } },\n                showDialog = visible\n            )\n\n            UiMode.Material -> ConfirmDialogMaterial(\n                handle.visuals,\n                confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } },\n                dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } },\n                showDialog = visible\n            )\n        }\n    }\n\n    return handle\n}\n\n@Composable\nfun rememberConfirmCallback(onConfirm: NullableCallback, onDismiss: NullableCallback): ConfirmCallback {\n    val currentOnConfirm by rememberUpdatedState(newValue = onConfirm)\n    val currentOnDismiss by rememberUpdatedState(newValue = onDismiss)\n    return remember {\n        ConfirmCallback({ currentOnConfirm }, { currentOnDismiss })\n    }\n}\n\n@Composable\nfun rememberConfirmDialog(onConfirm: NullableCallback = null, onDismiss: NullableCallback = null): ConfirmDialogHandle {\n    return rememberConfirmDialog(rememberConfirmCallback(onConfirm, onDismiss))\n}\n\n@Composable\nfun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle {\n    return rememberConfirmDialog(ConfirmDialogVisualsImpl.Empty, callback)\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/dialog/DialogMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.component.dialog\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.window.Dialog\nimport androidx.compose.ui.window.DialogProperties\nimport me.weishu.kernelsu.ui.component.GithubMarkdown\nimport me.weishu.kernelsu.ui.component.Markdown\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun LoadingDialogMaterial(showDialog: MutableState<Boolean>) {\n    if (showDialog.value) {\n        Dialog(\n            onDismissRequest = {},\n            properties = DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false)\n        ) {\n            Surface(\n                modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp)\n            ) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                ) {\n                    LoadingIndicator()\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun ConfirmDialogMaterial(\n    visuals: ConfirmDialogVisuals,\n    confirm: () -> Unit,\n    dismiss: () -> Unit,\n    showDialog: MutableState<Boolean>\n) {\n    if (showDialog.value) {\n        AlertDialog(\n            onDismissRequest = {\n                dismiss()\n                showDialog.value = false\n            },\n            title = { Text(visuals.title) },\n            text = {\n                visuals.content?.let { content ->\n                    when {\n                        visuals.isMarkdown -> Markdown(content = content)\n                        visuals.isHtml -> GithubMarkdown(content = content, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh)\n                        else -> Text(text = content)\n                    }\n                }\n            },\n            confirmButton = {\n                TextButton(\n                    onClick = {\n                        confirm()\n                        showDialog.value = false\n                    }\n                ) {\n                    Text(visuals.confirm ?: stringResource(id = android.R.string.ok))\n                }\n            },\n            dismissButton = {\n                TextButton(\n                    onClick = {\n                        dismiss()\n                        showDialog.value = false\n                    }\n                ) {\n                    Text(visuals.dismiss ?: stringResource(id = android.R.string.cancel))\n                }\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/dialog/DialogMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.component.dialog\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.GithubMarkdown\nimport me.weishu.kernelsu.ui.component.Markdown\nimport top.yukonga.miuix.kmp.basic.ButtonDefaults\nimport top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.extra.WindowDialog\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\n\n@Composable\nfun LoadingDialogMiuix(showDialog: MutableState<Boolean>) {\n    WindowDialog(\n        show = showDialog.value,\n        content = {\n            Box(\n                modifier = Modifier.fillMaxWidth(),\n                contentAlignment = Alignment.CenterStart\n            ) {\n                Row(\n                    verticalAlignment = Alignment.CenterVertically,\n                    horizontalArrangement = Arrangement.Start,\n                ) {\n                    InfiniteProgressIndicator(\n                        color = MiuixTheme.colorScheme.onBackground\n                    )\n                    Text(\n                        modifier = Modifier.padding(start = 12.dp),\n                        text = stringResource(R.string.processing),\n                        fontWeight = FontWeight.Medium\n                    )\n                }\n            }\n        }\n    )\n}\n\n@Composable\nfun ConfirmDialogMiuix(\n    visuals: ConfirmDialogVisuals,\n    confirm: () -> Unit,\n    dismiss: () -> Unit,\n    showDialog: MutableState<Boolean>\n) {\n    WindowDialog(\n        show = showDialog.value,\n        modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)),\n        title = visuals.title,\n        onDismissRequest = {\n            dismiss()\n            showDialog.value = false\n        },\n        content = {\n            Layout(\n                content = {\n                    visuals.content?.let { content ->\n                        when {\n                            visuals.isMarkdown -> Markdown(content = content)\n                            visuals.isHtml -> GithubMarkdown(content = content)\n                            else -> Text(text = content)\n                        }\n                    }\n                    Row(\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        modifier = Modifier.padding(top = 12.dp)\n                    ) {\n                        TextButton(\n                            text = visuals.dismiss ?: stringResource(id = android.R.string.cancel),\n                            onClick = {\n                                dismiss()\n                                showDialog.value = false\n                            },\n                            modifier = Modifier.weight(1f)\n                        )\n                        Spacer(Modifier.width(20.dp))\n                        TextButton(\n                            text = visuals.confirm ?: stringResource(id = android.R.string.ok),\n                            onClick = {\n                                confirm()\n                                showDialog.value = false\n                            },\n                            modifier = Modifier.weight(1f),\n                            colors = ButtonDefaults.textButtonColorsPrimary()\n                        )\n                    }\n                }\n            ) { measurables, constraints ->\n                if (measurables.size != 2) {\n                    val button = measurables[0].measure(constraints)\n                    layout(constraints.maxWidth, button.height) {\n                        button.place(0, 0)\n                    }\n                } else {\n                    val button = measurables[1].measure(constraints)\n                    val lazyList = measurables[0].measure(constraints.copy(maxHeight = constraints.maxHeight - button.height))\n                    layout(constraints.maxWidth, lazyList.height + button.height) {\n                        lazyList.place(0, 0)\n                        button.place(0, lazyList.height)\n                    }\n                }\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/filter/BaseFieldFilter.kt",
    "content": "package me.weishu.kernelsu.ui.component.filter\n\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\n\nopen class BaseFieldFilter() {\n    private var inputValue = mutableStateOf(TextFieldValue())\n\n    constructor(value: String) : this() {\n        inputValue.value = TextFieldValue(value, TextRange(value.lastIndex + 1))\n    }\n\n    protected open fun onFilter(inputTextFieldValue: TextFieldValue, lastTextFieldValue: TextFieldValue): TextFieldValue {\n        return TextFieldValue()\n    }\n\n    protected open fun computePos(): Int {\n        // TODO\n        return 0\n    }\n\n    protected fun getNewTextRange(\n        lastTextFiled: TextFieldValue,\n        inputTextFieldValue: TextFieldValue\n    ): TextRange? {\n        return null\n    }\n\n    protected fun getNewText(\n        lastTextFiled: TextFieldValue,\n        inputTextFieldValue: TextFieldValue\n    ): TextRange? {\n\n        return null\n    }\n\n    fun setInputValue(value: String) {\n        inputValue.value = TextFieldValue(value, TextRange(value.lastIndex + 1))\n    }\n\n    fun getInputValue(): TextFieldValue {\n        return inputValue.value\n    }\n\n    fun onValueChange(): (TextFieldValue) -> Unit {\n        return {\n            inputValue.value = onFilter(it, inputValue.value)\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/filter/FilterNumber.kt",
    "content": "package me.weishu.kernelsu.ui.component.filter\n\nimport androidx.compose.ui.text.TextRange\nimport androidx.compose.ui.text.input.TextFieldValue\n\nclass FilterNumber(\n    private val value: Int,\n    private val minValue: Int = Int.MIN_VALUE,\n    private val maxValue: Int = Int.MAX_VALUE,\n) : BaseFieldFilter(value.toString()) {\n\n    override fun onFilter(\n        inputTextFieldValue: TextFieldValue,\n        lastTextFieldValue: TextFieldValue\n    ): TextFieldValue {\n        return filterInputNumber(inputTextFieldValue, lastTextFieldValue, minValue, maxValue)\n    }\n\n    private fun filterInputNumber(\n        inputTextFieldValue: TextFieldValue,\n        lastInputTextFieldValue: TextFieldValue,\n        minValue: Int = Int.MIN_VALUE,\n        maxValue: Int = Int.MAX_VALUE,\n    ): TextFieldValue {\n        val inputString = inputTextFieldValue.text\n        lastInputTextFieldValue.text\n\n        val newString = StringBuilder()\n        val supportNegative = minValue < 0\n        var isNegative = false\n\n        // 只允许负号在首位，并且只允许一个负号\n        if (supportNegative && inputString.isNotEmpty() && inputString.first() == '-') {\n            isNegative = true\n            newString.append('-')\n        }\n\n        for ((i, c) in inputString.withIndex()) {\n            if (i == 0 && isNegative) continue // 首字符已经处理\n            when (c) {\n                in '0'..'9' -> {\n                    newString.append(c)\n                    // 检查是否超出范围\n                    val tempText = newString.toString()\n                    // 只在不是单独 '-' 时做判断（因为 '-' toInt 会异常）\n                    if (tempText != \"-\" && tempText.isNotEmpty()) {\n                        try {\n                            val tempValue = tempText.toInt()\n                            if (tempValue !in minValue..maxValue) {\n                                newString.deleteCharAt(newString.lastIndex)\n                            }\n                        } catch (e: NumberFormatException) {\n                            // 超出int范围\n                            newString.deleteCharAt(newString.lastIndex)\n                        }\n                    }\n                }\n                // 忽略其他字符（包括点号）\n            }\n        }\n\n        val textRange: TextRange\n        if (inputTextFieldValue.selection.collapsed) { // 表示的是光标范围\n            if (inputTextFieldValue.selection.end != inputTextFieldValue.text.length) { // 光标没有指向末尾\n                var newPosition = inputTextFieldValue.selection.end + (newString.length - inputString.length)\n                if (newPosition < 0) {\n                    newPosition = inputTextFieldValue.selection.end\n                }\n                textRange = TextRange(newPosition)\n            } else { // 光标指向了末尾\n                textRange = TextRange(newString.length)\n            }\n        } else {\n            textRange = TextRange(newString.length)\n        }\n\n        return lastInputTextFieldValue.copy(\n            text = newString.toString(),\n            selection = textRange\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/material/ExpressiveSwitch.kt",
    "content": "package me.weishu.kernelsu.ui.component.material\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Check\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Switch\nimport androidx.compose.material3.SwitchColors\nimport androidx.compose.material3.SwitchDefaults\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\n\n@Composable\nfun ExpressiveSwitch(\n    checked: Boolean,\n    onCheckedChange: ((Boolean) -> Unit)?,\n    modifier: Modifier = Modifier,\n    thumbContent: (@Composable () -> Unit)? = null,\n    enabled: Boolean = true,\n    colors: SwitchColors = SwitchDefaults.colors(),\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    showThumbIcon: Boolean = true,\n) {\n    Switch(\n        checked = checked,\n        onCheckedChange = onCheckedChange,\n        modifier = modifier,\n        thumbContent = thumbContent ?: if (showThumbIcon) {\n            {\n                if (checked) {\n                    Icon(\n                        imageVector = Icons.Filled.Check,\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.primary,\n                        modifier = Modifier.size(SwitchDefaults.IconSize),\n                    )\n                } else {\n                    Icon(\n                        imageVector = Icons.Filled.Close,\n                        contentDescription = null,\n                        tint = MaterialTheme.colorScheme.surfaceContainerHighest,\n                        modifier = Modifier.size(SwitchDefaults.IconSize),\n                    )\n                }\n            }\n        } else null,\n        enabled = enabled,\n        colors = colors,\n        interactionSource = interactionSource\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/material/SearchBar.kt",
    "content": "package me.weishu.kernelsu.ui.component.material\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.BoxScope\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.navigationBarsPadding\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.layout.windowInsetsPadding\nimport androidx.compose.foundation.text.input.rememberTextFieldState\nimport androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.ExpandedFullScreenContainedSearchBar\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeFlexibleTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.SearchBar\nimport androidx.compose.material3.SearchBarDefaults\nimport androidx.compose.material3.SearchBarValue\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.material3.rememberContainedSearchBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalFocusManager\nimport androidx.compose.ui.platform.LocalSoftwareKeyboardController\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.ui.util.LocalSnackbarHost\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SearchAppBar(\n    title: @Composable () -> Unit,\n    searchText: String,\n    onSearchTextChange: (String) -> Unit,\n    onClearClick: () -> Unit,\n    navigationIcon: @Composable (() -> Unit)? = null,\n    actions: @Composable (() -> Unit)? = null,\n    scrollBehavior: TopAppBarScrollBehavior? = null,\n    searchContent: @Composable BoxScope.(bottomPadding: Dp, closeSearch: () -> Unit) -> Unit = { _, _ -> }\n) {\n    val keyboardController = LocalSoftwareKeyboardController.current\n    val focusManager = LocalFocusManager.current\n    val interactionSource = remember { MutableInteractionSource() }\n\n    val scope = rememberCoroutineScope()\n    val searchBarState = rememberContainedSearchBarState()\n    val textFieldState = rememberTextFieldState()\n    val currentQuery = textFieldState.text.toString()\n    val latestSearchText by rememberUpdatedState(searchText)\n    val isSearchExpanded = searchBarState.currentValue != SearchBarValue.Collapsed || searchBarState.targetValue != SearchBarValue.Collapsed\n    var previousSearchBarValue by remember { mutableStateOf(searchBarState.currentValue) }\n    var shouldClearOnCollapse by remember { mutableStateOf(true) }\n    val clearSearchText: () -> Unit = {\n        textFieldState.setTextAndPlaceCursorAtEnd(\"\")\n        onClearClick()\n    }\n    val collapseAndClear: () -> Unit = {\n        shouldClearOnCollapse = false\n        clearSearchText()\n        scope.launch { searchBarState.animateToCollapsed() }\n        focusManager.clearFocus()\n        keyboardController?.hide()\n    }\n\n    DisposableEffect(Unit) {\n        onDispose {\n            keyboardController?.hide()\n        }\n    }\n\n    LaunchedEffect(searchText) {\n        val current = textFieldState.text.toString()\n        if (current != searchText) {\n            textFieldState.setTextAndPlaceCursorAtEnd(searchText)\n        }\n    }\n\n    LaunchedEffect(textFieldState, latestSearchText) {\n        snapshotFlow { textFieldState.text.toString() }\n            .distinctUntilChanged()\n            .collect { value ->\n                if (value != latestSearchText) {\n                    onSearchTextChange(value)\n                }\n            }\n    }\n\n    LaunchedEffect(searchBarState) {\n        snapshotFlow { searchBarState.currentValue }\n            .distinctUntilChanged()\n            .collect { value ->\n                val collapsedFromExpanded =\n                    previousSearchBarValue != SearchBarValue.Collapsed && value == SearchBarValue.Collapsed\n                previousSearchBarValue = value\n                if (collapsedFromExpanded) {\n                    if (shouldClearOnCollapse) {\n                        clearSearchText()\n                    }\n                    shouldClearOnCollapse = true\n                    focusManager.clearFocus()\n                    keyboardController?.hide()\n                }\n            }\n    }\n\n    BackHandler(isSearchExpanded) {\n        if (isSearchExpanded) {\n            collapseAndClear()\n        }\n    }\n\n    val inputField: @Composable () -> Unit = {\n        SearchBarDefaults.InputField(\n            textFieldState = textFieldState,\n            searchBarState = searchBarState,\n            onSearch = {\n                focusManager.clearFocus()\n                keyboardController?.hide()\n            },\n            leadingIcon = {\n                if (isSearchExpanded) {\n                    IconButton(\n                        onClick = { collapseAndClear() },\n                        content = { Icon(Icons.AutoMirrored.Filled.ArrowBack, null) }\n                    )\n                } else {\n                    Icon(Icons.Filled.Search, null)\n                }\n            },\n            trailingIcon = {\n                if (isSearchExpanded && currentQuery.isNotEmpty()) {\n                    IconButton(\n                        onClick = { clearSearchText() },\n                        content = { Icon(Icons.Filled.Close, null) }\n                    )\n                }\n            },\n            interactionSource = interactionSource\n        )\n    }\n\n    Surface {\n        Column(\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            LargeFlexibleTopAppBar(\n                title = title,\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surface,\n                    scrolledContainerColor = MaterialTheme.colorScheme.surface\n                ),\n                navigationIcon = { if (navigationIcon != null) navigationIcon() },\n                actions = { if (actions != null) actions() },\n                windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),\n                scrollBehavior = scrollBehavior\n            )\n\n            SearchBar(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal))\n                    .padding(horizontal = 16.dp)\n                    .padding(bottom = 8.dp),\n                state = searchBarState,\n                inputField = inputField,\n            )\n        }\n    }\n\n    ExpandedFullScreenContainedSearchBar(\n        state = searchBarState,\n        inputField = inputField,\n        windowInsets = { SearchBarDefaults.fullScreenWindowInsets.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) },\n        colors = SearchBarDefaults.colors(\n            containerColor = MaterialTheme.colorScheme.surfaceContainerHighest,\n            inputFieldColors = SearchBarDefaults.inputFieldColors(\n                focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest,\n                unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest,\n                disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest,\n            )\n        ),\n        content = {\n            val snackBarHostState = LocalSnackbarHost.current\n            val bottomPadding = SearchBarDefaults.fullScreenWindowInsets.asPaddingValues().calculateBottomPadding()\n            Box(modifier = Modifier.fillMaxSize()) {\n                if (currentQuery.isNotEmpty()) {\n                    searchContent(bottomPadding, collapseAndClear)\n                }\n                SnackbarHost(\n                    hostState = snackBarHostState,\n                    modifier = Modifier\n                        .align(Alignment.BottomCenter)\n                        .navigationBarsPadding()\n                        .imePadding()\n                        .padding(bottom = 16.dp)\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/material/SegmentedList.kt",
    "content": "package me.weishu.kernelsu.ui.component.material\n\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.wrapContentSize\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.itemsIndexed\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.relocation.BringIntoViewRequester\nimport androidx.compose.foundation.relocation.bringIntoViewRequester\nimport androidx.compose.foundation.text.BasicTextField\nimport androidx.compose.foundation.text.KeyboardActions\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.ListItemColors\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.ListItemShapes\nimport androidx.compose.material3.LocalContentColor\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.SegmentedListItem\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.surfaceColorAtElevation\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.compositionLocalOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.focus.onFocusChanged\nimport androidx.compose.ui.graphics.Brush\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.input.VisualTransformation\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.launch\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\nval LocalListItemShapes = compositionLocalOf<ListItemShapes?> { null }\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun defaultSegmentedColors(): ListItemColors = ListItemDefaults.segmentedColors().copy(\n    containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),\n    disabledContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),\n    supportingContentColor = MaterialTheme.colorScheme.outline\n)\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun defaultSingleSegmentedShape(index: Int, count: Int): ListItemShapes {\n    val base = ListItemDefaults.segmentedShapes(index, count)\n    return if (count == 1) {\n        base.copy(shape = MaterialTheme.shapes.large)\n    } else {\n        base\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SegmentedColumn(\n    modifier: Modifier = Modifier,\n    title: String = \"\",\n    visibleLen: Int = 0,\n    content: List<@Composable () -> Unit>,\n) {\n    if (content.isEmpty()) return\n\n    Column(modifier = modifier) {\n        if (title.isNotEmpty()) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.padding(start = 16.dp, bottom = 8.dp)\n            )\n        }\n        Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {\n            content.forEachIndexed { index, itemContent ->\n                CompositionLocalProvider(\n                    LocalListItemShapes provides defaultSingleSegmentedShape(\n                        index = index,\n                        count = if (visibleLen > 0) visibleLen else content.size\n                    ),\n                ) {\n                    itemContent()\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun <T> SegmentedLazyColumn(\n    modifier: Modifier = Modifier,\n    state: LazyListState = rememberLazyListState(),\n    contentPadding: PaddingValues = PaddingValues(all = 16.dp),\n    title: String = \"\",\n    key: ((T) -> Any)? = null,\n    items: List<T>,\n    itemContent: @Composable (T) -> Unit\n) {\n    Column(modifier = modifier) {\n        if (title.isNotEmpty()) {\n            Text(\n                text = title,\n                style = MaterialTheme.typography.titleSmall,\n                color = MaterialTheme.colorScheme.primary,\n                modifier = Modifier.padding(start = 16.dp, bottom = 8.dp)\n            )\n        }\n        LazyColumn(\n            state = state,\n            modifier = Modifier.fillMaxSize(),\n            verticalArrangement = Arrangement.spacedBy(2.dp),\n            contentPadding = contentPadding\n        ) {\n            itemsIndexed(\n                items = items,\n                key = if (key != null) { _, item -> key(item) } else null\n            ) { index, item ->\n                CompositionLocalProvider(\n                    LocalListItemShapes provides defaultSingleSegmentedShape(index, items.size),\n                ) {\n                    itemContent(item)\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SegmentedListItem(\n    modifier: Modifier = Modifier,\n    onClick: (() -> Unit)? = null,\n    onLongClick: (() -> Unit)? = null,\n    enabled: Boolean = true,\n    colors: ListItemColors = defaultSegmentedColors(),\n    interactionSource: MutableInteractionSource? = null,\n    headlineContent: @Composable () -> Unit,\n    overlineContent: @Composable (() -> Unit)? = null,\n    supportingContent: @Composable (() -> Unit)? = null,\n    leadingContent: @Composable (() -> Unit)? = null,\n    trailingContent: @Composable (() -> Unit)? = null,\n    bottomContent: @Composable (() -> Unit)? = null,\n) {\n    Column(modifier = Modifier.fillMaxWidth()) {\n        SegmentedListItem(\n            onClick = onClick ?: {},\n            onLongClick = onLongClick,\n            enabled = enabled,\n            colors = colors,\n            interactionSource = interactionSource,\n            shapes = LocalListItemShapes.current ?: ListItemDefaults.segmentedShapes(0, 1),\n            modifier = modifier,\n            leadingContent = leadingContent,\n            trailingContent = trailingContent,\n            overlineContent = overlineContent,\n            supportingContent = {\n                Column {\n                    supportingContent?.invoke()\n                    bottomContent?.invoke()\n                }\n            },\n            verticalAlignment = Alignment.CenterVertically,\n            content = headlineContent\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SegmentedListItem(\n    checked: Boolean,\n    onCheckedChange: (Boolean) -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    colors: ListItemColors = defaultSegmentedColors(),\n    interactionSource: MutableInteractionSource? = null,\n    headlineContent: @Composable () -> Unit,\n    overlineContent: @Composable (() -> Unit)? = null,\n    supportingContent: @Composable (() -> Unit)? = null,\n    leadingContent: @Composable (() -> Unit)? = null,\n    trailingContent: @Composable (() -> Unit)? = null,\n    onLongClick: (() -> Unit)? = null,\n) {\n    SegmentedListItem(\n        checked = checked,\n        onCheckedChange = onCheckedChange,\n        shapes = LocalListItemShapes.current ?: ListItemDefaults.segmentedShapes(0, 1),\n        modifier = modifier,\n        enabled = enabled,\n        colors = colors,\n        interactionSource = interactionSource,\n        leadingContent = leadingContent,\n        trailingContent = trailingContent,\n        overlineContent = overlineContent,\n        supportingContent = supportingContent,\n        verticalAlignment = Alignment.CenterVertically,\n        onLongClick = onLongClick,\n        content = headlineContent\n    )\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SegmentedListItem(\n    selected: Boolean,\n    onClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    colors: ListItemColors = defaultSegmentedColors(),\n    interactionSource: MutableInteractionSource? = null,\n    headlineContent: @Composable () -> Unit,\n    overlineContent: @Composable (() -> Unit)? = null,\n    supportingContent: @Composable (() -> Unit)? = null,\n    leadingContent: @Composable (() -> Unit)? = null,\n    trailingContent: @Composable (() -> Unit)? = null,\n    onLongClick: (() -> Unit)? = null,\n) {\n    SegmentedListItem(\n        selected = selected,\n        onClick = onClick,\n        shapes = LocalListItemShapes.current ?: ListItemDefaults.segmentedShapes(0, 1),\n        modifier = modifier,\n        enabled = enabled,\n        colors = colors,\n        interactionSource = interactionSource,\n        leadingContent = leadingContent,\n        trailingContent = trailingContent,\n        overlineContent = overlineContent,\n        supportingContent = supportingContent,\n        verticalAlignment = Alignment.CenterVertically,\n        onLongClick = onLongClick,\n        content = headlineContent\n    )\n}\n\n@Composable\nfun SegmentedSwitchItem(\n    icon: ImageVector? = null,\n    title: String,\n    summary: String? = null,\n    colors: ListItemColors = defaultSegmentedColors(),\n    checked: Boolean,\n    enabled: Boolean = true,\n    onCheckedChange: (Boolean) -> Unit,\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n\n    SegmentedListItem(\n        onClick = { onCheckedChange(!checked) },\n        enabled = enabled,\n        interactionSource = interactionSource,\n        colors = colors,\n        headlineContent = { Text(title) },\n        leadingContent = icon?.let { { Icon(it, title) } },\n        trailingContent = {\n            ExpressiveSwitch(\n                checked = checked,\n                enabled = enabled,\n                onCheckedChange = null,\n                interactionSource = interactionSource,\n            )\n        },\n        supportingContent = summary?.let { { Text(it) } }\n    )\n}\n\n@Composable\nfun SegmentedDropdownItem(\n    icon: ImageVector? = null,\n    title: String,\n    summary: String? = null,\n    items: List<String>,\n    colors: ListItemColors = defaultSegmentedColors(),\n    enabled: Boolean = true,\n    selectedIndex: Int,\n    onItemSelected: (Int) -> Unit,\n) {\n    var expanded by remember { mutableStateOf(false) }\n\n    val hasItems = items.isNotEmpty()\n    val safeIndex = if (hasItems) {\n        selectedIndex.coerceIn(0, items.lastIndex)\n    } else {\n        -1\n    }\n\n    SegmentedListItem(\n        onClick = if (enabled) {\n            { expanded = true }\n        } else null,\n        enabled = enabled,\n        colors = colors,\n        leadingContent = icon?.let { { Icon(it, title) } },\n        headlineContent = { Text(text = title) },\n        supportingContent = summary?.let { { Text(it) } },\n        trailingContent = {\n            Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {\n                Text(\n                    text = if (hasItems && safeIndex >= 0) items[safeIndex] else \"\",\n                    textAlign = TextAlign.End,\n                    modifier = Modifier.fillMaxWidth(0.3f),\n                    color = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant\n                )\n                DropdownMenu(\n                    expanded = expanded,\n                    onDismissRequest = { expanded = false }\n                ) {\n                    items.forEachIndexed { index, text ->\n                        DropdownMenuItem(\n                            text = { Text(text) },\n                            onClick = {\n                                if (index in items.indices) {\n                                    onItemSelected(index)\n                                }\n                                expanded = false\n                            }\n                        )\n                    }\n                }\n            }\n        }\n    )\n}\n\n@Composable\nfun SegmentedRadioItem(\n    title: String,\n    summary: String? = null,\n    colors: ListItemColors = defaultSegmentedColors(),\n    selected: Boolean,\n    enabled: Boolean = true,\n    onClick: () -> Unit,\n) {\n    SegmentedListItem(\n        selected = selected,\n        onClick = onClick,\n        enabled = enabled,\n        colors = colors,\n        headlineContent = { Text(title) },\n        leadingContent = {\n            RadioButton(\n                selected = selected,\n                onClick = null,\n                enabled = enabled\n            )\n        },\n        supportingContent = summary?.let { { Text(it) } }\n    )\n}\n\n@Composable\nfun SegmentedCheckboxItem(\n    title: String,\n    summary: String? = null,\n    colors: ListItemColors = defaultSegmentedColors(),\n    checked: Boolean,\n    enabled: Boolean = true,\n    onCheckedChange: (Boolean) -> Unit,\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n\n    SegmentedListItem(\n        checked = checked,\n        onCheckedChange = onCheckedChange,\n        enabled = enabled,\n        colors = colors,\n        interactionSource = interactionSource,\n        headlineContent = { Text(title) },\n        leadingContent = {\n            Checkbox(\n                checked = checked,\n                enabled = enabled,\n                onCheckedChange = null,\n                interactionSource = interactionSource,\n                modifier = Modifier.size(24.dp)\n            )\n        },\n        supportingContent = summary?.let { { Text(it) } }\n    )\n}\n\n@Composable\nfun SegmentedTextField(\n    modifier: Modifier = Modifier,\n    label: String = \"\",\n    value: String,\n    onValueChange: (String) -> Unit,\n    enabled: Boolean = true,\n    readOnly: Boolean = false,\n    colors: ListItemColors = defaultSegmentedColors(),\n    textStyle: TextStyle = MaterialTheme.typography.bodyLarge,\n    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,\n    keyboardActions: KeyboardActions = KeyboardActions.Default,\n    singleLine: Boolean = false,\n    maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,\n    minLines: Int = 1,\n    visualTransformation: VisualTransformation = VisualTransformation.None,\n    onTextLayout: (TextLayoutResult) -> Unit = {},\n    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },\n    cursorBrush: Brush = SolidColor(MaterialTheme.colorScheme.primary),\n    placeholder: @Composable (() -> Unit)? = { Text(\"-\") },\n    leadingContent: @Composable (() -> Unit)? = null,\n    trailingContent: @Composable (() -> Unit)? = null,\n    supportingContent: @Composable (() -> Unit)? = null,\n    isError: Boolean = false\n) {\n    val bringIntoViewRequester = remember { BringIntoViewRequester() }\n    val coroutineScope = rememberCoroutineScope()\n    val focusRequester = remember { FocusRequester() }\n\n    SegmentedListItem(\n        modifier = modifier\n            .bringIntoViewRequester(bringIntoViewRequester)\n            .focusRequester(focusRequester),\n        colors = colors,\n        onClick = { focusRequester.requestFocus() },\n        leadingContent = leadingContent,\n        supportingContent = supportingContent,\n        trailingContent = trailingContent,\n        headlineContent = {\n            Column {\n                if (label.isNotEmpty()) {\n                    Text(text = label, color = if (isError) MaterialTheme.colorScheme.error else colors.contentColor)\n                }\n                BasicTextField(\n                    value = value,\n                    onValueChange = onValueChange,\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .focusRequester(focusRequester)\n                        .onFocusChanged {\n                            if (it.isFocused) {\n                                coroutineScope.launch {\n                                    bringIntoViewRequester.bringIntoView()\n                                }\n                            }\n                        },\n                    enabled = enabled,\n                    readOnly = readOnly,\n                    textStyle = textStyle.copy(\n                        colors.supportingContentColor,\n                        fontSize = MaterialTheme.typography.bodyMedium.fontSize,\n                        lineHeight = MaterialTheme.typography.bodyMedium.lineHeight\n                    ),\n                    keyboardOptions = keyboardOptions,\n                    keyboardActions = keyboardActions,\n                    singleLine = singleLine,\n                    maxLines = maxLines,\n                    minLines = minLines,\n                    visualTransformation = visualTransformation,\n                    onTextLayout = onTextLayout,\n                    interactionSource = interactionSource,\n                    cursorBrush = cursorBrush,\n                    decorationBox = { innerTextField ->\n                        if (value.isEmpty() && placeholder != null) {\n                            Box(contentAlignment = Alignment.CenterStart) {\n                                CompositionLocalProvider(\n                                    LocalContentColor provides colors.supportingContentColor\n                                ) {\n                                    ProvideTextStyle(value = MaterialTheme.typography.bodyMedium) {\n                                        placeholder()\n                                    }\n                                }\n                            }\n                        }\n                        innerTextField()\n                    }\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/material/SendLogBottomSheet.kt",
    "content": "package me.weishu.kernelsu.ui.component.material\n\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material.icons.filled.Share\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.FilledTonalIconButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.hapticfeedback.HapticFeedbackType\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalHapticFeedback\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.LineHeightStyle\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport androidx.core.content.FileProvider\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.BuildConfig\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.dialog.rememberLoadingDialog\nimport me.weishu.kernelsu.ui.util.LocalSnackbarHost\nimport me.weishu.kernelsu.ui.util.getBugreportFile\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SendLogBottomSheet(onDismiss: () -> Unit) {\n    val context = LocalContext.current\n    val logSaved = stringResource(R.string.log_saved)\n    val sendLog = stringResource(R.string.send_log)\n    val snackBarHost = LocalSnackbarHost.current\n    val loadingDialog = rememberLoadingDialog()\n    val haptic = LocalHapticFeedback.current\n    val sheetState = rememberModalBottomSheetState()\n    val scope = rememberCoroutineScope()\n    val dismiss = {\n        scope.launch { sheetState.hide() }\n        onDismiss()\n    }\n\n    val exportBugreportLauncher = rememberLauncherForActivityResult(\n        ActivityResultContracts.CreateDocument(\"application/gzip\")\n    ) { uri: Uri? ->\n        if (uri == null) return@rememberLauncherForActivityResult\n        scope.launch(Dispatchers.IO) {\n            loadingDialog.show()\n            context.contentResolver.openOutputStream(uri)?.use { output ->\n                getBugreportFile(context).inputStream().use {\n                    it.copyTo(output)\n                }\n            }\n            loadingDialog.hide()\n            snackBarHost.currentSnackbarData?.dismiss()\n            snackBarHost.showSnackbar(logSaved)\n        }\n    }\n    ModalBottomSheet(\n        onDismissRequest = { dismiss() },\n        containerColor = MaterialTheme.colorScheme.surfaceContainer,\n        content = {\n            Row(\n                modifier = Modifier\n                    .padding(top = 16.dp, bottom = 32.dp)\n                    .align(Alignment.CenterHorizontally)\n            ) {\n                Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                    FilledTonalIconButton(\n                        modifier = Modifier.size(64.dp),\n                        onClick = {\n                            haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)\n                            val formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd_HH_mm\")\n                            val current = LocalDateTime.now().format(formatter)\n                            exportBugreportLauncher.launch(\"KernelSU_bugreport_${current}.tar.gz\")\n                            dismiss()\n                        }) {\n                        Icon(\n                            Icons.Filled.Save,\n                            contentDescription = stringResource(id = R.string.save_log),\n                            modifier = Modifier.align(Alignment.CenterHorizontally)\n                        )\n                    }\n                    Spacer(modifier = Modifier.height(8.dp))\n                    Text(\n                        text = stringResource(id = R.string.save_log), textAlign = TextAlign.Center.also {\n                            LineHeightStyle(\n                                alignment = LineHeightStyle.Alignment.Center, trim = LineHeightStyle.Trim.None\n                            )\n                        }\n                    )\n                }\n                Spacer(Modifier.width(16.dp))\n                Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                    FilledTonalIconButton(\n                        modifier = Modifier.size(64.dp),\n                        onClick = {\n                            haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)\n                            scope.launch {\n                                val bugreport = loadingDialog.withLoading {\n                                    withContext(Dispatchers.IO) {\n                                        getBugreportFile(context)\n                                    }\n                                }\n\n                                val uri: Uri = FileProvider.getUriForFile(\n                                    context, \"${BuildConfig.APPLICATION_ID}.fileprovider\", bugreport\n                                )\n\n                                val shareIntent = Intent(Intent.ACTION_SEND).apply {\n                                    putExtra(Intent.EXTRA_STREAM, uri)\n                                    setDataAndType(uri, \"application/gzip\")\n                                    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                                }\n\n                                context.startActivity(\n                                    Intent.createChooser(\n                                        shareIntent, sendLog\n                                    )\n                                )\n                            }\n                        }) {\n                        Icon(\n                            Icons.Filled.Share,\n                            contentDescription = stringResource(id = R.string.send_log),\n                            modifier = Modifier.align(Alignment.CenterHorizontally)\n                        )\n                    }\n                    Spacer(modifier = Modifier.height(8.dp))\n                    Text(\n                        text = stringResource(id = R.string.send_log), textAlign = TextAlign.Center.also {\n                            LineHeightStyle(\n                                alignment = LineHeightStyle.Alignment.Center, trim = LineHeightStyle.Trim.None\n                            )\n                        })\n                }\n            }\n        })\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/material/SettingsItem.kt",
    "content": "package me.weishu.kernelsu.ui.component.material\n\nimport androidx.compose.foundation.LocalIndication\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.selection.toggleable\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.semantics.Role\n\n@Composable\nfun SwitchItem(\n    icon: ImageVector? = null,\n    title: String,\n    summary: String? = null,\n    checked: Boolean,\n    enabled: Boolean = true,\n    onCheckedChange: (Boolean) -> Unit\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n\n    ListItem(\n        modifier = Modifier\n            .toggleable(\n                value = checked,\n                interactionSource = interactionSource,\n                role = Role.Switch,\n                enabled = enabled,\n                indication = LocalIndication.current,\n                onValueChange = onCheckedChange\n            ),\n        headlineContent = {\n            Text(title)\n        },\n        leadingContent = icon?.let {\n            { Icon(icon, title) }\n        },\n        trailingContent = {\n            ExpressiveSwitch(\n                checked = checked,\n                enabled = enabled,\n                onCheckedChange = onCheckedChange,\n                interactionSource = interactionSource\n            )\n        },\n        supportingContent = {\n            if (summary != null) {\n                Text(summary, color = MaterialTheme.colorScheme.outline)\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/DropdownItem.kt",
    "content": "package me.weishu.kernelsu.ui.component.miuix\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport top.yukonga.miuix.kmp.basic.DropdownColors\nimport top.yukonga.miuix.kmp.basic.DropdownDefaults\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\n\n@Composable\nfun DropdownItem(\n    text: String,\n    optionSize: Int,\n    index: Int,\n    dropdownColors: DropdownColors = DropdownDefaults.dropdownColors(),\n    onSelectedIndexChange: (Int) -> Unit\n) {\n    val currentOnSelectedIndexChange = rememberUpdatedState(onSelectedIndexChange)\n    val additionalTopPadding = if (index == 0) 20f.dp else 12f.dp\n    val additionalBottomPadding = if (index == optionSize - 1) 20f.dp else 12f.dp\n\n    Row(\n        modifier = Modifier\n            .clickable { currentOnSelectedIndexChange.value(index) }\n            .background(dropdownColors.containerColor)\n            .padding(horizontal = 20.dp)\n            .padding(\n                top = additionalTopPadding,\n                bottom = additionalBottomPadding\n            ),\n        verticalAlignment = Alignment.CenterVertically\n    ) {\n        Text(\n            text = text,\n            fontSize = MiuixTheme.textStyles.body1.fontSize,\n            fontWeight = FontWeight.Medium,\n            color = dropdownColors.contentColor,\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/EditText.kt",
    "content": "package me.weishu.kernelsu.ui.component.miuix\n\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.interaction.FocusInteraction\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.interaction.collectIsFocusedAsState\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.text.BasicTextField\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.Immutable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.SolidColor\nimport androidx.compose.ui.layout.Layout\nimport androidx.compose.ui.semantics.onClick\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.launch\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport kotlin.math.max\n\n@Composable\nfun EditText(\n    title: String,\n    summary: String? = null,\n    textValue: MutableState<String>,\n    onTextValueChange: (String) -> Unit = {},\n    textHint: String = \"\",\n    enabled: Boolean = true,\n    keyboardOptions: KeyboardOptions = KeyboardOptions.Default,\n    titleColor: BasicComponentColors = EditTextDefaults.titleColor(),\n    summaryColor: BasicComponentColors = EditTextDefaults.summaryColor(),\n    rightActionColor: BasicComponentColors = EditTextDefaults.rightActionColors(),\n    isError: Boolean = false,\n) {\n    val interactionSource = remember { MutableInteractionSource() }\n    val coroutineScope = rememberCoroutineScope()\n    val focused = interactionSource.collectIsFocusedAsState().value\n    val focusRequester = remember { FocusRequester() }\n    if (focused) {\n        focusRequester.requestFocus()\n    }\n\n    Box(\n        modifier = Modifier\n            .clickable(\n                indication = null,\n                interactionSource = null\n            ) {\n                if (enabled) {\n                    coroutineScope.launch {\n                        interactionSource.emit(FocusInteraction.Focus())\n                    }\n                }\n            }\n            .heightIn(min = 56.dp)\n            .fillMaxWidth()\n            .padding(EditTextDefaults.InsideMargin),\n    ) {\n        Layout(\n            content = {\n                Text(\n                    text = title,\n                    fontSize = MiuixTheme.textStyles.headline1.fontSize,\n                    fontWeight = FontWeight.Medium,\n                    color = titleColor.color(enabled)\n                )\n                summary?.let {\n                    Text(\n                        text = it,\n                        fontSize = MiuixTheme.textStyles.body2.fontSize,\n                        color = summaryColor.color(enabled)\n                    )\n                }\n                BasicTextField(\n                    value = textValue.value,\n                    onValueChange = {\n                        onTextValueChange(it)\n                    },\n                    modifier = Modifier\n                        .focusRequester(focusRequester)\n                        .semantics {\n                            onClick {\n                                focusRequester.requestFocus()\n                                true\n                            }\n                        },\n                    enabled = enabled,\n                    textStyle = MiuixTheme.textStyles.main.copy(\n                        textAlign = TextAlign.End,\n                        color = if (isError) {\n                            Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f)\n                        } else {\n                            rightActionColor.color(enabled)\n                        }\n                    ),\n                    keyboardOptions = keyboardOptions,\n                    cursorBrush = SolidColor(colorScheme.primary),\n                    interactionSource = interactionSource,\n                    decorationBox =\n                        @Composable { innerTextField ->\n                            Box(\n                                contentAlignment = Alignment.CenterEnd\n                            ) {\n                                Text(\n                                    text = if (textValue.value.isEmpty()) textHint else \"\",\n                                    color = rightActionColor.color(enabled),\n                                    textAlign = TextAlign.End,\n                                    softWrap = false,\n                                    maxLines = 1\n                                )\n                                innerTextField()\n                            }\n                        }\n                )\n            }\n        ) { measurables, constraints ->\n            val leftConstraints = constraints.copy(maxWidth = constraints.maxWidth / 2)\n            val hasSummary = measurables.size > 2\n            val titleText = measurables[0].measure(leftConstraints)\n            val summaryText = (if (hasSummary) measurables[1] else null)?.measure(leftConstraints)\n            val leftWidth = max(titleText.width, (summaryText?.width ?: 0))\n            val leftHeight = titleText.height + (summaryText?.height ?: 0)\n            val rightWidth = constraints.maxWidth - leftWidth - 16.dp.roundToPx()\n            val rightConstraints = constraints.copy(maxWidth = rightWidth)\n            val inputField = (if (hasSummary) measurables[2] else measurables[1]).measure(rightConstraints)\n            val totalHeight = max(leftHeight, inputField.height)\n            layout(constraints.maxWidth, totalHeight) {\n                val titleY = (totalHeight - leftHeight) / 2\n                titleText.placeRelative(0, titleY)\n                summaryText?.placeRelative(0, titleY + titleText.height)\n                inputField.placeRelative(constraints.maxWidth - inputField.width, (totalHeight - inputField.height) / 2)\n            }\n        }\n    }\n}\n\nobject EditTextDefaults {\n    val InsideMargin = PaddingValues(16.dp)\n\n    @Composable\n    fun titleColor(\n        color: Color = colorScheme.onSurface,\n        disabledColor: Color = colorScheme.disabledOnSecondaryVariant\n    ): BasicComponentColors {\n        return BasicComponentColors(\n            color = color,\n            disabledColor = disabledColor\n        )\n    }\n\n    @Composable\n    fun summaryColor(\n        color: Color = colorScheme.onSurfaceVariantSummary,\n        disabledColor: Color = colorScheme.disabledOnSecondaryVariant\n    ): BasicComponentColors {\n        return BasicComponentColors(\n            color = color,\n            disabledColor = disabledColor\n        )\n    }\n\n    @Composable\n    fun rightActionColors(\n        color: Color = colorScheme.onSurfaceVariantActions,\n        disabledColor: Color = colorScheme.disabledOnSecondaryVariant,\n    ): BasicComponentColors {\n        return BasicComponentColors(\n            color = color,\n            disabledColor = disabledColor\n        )\n    }\n}\n\n@Immutable\nclass BasicComponentColors(\n    private val color: Color,\n    private val disabledColor: Color\n) {\n    @Stable\n    fun color(enabled: Boolean): Color = if (enabled) color else disabledColor\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/ScaleDialog.kt",
    "content": "package me.weishu.kernelsu.ui.component.miuix\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport top.yukonga.miuix.kmp.basic.ButtonDefaults\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.basic.TextField\nimport top.yukonga.miuix.kmp.extra.SuperDialog\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\n\n@Composable\nfun ScaleDialog(\n    show: Boolean,\n    onDismissRequest: () -> Unit,\n    volumeState: () -> Float,\n    onVolumeChange: (Float) -> Unit,\n) {\n    SuperDialog(\n        show = show,\n        title = stringResource(R.string.settings_page_scale),\n        summary = \"80% - 110%\",\n        onDismissRequest = onDismissRequest,\n        content = {\n            var text by remember(show) {\n                mutableStateOf((volumeState() * 100).toInt().toString())\n            }\n            TextField(\n                modifier = Modifier.padding(bottom = 16.dp),\n                value = text,\n                maxLines = 1,\n                trailingIcon = {\n                    Text(\n                        text = \"%\",\n                        modifier = Modifier.padding(horizontal = 16.dp),\n                        color = colorScheme.onSurfaceVariantActions,\n                    )\n                },\n                onValueChange = { newValue ->\n                    if (newValue.isEmpty()) {\n                        text = \"\"\n                    } else {\n                        val valid = newValue.all { it.isDigit() }\n                        if (valid) {\n                            text = newValue\n                        }\n                    }\n                },\n            )\n            Row(horizontalArrangement = Arrangement.SpaceBetween) {\n                TextButton(\n                    text = stringResource(android.R.string.cancel),\n                    onClick = onDismissRequest,\n                    modifier = Modifier.weight(1f),\n                )\n                Spacer(Modifier.width(20.dp))\n                TextButton(\n                    text = stringResource(android.R.string.ok),\n                    onClick = {\n                        val parsed = text.toIntOrNull()\n                        val clamped = parsed?.coerceIn(80, 110) ?: (volumeState() * 100).toInt()\n                        onVolumeChange(clamped / 100f)\n                        onDismissRequest()\n                    },\n                    modifier = Modifier.weight(1f),\n                    colors = ButtonDefaults.textButtonColorsPrimary(),\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/SendLogDialog.kt",
    "content": "package me.weishu.kernelsu.ui.component.miuix\n\nimport android.content.Intent\nimport android.net.Uri\nimport android.widget.Toast\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Save\nimport androidx.compose.material.icons.rounded.Share\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport androidx.core.content.FileProvider\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.BuildConfig\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.dialog.LoadingDialogHandle\nimport me.weishu.kernelsu.ui.util.getBugreportFile\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.extra.SuperArrow\nimport top.yukonga.miuix.kmp.extra.SuperDialog\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\n\n@Composable\nfun SendLogDialog(\n    show: Boolean,\n    onDismissRequest: () -> Unit,\n    loadingDialog: LoadingDialogHandle,\n) {\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val logSavedText = stringResource(R.string.log_saved)\n    val sendLogText = stringResource(R.string.send_log)\n    val exportBugreportLauncher = rememberLauncherForActivityResult(\n        ActivityResultContracts.CreateDocument(\"application/gzip\")\n    ) { uri: Uri? ->\n        if (uri == null) return@rememberLauncherForActivityResult\n        scope.launch(Dispatchers.IO) {\n            loadingDialog.show()\n            context.contentResolver.openOutputStream(uri)?.use { output ->\n                getBugreportFile(context).inputStream().use {\n                    it.copyTo(output)\n                }\n            }\n            loadingDialog.hide()\n            withContext(Dispatchers.Main) {\n                Toast.makeText(context, logSavedText, Toast.LENGTH_SHORT).show()\n            }\n        }\n    }\n    SuperDialog(\n        show = show,\n        onDismissRequest = onDismissRequest,\n        insideMargin = DpSize(0.dp, 0.dp),\n        content = {\n            Text(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = 24.dp, bottom = 12.dp),\n                text = stringResource(R.string.send_log),\n                fontSize = MiuixTheme.textStyles.title4.fontSize,\n                fontWeight = FontWeight.Medium,\n                textAlign = TextAlign.Center,\n                color = colorScheme.onSurface\n            )\n            SuperArrow(\n                title = stringResource(id = R.string.save_log),\n                startAction = {\n                    Icon(\n                        Icons.Rounded.Save,\n                        contentDescription = null,\n                        modifier = Modifier.padding(end = 16.dp),\n                        tint = colorScheme.onSurface\n                    )\n                },\n                onClick = {\n                    val formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd_HH_mm\")\n                    val current = LocalDateTime.now().format(formatter)\n                    exportBugreportLauncher.launch(\"KernelSU_bugreport_${current}.tar.gz\")\n                    onDismissRequest()\n                },\n                insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp)\n            )\n            SuperArrow(\n                title = stringResource(id = R.string.send_log),\n                startAction = {\n                    Icon(\n                        Icons.Rounded.Share,\n                        contentDescription = null,\n                        modifier = Modifier.padding(end = 16.dp),\n                        tint = colorScheme.onSurface\n                    )\n                },\n                onClick = {\n                    scope.launch {\n                        onDismissRequest()\n                        val bugreport = loadingDialog.withLoading {\n                            withContext(Dispatchers.IO) {\n                                getBugreportFile(context)\n                            }\n                        }\n\n                        val uri: Uri =\n                            FileProvider.getUriForFile(\n                                context,\n                                \"${BuildConfig.APPLICATION_ID}.fileprovider\",\n                                bugreport\n                            )\n\n                        val shareIntent = Intent(Intent.ACTION_SEND).apply {\n                            putExtra(Intent.EXTRA_STREAM, uri)\n                            setDataAndType(uri, \"application/gzip\")\n                            addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)\n                        }\n\n                        context.startActivity(\n                            Intent.createChooser(\n                                shareIntent,\n                                sendLogText\n                            )\n                        )\n                    }\n                },\n                insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp)\n            )\n            TextButton(\n                text = stringResource(id = android.R.string.cancel),\n                onClick = {\n                    onDismissRequest()\n                },\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = 12.dp, bottom = 24.dp)\n                    .padding(horizontal = 24.dp)\n            )\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/SuperEditArrow.kt",
    "content": "package me.weishu.kernelsu.ui.component.miuix\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.filter.FilterNumber\nimport top.yukonga.miuix.kmp.basic.BasicComponentColors\nimport top.yukonga.miuix.kmp.basic.BasicComponentDefaults\nimport top.yukonga.miuix.kmp.basic.ButtonDefaults\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.basic.TextField\nimport top.yukonga.miuix.kmp.extra.SuperArrow\nimport top.yukonga.miuix.kmp.extra.SuperDialog\n\n@Composable\nfun SuperEditArrow(\n    modifier: Modifier = Modifier,\n    title: String,\n    titleColor: BasicComponentColors = BasicComponentDefaults.titleColor(),\n    defaultValue: Int = -1,\n    summaryColor: BasicComponentColors = BasicComponentDefaults.summaryColor(),\n    startAction: @Composable (() -> Unit)? = null,\n    insideMargin: PaddingValues = BasicComponentDefaults.InsideMargin,\n    enabled: Boolean = true,\n    onValueChange: ((Int) -> Unit)? = null\n) {\n    val showDialog = remember { mutableStateOf(false) }\n    val dialogTextFieldValue = remember { mutableIntStateOf(defaultValue) }\n\n    SuperArrow(\n        title = title,\n        titleColor = titleColor,\n        summary = dialogTextFieldValue.intValue.toString(),\n        summaryColor = summaryColor,\n        startAction = startAction,\n        modifier = modifier,\n        insideMargin = insideMargin,\n        onClick = {\n            showDialog.value = true\n        },\n        holdDownState = showDialog.value,\n        enabled = enabled\n    )\n\n    EditDialog(\n        title = title,\n        show = showDialog.value,\n        onDismissRequest = { showDialog.value = false },\n        dialogTextFieldValue = dialogTextFieldValue.intValue,\n        onValueChange = {\n            dialogTextFieldValue.intValue = it\n            onValueChange?.invoke(dialogTextFieldValue.intValue)\n        }\n    )\n\n}\n\n@Composable\nprivate fun EditDialog(\n    title: String,\n    show: Boolean,\n    onDismissRequest: () -> Unit,\n    dialogTextFieldValue: Int,\n    onValueChange: (Int) -> Unit,\n) {\n    val inputTextFieldValue = remember { mutableIntStateOf(dialogTextFieldValue) }\n    val filter = remember(key1 = inputTextFieldValue.intValue) { FilterNumber(dialogTextFieldValue) }\n\n    SuperDialog(\n        show = show,\n        title = title,\n        onDismissRequest = {\n            onDismissRequest()\n            filter.setInputValue(dialogTextFieldValue.toString())\n        },\n        content = {\n            TextField(\n                modifier = Modifier.padding(bottom = 16.dp),\n                value = filter.getInputValue(),\n                maxLines = 1,\n                keyboardOptions = KeyboardOptions(\n                    keyboardType = KeyboardType.Number,\n                ),\n                onValueChange = filter.onValueChange()\n            )\n            Row(\n                horizontalArrangement = Arrangement.SpaceBetween\n            ) {\n                TextButton(\n                    text = stringResource(android.R.string.cancel),\n                    onClick = {\n                        onDismissRequest()\n                        filter.setInputValue(dialogTextFieldValue.toString())\n                    },\n                    modifier = Modifier.weight(1f)\n                )\n                Spacer(Modifier.width(20.dp))\n                TextButton(\n                    text = stringResource(R.string.confirm),\n                    onClick = {\n                        onDismissRequest()\n                        with(filter.getInputValue().text) {\n                            if (isEmpty()) {\n                                onValueChange(0)\n                                filter.setInputValue(\"0\")\n                            } else {\n                                onValueChange(this@with.toInt())\n                            }\n\n                        }\n                    },\n                    modifier = Modifier.weight(1f),\n                    colors = ButtonDefaults.textButtonColorsPrimary()\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/SuperSearchBar.kt",
    "content": "package me.weishu.kernelsu.ui.component.miuix\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.LinearOutSlowInEasing\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideInVertically\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.animation.slideOutVertically\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.gestures.detectTapGestures\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.alpha\nimport androidx.compose.ui.draw.drawBehind\nimport androidx.compose.ui.focus.FocusRequester\nimport androidx.compose.ui.focus.focusRequester\nimport androidx.compose.ui.input.pointer.pointerInput\nimport androidx.compose.ui.layout.onGloballyPositioned\nimport androidx.compose.ui.layout.positionInWindow\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.semantics.onClick\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.max\nimport androidx.compose.ui.zIndex\nimport androidx.navigationevent.NavigationEventInfo\nimport androidx.navigationevent.compose.NavigationBackHandler\nimport androidx.navigationevent.compose.rememberNavigationEventState\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport me.weishu.kernelsu.ui.component.SearchStatus\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.InputField\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.basic.Search\nimport top.yukonga.miuix.kmp.icon.basic.SearchCleanup\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\n\n// Search Box Composable\n@Composable\nfun SearchStatus.SearchBox(\n    onSearchStatusChange: (SearchStatus) -> Unit,\n    collapseBar: @Composable (SearchStatus, Dp, PaddingValues) -> Unit = { searchStatus, topPadding, innerPadding ->\n        SearchBarFake(searchStatus.label, topPadding, innerPadding)\n    },\n    searchBarTopPadding: Dp = 12.dp,\n    contentPadding: PaddingValues = PaddingValues(0.dp),\n    hazeState: HazeState? = null,\n    hazeStyle: HazeStyle? = null,\n    content: @Composable (MutableState<Dp>) -> Unit\n) {\n    val searchStatus = this\n    val density = LocalDensity.current\n\n    val offsetY = remember { mutableIntStateOf(0) }\n    val boxHeight = remember { mutableStateOf(0.dp) }\n\n    Box(\n        modifier = Modifier\n            .fillMaxWidth()\n            .zIndex(10f)\n            .alpha(if (searchStatus.isCollapsed()) 1f else 0f)\n            .offset(y = contentPadding.calculateTopPadding())\n            .onGloballyPositioned {\n                it.positionInWindow().y.apply {\n                    offsetY.intValue = (this@apply * 0.9).toInt()\n                    with(density) {\n                        val newOffsetY = this@apply.toDp()\n                        val newBoxHeight = it.size.height.toDp()\n                        if (searchStatus.offsetY != newOffsetY) {\n                            onSearchStatusChange(searchStatus.copy(offsetY = newOffsetY))\n                        }\n                        boxHeight.value = newBoxHeight\n                    }\n                }\n            }\n            .pointerInput(Unit) {\n                detectTapGestures { onSearchStatusChange(searchStatus.copy(current = SearchStatus.Status.EXPANDING)) }\n            }\n            .then(\n                if (hazeState != null && hazeStyle != null) {\n                    Modifier.defaultHazeEffect(hazeState, hazeStyle)\n                } else {\n                    Modifier.background(colorScheme.surface)\n                }\n            )\n    ) {\n        collapseBar(searchStatus, searchBarTopPadding, contentPadding)\n    }\n    Box {\n        AnimatedVisibility(\n            visible = searchStatus.shouldCollapsed(),\n            enter = fadeIn(tween(300, easing = LinearOutSlowInEasing)) + slideInVertically(\n                tween(\n                    300,\n                    easing = LinearOutSlowInEasing\n                )\n            ) { -offsetY.intValue },\n            exit = fadeOut(tween(300, easing = LinearOutSlowInEasing)) + slideOutVertically(\n                tween(\n                    300,\n                    easing = LinearOutSlowInEasing\n                )\n            ) { -offsetY.intValue }\n        ) {\n            content(boxHeight)\n        }\n    }\n}\n\n// Search Pager Composable\n@Composable\nfun SearchStatus.SearchPager(\n    onSearchStatusChange: (SearchStatus) -> Unit,\n    defaultResult: @Composable () -> Unit,\n    expandBar: @Composable (SearchStatus, (SearchStatus) -> Unit, Dp) -> Unit = { searchStatus, onStatusChange, padding ->\n        SearchBar(searchStatus, onStatusChange, padding)\n    },\n    searchBarTopPadding: Dp = 12.dp,\n    result: @Composable () -> Unit\n) {\n    val searchStatus = this\n    val systemBarsPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding()\n    val topPadding by animateDpAsState(\n        targetValue = if (searchStatus.shouldExpand()) {\n            systemBarsPadding + 5.dp\n        } else {\n            max(searchStatus.offsetY, 0.dp)\n        },\n        animationSpec = tween(300, easing = LinearOutSlowInEasing),\n        label = \"SearchPagerTopPadding\"\n    ) {\n        onSearchStatusChange(searchStatus.onAnimationComplete())\n    }\n    val surfaceAlpha by animateFloatAsState(\n        if (searchStatus.shouldExpand()) 1f else 0f,\n        animationSpec = tween(200, easing = FastOutSlowInEasing),\n        label = \"SearchPagerSurfaceAlpha\"\n    )\n    val surfaceColor = colorScheme.surface\n\n    Column(\n        modifier = Modifier\n            .fillMaxSize()\n            .zIndex(5f)\n            .drawBehind { drawRect(surfaceColor.copy(alpha = surfaceAlpha)) }\n            .semantics { onClick { false } }\n            .then(\n                if (!searchStatus.isCollapsed()) Modifier.pointerInput(Unit) { } else Modifier\n            )\n    ) {\n        Row(\n            Modifier\n                .fillMaxWidth()\n                .padding(top = topPadding)\n                .then(\n                    if (!searchStatus.isCollapsed()) Modifier.background(colorScheme.surface)\n                    else Modifier\n                ),\n            horizontalArrangement = Arrangement.Start,\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            if (!searchStatus.isCollapsed()) {\n                Box(\n                    modifier = Modifier\n                        .weight(1f)\n                        .background(colorScheme.surface)\n                ) {\n                    expandBar(searchStatus, onSearchStatusChange, searchBarTopPadding)\n                }\n            }\n            AnimatedVisibility(\n                visible = searchStatus.isExpand() || searchStatus.isAnimatingExpand(),\n                enter = expandHorizontally() + slideInHorizontally(initialOffsetX = { it }),\n                exit = shrinkHorizontally() + slideOutHorizontally(targetOffsetX = { it })\n            ) {\n                Text(\n                    text = stringResource(android.R.string.cancel),\n                    fontWeight = FontWeight.Bold,\n                    color = colorScheme.primary,\n                    modifier = Modifier\n                        .padding(start = 4.dp, end = 16.dp, top = searchBarTopPadding)\n                        .clickable(\n                            interactionSource = null,\n                            enabled = searchStatus.isExpand(),\n                            indication = null\n                        ) {\n                            onSearchStatusChange(\n                                searchStatus.copy(\n                                    searchText = \"\",\n                                    current = SearchStatus.Status.COLLAPSING\n                                )\n                            )\n                        }\n                )\n                run {\n                    val navEventState = rememberNavigationEventState(NavigationEventInfo.None)\n                    NavigationBackHandler(\n                        state = navEventState,\n                        isBackEnabled = true,\n                        onBackCompleted = {\n                            onSearchStatusChange(\n                                searchStatus.copy(\n                                    searchText = \"\",\n                                    current = SearchStatus.Status.COLLAPSING\n                                )\n                            )\n                        }\n                    )\n                }\n            }\n        }\n        AnimatedVisibility(\n            visible = searchStatus.isExpand(),\n            modifier = Modifier\n                .fillMaxSize()\n                .zIndex(1f),\n            enter = fadeIn(),\n            exit = fadeOut()\n        ) {\n            when (searchStatus.resultStatus) {\n                SearchStatus.ResultStatus.DEFAULT -> defaultResult()\n                SearchStatus.ResultStatus.EMPTY -> {}\n                SearchStatus.ResultStatus.LOAD -> {}\n                SearchStatus.ResultStatus.SHOW -> result()\n            }\n        }\n    }\n}\n\n@Composable\nfun SearchBar(\n    searchStatus: SearchStatus,\n    onSearchStatusChange: (SearchStatus) -> Unit,\n    searchBarTopPadding: Dp = 12.dp,\n) {\n    val focusRequester = remember { FocusRequester() }\n    var expanded by rememberSaveable { mutableStateOf(false) }\n\n    InputField(\n        query = searchStatus.searchText,\n        onQueryChange = { onSearchStatusChange(searchStatus.copy(searchText = it)) },\n        label = \"\",\n        leadingIcon = {\n            Icon(\n                imageVector = MiuixIcons.Basic.Search,\n                contentDescription = \"back\",\n                modifier = Modifier\n                    .size(44.dp)\n                    .padding(start = 16.dp, end = 8.dp),\n                tint = colorScheme.onSurfaceContainerHigh,\n            )\n        },\n        trailingIcon = {\n            AnimatedVisibility(\n                searchStatus.searchText.isNotEmpty(),\n                enter = fadeIn() + scaleIn(),\n                exit = fadeOut() + scaleOut(),\n            ) {\n                Icon(\n                    imageVector = MiuixIcons.Basic.SearchCleanup,\n                    tint = colorScheme.onSurface,\n                    contentDescription = \"Clean\",\n                    modifier = Modifier\n                        .size(44.dp)\n                        .padding(start = 8.dp, end = 16.dp)\n                        .clickable(\n                            interactionSource = null,\n                            indication = null\n                        ) {\n                            onSearchStatusChange(searchStatus.copy(searchText = \"\"))\n                        },\n                )\n            }\n        },\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 12.dp)\n            .padding(top = searchBarTopPadding, bottom = 6.dp)\n            .focusRequester(focusRequester),\n        onSearch = { },\n        expanded = searchStatus.shouldExpand(),\n        onExpandedChange = {\n            onSearchStatusChange(\n                searchStatus.copy(\n                    current = if (it) SearchStatus.Status.EXPANDED else SearchStatus.Status.COLLAPSED\n                )\n            )\n        }\n    )\n    LaunchedEffect(Unit) {\n        if (!expanded && searchStatus.shouldExpand()) {\n            focusRequester.requestFocus()\n            expanded = true\n        }\n    }\n}\n\n@Composable\nfun SearchBarFake(\n    label: String,\n    searchBarTopPadding: Dp = 12.dp,\n    innerPadding: PaddingValues = PaddingValues(0.dp)\n) {\n    val layoutDirection = LocalLayoutDirection.current\n    val enableBlur = LocalEnableBlur.current\n    InputField(\n        query = \"\",\n        onQueryChange = { },\n        label = label,\n        leadingIcon = {\n            Icon(\n                imageVector = MiuixIcons.Basic.Search,\n                contentDescription = \"Clean\",\n                modifier = Modifier\n                    .size(44.dp)\n                    .padding(start = 16.dp, end = 8.dp),\n                tint = colorScheme.onSurfaceContainerHigh,\n            )\n        },\n        modifier = Modifier\n            .let { if (!enableBlur) it.background(colorScheme.surface) else it }\n            .fillMaxWidth()\n            .padding(horizontal = 12.dp)\n            .padding(\n                start = innerPadding.calculateStartPadding(layoutDirection),\n                end = innerPadding.calculateEndPadding(layoutDirection)\n            )\n            .padding(top = searchBarTopPadding, bottom = 6.dp),\n        onSearch = { },\n        enabled = false,\n        expanded = false,\n        onExpandedChange = { }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfigMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.component.profile\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedSwitchItem\nimport me.weishu.kernelsu.ui.component.material.SegmentedTextField\n\n@Composable\nfun AppProfileConfigMaterial(\n    modifier: Modifier = Modifier,\n    fixedName: Boolean,\n    enabled: Boolean,\n    profile: Natives.Profile,\n    onProfileChange: (Natives.Profile) -> Unit,\n) {\n    Column(modifier = modifier) {\n        if (!fixedName) {\n            SegmentedColumn(\n                modifier = Modifier.padding(horizontal = 16.dp),\n                content = listOf {\n                    SegmentedTextField(\n                        value = profile.name,\n                        onValueChange = { onProfileChange(profile.copy(name = it)) },\n                        label = stringResource(R.string.profile_name),\n                        readOnly = !enabled,\n                        singleLine = true\n                    )\n                }\n            )\n        }\n\n        SegmentedColumn(\n            modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n            content = listOf {\n                SegmentedSwitchItem(\n                    title = stringResource(R.string.profile_umount_modules),\n                    summary = stringResource(R.string.profile_umount_modules_summary),\n                    checked = if (enabled) {\n                        profile.umountModules\n                    } else {\n                        Natives.isDefaultUmountModules()\n                    },\n                    enabled = enabled,\n                    onCheckedChange = {\n                        onProfileChange(\n                            profile.copy(\n                                umountModules = it,\n                                nonRootUseDefault = false\n                            )\n                        )\n                    }\n                )\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfigMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.component.profile\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.tooling.preview.Preview\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.miuix.EditText\nimport top.yukonga.miuix.kmp.extra.SuperSwitch\n\n@Composable\nfun AppProfileConfigMiuix(\n    modifier: Modifier = Modifier,\n    fixedName: Boolean,\n    enabled: Boolean,\n    profile: Natives.Profile,\n    onProfileChange: (Natives.Profile) -> Unit,\n) {\n    Column(modifier = modifier) {\n        if (!fixedName) {\n            EditText(\n                title = stringResource(R.string.profile_name),\n                textValue = remember { mutableStateOf(profile.name) },\n                onTextValueChange = { onProfileChange(profile.copy(name = it)) },\n                enabled = enabled,\n            )\n        }\n\n        SuperSwitch(\n            title = stringResource(R.string.profile_umount_modules),\n            summary = stringResource(R.string.profile_umount_modules_summary),\n            checked = if (enabled) {\n                profile.umountModules\n            } else {\n                Natives.isDefaultUmountModules()\n            },\n            enabled = enabled,\n            onCheckedChange = {\n                onProfileChange(\n                    profile.copy(\n                        umountModules = it,\n                        nonRootUseDefault = false\n                    )\n                )\n            }\n        )\n    }\n}\n\n@Preview\n@Composable\nprivate fun AppProfileConfigPreview() {\n    var profile by remember { mutableStateOf(Natives.Profile(\"\")) }\n    AppProfileConfigMiuix(fixedName = true, enabled = false, profile = profile) {\n        profile = it\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/ProfileConfig.kt",
    "content": "package me.weishu.kernelsu.ui.component.profile\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\n\n@Composable\nfun AppProfileConfig(\n    modifier: Modifier = Modifier,\n    fixedName: Boolean,\n    enabled: Boolean,\n    profile: Natives.Profile,\n    onProfileChange: (Natives.Profile) -> Unit,\n) {\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> AppProfileConfigMiuix(\n            modifier = modifier,\n            fixedName = fixedName,\n            enabled = enabled,\n            profile = profile,\n            onProfileChange = onProfileChange\n        )\n\n        UiMode.Material -> AppProfileConfigMaterial(\n            modifier = modifier,\n            fixedName = fixedName,\n            enabled = enabled,\n            profile = profile,\n            onProfileChange = onProfileChange\n        )\n    }\n}\n\n@Composable\nfun RootProfileConfig(\n    modifier: Modifier = Modifier,\n    fixedName: Boolean,\n    enabled: Boolean = true,\n    profile: Natives.Profile,\n    onProfileChange: (Natives.Profile) -> Unit,\n) {\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> RootProfileConfigMiuix(\n            modifier = modifier,\n            fixedName = fixedName,\n            enabled = enabled,\n            profile = profile,\n            onProfileChange = onProfileChange\n        )\n\n        UiMode.Material -> RootProfileConfigMaterial(\n            modifier = modifier,\n            enabled = enabled,\n            profile = profile,\n            onProfileChange = onProfileChange\n        )\n    }\n}\n\n@Composable\nfun TemplateConfig(\n    modifier: Modifier = Modifier,\n    profile: Natives.Profile,\n    onViewTemplate: (id: String) -> Unit = {},\n    onManageTemplate: () -> Unit = {},\n    onProfileChange: (Natives.Profile) -> Unit\n) {\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> TemplateConfigMiuix(\n            modifier = modifier,\n            profile = profile,\n            onViewTemplate = onViewTemplate,\n            onManageTemplate = onManageTemplate,\n            onProfileChange = onProfileChange\n        )\n\n        UiMode.Material -> TemplateConfigMaterial(\n            profile = profile,\n            onViewTemplate = onViewTemplate,\n            onManageTemplate = onManageTemplate,\n            onProfileChange = onProfileChange\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfigMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.component.profile\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.profile.Capabilities\nimport me.weishu.kernelsu.profile.Groups\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedListItem\nimport me.weishu.kernelsu.ui.component.material.SegmentedTextField\nimport me.weishu.kernelsu.ui.component.profile.dialogs.MultiSelectDialog\nimport me.weishu.kernelsu.ui.component.profile.dialogs.SingleSelectDialog\nimport me.weishu.kernelsu.ui.util.isSepolicyValid\n\n@Composable\nfun RootProfileConfigMaterial(\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    profile: Natives.Profile,\n    onProfileChange: (Natives.Profile) -> Unit\n) {\n    Column(\n        modifier = modifier.padding(vertical = 8.dp),\n        verticalArrangement = Arrangement.spacedBy(16.dp)\n    ) {\n        UidGidPanel(\n            enabled = enabled,\n            uid = profile.uid,\n            gid = profile.gid,\n            onUidChange = { onProfileChange(profile.copy(uid = it, rootUseDefault = false)) },\n            onGidChange = { onProfileChange(profile.copy(gid = it, rootUseDefault = false)) }\n        )\n\n        GroupsPanel(\n            enabled = enabled,\n            selected = profile.groups.mapNotNull { gid ->\n                Groups.entries.find { it.gid == gid }\n            },\n            onSelectionChange = { selection ->\n                onProfileChange(\n                    profile.copy(\n                        groups = selection.map { it.gid },\n                        rootUseDefault = false\n                    )\n                )\n            }\n        )\n\n        CapsPanel(\n            enabled = enabled,\n            selected = profile.capabilities,\n            onSelectionChange = { selection ->\n                onProfileChange(\n                    profile.copy(\n                        capabilities = selection.map { it.cap },\n                        rootUseDefault = false\n                    )\n                )\n            }\n        )\n\n        MountNameSpacePanel(\n            enabled = enabled,\n            namespace = profile.namespace,\n            onNamespaceChange = { onProfileChange(profile.copy(namespace = it, rootUseDefault = false)) }\n        )\n\n        SELinuxPanel(\n            enabled = enabled,\n            context = profile.context,\n            rules = profile.rules,\n            onContextChange = { domain ->\n                onProfileChange(profile.copy(context = domain, rootUseDefault = false))\n            },\n            onRulesChange = { rules ->\n                onProfileChange(profile.copy(rules = rules, rootUseDefault = false))\n            }\n        )\n    }\n}\n\n@Composable\nprivate fun UidGidPanel(\n    enabled: Boolean,\n    uid: Int,\n    gid: Int,\n    onUidChange: (Int) -> Unit,\n    onGidChange: (Int) -> Unit\n) {\n    SegmentedColumn(\n        modifier = Modifier.padding(horizontal = 16.dp),\n        content = listOf(\n            {\n                SegmentedTextField(\n                    enabled = enabled,\n                    value = uid.toString(),\n                    onValueChange = { onUidChange(it.toIntOrNull() ?: 0) },\n                    label = \"UID\",\n                    singleLine = true\n                )\n            },\n            {\n                SegmentedTextField(\n                    enabled = enabled,\n                    value = gid.toString(),\n                    onValueChange = { onGidChange(it.toIntOrNull() ?: 0) },\n                    label = \"GID\",\n                    singleLine = true\n                )\n            }\n        )\n    )\n}\n\n@Composable\nprivate fun GroupsPanel(\n    enabled: Boolean,\n    selected: List<Groups>,\n    onSelectionChange: (Set<Groups>) -> Unit\n) {\n    val showDialog = remember { mutableStateOf(false) }\n\n    val groups = remember {\n        Groups.entries.sortedWith(\n            compareBy<Groups> {\n                when (it) {\n                    Groups.ROOT -> 0\n                    Groups.SYSTEM -> 1\n                    Groups.SHELL -> 2\n                    else -> Int.MAX_VALUE\n                }\n            }\n                .then(compareBy { it.name })\n        )\n    }\n\n    if (showDialog.value) {\n        MultiSelectDialog(\n            title = \"Groups\",\n            subtitle = \"${selected.size} / 32\",\n            items = groups,\n            selectedItems = selected.toSet(),\n            itemTitle = { it.display },\n            itemSubtitle = { it.desc },\n            maxSelection = 32,\n            onSelectionChange = {\n                onSelectionChange(it)\n            },\n            onDismiss = { showDialog.value = false }\n        )\n    }\n\n    val tag = if (selected.isEmpty()) {\n        \"None\"\n    } else {\n        selected.joinToString(\", \") { it.display }\n    }\n\n    SegmentedColumn(\n        modifier = Modifier.padding(horizontal = 16.dp),\n        content = listOf {\n            SegmentedListItem(\n                headlineContent = { Text(stringResource(R.string.profile_groups)) },\n                supportingContent = { Text(tag) },\n                onClick = if (enabled) {\n                    { showDialog.value = true }\n                } else null\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun MountNameSpacePanel(\n    enabled: Boolean,\n    namespace: Int,\n    onNamespaceChange: (Int) -> Unit\n) {\n    data class NamespaceOption(\n        val value: Int,\n        val label: String\n    )\n\n    val showDialog = remember { mutableStateOf(false) }\n\n    val inheritedLabel = stringResource(R.string.profile_namespace_inherited)\n    val globalLabel = stringResource(R.string.profile_namespace_global)\n    val individualLabel = stringResource(R.string.profile_namespace_individual)\n\n    val options = remember(inheritedLabel, globalLabel, individualLabel) {\n        listOf(\n            NamespaceOption(0, inheritedLabel),\n            NamespaceOption(1, globalLabel),\n            NamespaceOption(2, individualLabel)\n        )\n    }\n\n    val selectedOption = options.find { it.value == namespace } ?: options[0]\n\n    if (showDialog.value) {\n        SingleSelectDialog(\n            title = stringResource(R.string.profile_namespace),\n            items = options,\n            selectedItem = selectedOption,\n            itemTitle = { it.label },\n            onConfirm = {\n                onNamespaceChange(it.value)\n                showDialog.value = false\n            },\n            onDismiss = { showDialog.value = false }\n        )\n    }\n\n    SegmentedColumn(\n        modifier = Modifier.padding(horizontal = 16.dp),\n        content = listOf {\n            SegmentedListItem(\n                headlineContent = { Text(stringResource(R.string.profile_namespace)) },\n                supportingContent = { Text(selectedOption.label) },\n                onClick = if (enabled) {\n                    { showDialog.value = true }\n                } else null\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun CapsPanel(\n    enabled: Boolean,\n    selected: List<Int>,\n    onSelectionChange: (Set<Capabilities>) -> Unit\n) {\n    val showDialog = remember { mutableStateOf(false) }\n\n    val selectedCaps = remember(selected) {\n        selected.mapNotNull { cap ->\n            Capabilities.entries.find { it.cap == cap }\n        }\n    }\n\n    val capabilities = remember {\n        Capabilities.entries.sortedBy { it.display }\n    }\n\n    if (showDialog.value) {\n        MultiSelectDialog(\n            title = \"Capabilities\",\n            subtitle = \"${selectedCaps.size} / ${Capabilities.entries.size}\",\n            items = capabilities,\n            selectedItems = selectedCaps.toSet(),\n            itemTitle = { it.display },\n            itemSubtitle = { null },\n            maxSelection = Int.MAX_VALUE,\n            onSelectionChange = {\n                onSelectionChange(it)\n            },\n            onDismiss = { showDialog.value = false }\n        )\n    }\n\n    val tag = if (selectedCaps.isEmpty()) {\n        \"None\"\n    } else {\n        selectedCaps.joinToString(\", \") { it.display }\n    }\n\n    SegmentedColumn(\n        modifier = Modifier.padding(horizontal = 16.dp),\n        content = listOf {\n            SegmentedListItem(\n                headlineContent = { Text(stringResource(R.string.profile_capabilities)) },\n                supportingContent = { Text(tag) },\n                onClick = if (enabled) {\n                    { showDialog.value = true }\n                } else null\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun SELinuxPanel(\n    enabled: Boolean,\n    context: String,\n    rules: String,\n    onContextChange: (String) -> Unit,\n    onRulesChange: (String) -> Unit\n) {\n    val showDialog = remember { mutableStateOf(false) }\n\n    if (showDialog.value) {\n        SELinuxDialog(\n            domain = context,\n            rules = rules,\n            onConfirm = { domain, r ->\n                onContextChange(domain)\n                onRulesChange(r)\n                showDialog.value = false\n            },\n            onDismiss = { showDialog.value = false }\n        )\n    }\n\n    SegmentedColumn(\n        modifier = Modifier.padding(horizontal = 16.dp),\n        content = listOf {\n            SegmentedListItem(\n                headlineContent = { Text(stringResource(R.string.profile_selinux_context)) },\n                supportingContent = { Text(context.ifEmpty { \"—\" }) },\n                onClick = if (enabled) {\n                    { showDialog.value = true }\n                } else null\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun SELinuxDialog(\n    domain: String,\n    rules: String,\n    onConfirm: (String, String) -> Unit,\n    onDismiss: () -> Unit\n) {\n    var currentDomain by remember { mutableStateOf(domain) }\n    var currentRules by remember { mutableStateOf(rules) }\n\n    val isDomainValid = remember(currentDomain) {\n        val regex = Regex(\"^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$\")\n        currentDomain.matches(regex)\n    }\n    val isRulesValid = remember(currentRules) { isSepolicyValid(currentRules) }\n\n    AlertDialog(\n        onDismissRequest = onDismiss,\n        title = { Text(stringResource(R.string.profile_selinux_context)) },\n        text = {\n            Column {\n                OutlinedTextField(\n                    value = currentDomain,\n                    onValueChange = { currentDomain = it },\n                    label = { Text(stringResource(R.string.profile_selinux_domain)) },\n                    isError = !isDomainValid,\n                    modifier = Modifier.fillMaxWidth(),\n                    keyboardOptions = KeyboardOptions(\n                        keyboardType = KeyboardType.Ascii,\n                        imeAction = ImeAction.Next\n                    ),\n                    singleLine = true\n                )\n                Spacer(Modifier.height(8.dp))\n                OutlinedTextField(\n                    value = currentRules,\n                    onValueChange = { currentRules = it },\n                    label = { Text(stringResource(R.string.profile_selinux_rules)) },\n                    isError = !isRulesValid,\n                    modifier = Modifier.fillMaxWidth(),\n                    keyboardOptions = KeyboardOptions(\n                        keyboardType = KeyboardType.Ascii\n                    ),\n                    minLines = 3,\n                    maxLines = 10\n                )\n            }\n        },\n        confirmButton = {\n            TextButton(\n                onClick = { onConfirm(currentDomain, currentRules) },\n                enabled = isDomainValid && isRulesValid\n            ) {\n                Text(stringResource(R.string.confirm))\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = onDismiss) {\n                Text(stringResource(android.R.string.cancel))\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfigMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.component.profile\n\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.ImeAction\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.profile.Capabilities\nimport me.weishu.kernelsu.profile.Groups\nimport me.weishu.kernelsu.ui.component.miuix.SuperEditArrow\nimport me.weishu.kernelsu.ui.util.isSepolicyValid\nimport top.yukonga.miuix.kmp.basic.ButtonDefaults\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.basic.TextField\nimport top.yukonga.miuix.kmp.extra.CheckboxLocation\nimport top.yukonga.miuix.kmp.extra.SuperArrow\nimport top.yukonga.miuix.kmp.extra.SuperCheckbox\nimport top.yukonga.miuix.kmp.extra.SuperDialog\nimport top.yukonga.miuix.kmp.extra.SuperDropdown\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\n\n@Composable\nfun RootProfileConfigMiuix(\n    modifier: Modifier = Modifier,\n    fixedName: Boolean,\n    enabled: Boolean = true,\n    profile: Natives.Profile,\n    onProfileChange: (Natives.Profile) -> Unit,\n) {\n    Column(\n        modifier = modifier\n    ) {\n        if (!fixedName) {\n            TextField(\n                enabled = enabled,\n                label = stringResource(R.string.profile_name),\n                value = profile.name,\n                onValueChange = { onProfileChange(profile.copy(name = it)) }\n            )\n        }\n\n        SuperEditArrow(\n            enabled = enabled,\n            title = \"UID\",\n            defaultValue = profile.uid,\n        ) {\n            onProfileChange(\n                profile.copy(\n                    uid = it,\n                    rootUseDefault = false\n                )\n            )\n\n        }\n\n        SuperEditArrow(\n            enabled = enabled,\n            title = \"GID\",\n            defaultValue = profile.gid,\n        ) {\n            onProfileChange(\n                profile.copy(\n                    gid = it,\n                    rootUseDefault = false\n                )\n            )\n\n        }\n\n        val selectedGroups = profile.groups.let { e ->\n            e.mapNotNull { g ->\n                Groups.entries.find { it.gid == g }\n            }\n        }\n\n        GroupsPanel(enabled, selectedGroups) {\n            onProfileChange(\n                profile.copy(\n                    groups = it.map { group -> group.gid },\n                    rootUseDefault = false\n                )\n            )\n        }\n\n        val selectedCaps = profile.capabilities.mapNotNull { e ->\n            Capabilities.entries.find { it.cap == e }\n        }\n\n        CapsPanel(enabled, selectedCaps) {\n            onProfileChange(\n                profile.copy(\n                    capabilities = it.map { cap -> cap.cap },\n                    rootUseDefault = false\n                )\n            )\n        }\n\n        MountNameSpacePanel(enabled = enabled, profile = profile) {\n            onProfileChange(\n                profile.copy(\n                    namespace = it,\n                    rootUseDefault = false\n                )\n            )\n        }\n\n        SELinuxPanel(enabled = enabled, profile = profile, onSELinuxChange = { domain, rules ->\n            onProfileChange(\n                profile.copy(\n                    context = domain,\n                    rules = rules,\n                    rootUseDefault = false\n                )\n            )\n        })\n    }\n}\n\n@Composable\nprivate fun GroupsPanel(\n    enabled: Boolean,\n    selected: List<Groups>,\n    closeSelection: (selection: Set<Groups>) -> Unit\n) {\n    val showDialog = remember { mutableStateOf(false) }\n\n    val groups = remember {\n        Groups.entries.toTypedArray().sortedWith(\n            compareBy<Groups> {\n                when (it) {\n                    Groups.ROOT -> 0\n                    Groups.SYSTEM -> 1\n                    Groups.SHELL -> 2\n                    else -> Int.MAX_VALUE\n                }\n            }\n                .then(compareBy { it.name })\n        )\n    }\n\n    val currentSelection = remember { mutableStateOf(selected.toSet()) }\n\n    SuperDialog(\n        show = showDialog.value,\n        title = stringResource(R.string.profile_groups),\n        summary = \"${currentSelection.value.size} / 32\",\n        onDismissRequest = { showDialog.value = false },\n        insideMargin = DpSize(0.dp, 24.dp),\n        content = {\n            Column(modifier = Modifier.heightIn(max = 500.dp)) {\n                LazyColumn(modifier = Modifier.weight(1f, fill = false)) {\n                    items(groups) { group ->\n                        SuperCheckbox(\n                            title = group.display,\n                            summary = group.desc,\n                            insideMargin = PaddingValues(horizontal = 30.dp, vertical = 16.dp),\n                            checkboxLocation = CheckboxLocation.End,\n                            checked = currentSelection.value.contains(group),\n                            holdDownState = currentSelection.value.contains(group),\n                            onCheckedChange = { isChecked ->\n                                val newSelection = currentSelection.value.toMutableSet()\n                                if (isChecked) {\n                                    if (newSelection.size < 32) newSelection.add(group)\n                                } else {\n                                    newSelection.remove(group)\n                                }\n                                currentSelection.value = newSelection\n                            }\n                        )\n                    }\n                }\n                Spacer(Modifier.height(12.dp))\n                Row(\n                    modifier = Modifier.padding(horizontal = 24.dp),\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    TextButton(\n                        onClick = {\n                            currentSelection.value = selected.toSet()\n                            showDialog.value = false\n                        },\n                        text = stringResource(android.R.string.cancel),\n                        modifier = Modifier.weight(1f),\n                    )\n                    Spacer(modifier = Modifier.width(20.dp))\n                    TextButton(\n                        onClick = {\n                            closeSelection(currentSelection.value)\n                            showDialog.value = false\n                        },\n                        text = stringResource(R.string.confirm),\n                        modifier = Modifier.weight(1f),\n                        colors = ButtonDefaults.textButtonColorsPrimary()\n                    )\n                }\n            }\n        }\n    )\n\n    val tag = if (selected.isEmpty()) {\n        \"None\"\n    } else {\n        selected.joinToString(separator = \",\", transform = { it.display })\n    }\n    SuperArrow(\n        enabled = enabled,\n        title = stringResource(R.string.profile_groups),\n        summary = tag,\n        onClick = {\n            showDialog.value = true\n        }\n    )\n\n}\n\n@Composable\nprivate fun MountNameSpacePanel(\n    enabled: Boolean,\n    profile: Natives.Profile,\n    onMntNamespaceChange: (namespaceType: Int) -> Unit\n) {\n    SuperDropdown(\n        enabled = enabled,\n        title = stringResource(id = R.string.profile_namespace),\n        items = listOf(\n            stringResource(id = R.string.profile_namespace_inherited),\n            stringResource(id = R.string.profile_namespace_global),\n            stringResource(id = R.string.profile_namespace_individual),\n        ),\n        selectedIndex = profile.namespace, onSelectedIndexChange = { index ->\n            onMntNamespaceChange(index)\n        }\n    )\n}\n\n@Composable\nprivate fun CapsPanel(\n    enabled: Boolean,\n    selected: Collection<Capabilities>,\n    closeSelection: (selection: Set<Capabilities>) -> Unit\n) {\n    val showDialog = remember { mutableStateOf(false) }\n\n    val caps = remember {\n        Capabilities.entries.toTypedArray().sortedBy { it.display }\n    }\n\n    val currentSelection = remember { mutableStateOf(selected.toSet()) }\n\n    SuperDialog(\n        show = showDialog.value,\n        title = stringResource(R.string.profile_capabilities),\n        onDismissRequest = { showDialog.value = false },\n        insideMargin = DpSize(0.dp, 24.dp),\n        content = {\n            Column(modifier = Modifier.heightIn(max = 500.dp)) {\n                LazyColumn(modifier = Modifier.weight(1f, fill = false)) {\n                    items(caps) { cap ->\n                        SuperCheckbox(\n                            title = cap.display,\n                            summary = cap.desc,\n                            insideMargin = PaddingValues(horizontal = 30.dp, vertical = 16.dp),\n                            checkboxLocation = CheckboxLocation.End,\n                            checked = currentSelection.value.contains(cap),\n                            holdDownState = currentSelection.value.contains(cap),\n                            onCheckedChange = { isChecked ->\n                                val newSelection = currentSelection.value.toMutableSet()\n                                if (isChecked) {\n                                    newSelection.add(cap)\n                                } else {\n                                    newSelection.remove(cap)\n                                }\n                                currentSelection.value = newSelection\n                            }\n                        )\n                    }\n                }\n                Spacer(Modifier.height(12.dp))\n                Row(\n                    modifier = Modifier.padding(horizontal = 24.dp),\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    TextButton(\n                        onClick = {\n                            showDialog.value = false\n                            currentSelection.value = selected.toSet()\n                        },\n                        text = stringResource(android.R.string.cancel),\n                        modifier = Modifier.weight(1f)\n                    )\n                    Spacer(modifier = Modifier.width(20.dp))\n                    TextButton(\n                        onClick = {\n                            closeSelection(currentSelection.value)\n                            showDialog.value = false\n                        },\n                        text = stringResource(R.string.confirm),\n                        modifier = Modifier.weight(1f),\n                        colors = ButtonDefaults.textButtonColorsPrimary()\n                    )\n                }\n            }\n        }\n    )\n\n    val tag = if (selected.isEmpty()) {\n        \"None\"\n    } else {\n        selected.joinToString(separator = \",\", transform = { it.display })\n    }\n    SuperArrow(\n        enabled = enabled,\n        title = stringResource(R.string.profile_capabilities),\n        summary = tag,\n        onClick = {\n            showDialog.value = true\n        }\n    )\n\n}\n\n@Composable\nprivate fun SELinuxPanel(\n    enabled: Boolean,\n    profile: Natives.Profile,\n    onSELinuxChange: (domain: String, rules: String) -> Unit\n) {\n    val showDialog = remember { mutableStateOf(false) }\n\n    var domain by remember { mutableStateOf(profile.context) }\n    var rules by remember { mutableStateOf(profile.rules) }\n\n    val isDomainValid = remember(domain) {\n        val regex = Regex(\"^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$\")\n        domain.matches(regex)\n    }\n    val isRulesValid = remember(rules) { isSepolicyValid(rules) }\n\n    SuperDialog(\n        show = showDialog.value,\n        title = stringResource(R.string.profile_selinux_context),\n        onDismissRequest = { showDialog.value = false },\n        content = {\n            Column(modifier = Modifier.heightIn(max = 500.dp)) {\n                Column(modifier = Modifier.weight(1f, fill = false)) {\n                    TextField(\n                        value = domain,\n                        onValueChange = { domain = it },\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(vertical = 8.dp),\n                        label = stringResource(id = R.string.profile_selinux_domain),\n                        borderColor = if (isDomainValid) {\n                            colorScheme.primary\n                        } else {\n                            Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f)\n                        },\n                        keyboardOptions = KeyboardOptions(\n                            keyboardType = KeyboardType.Ascii,\n                            imeAction = ImeAction.Next\n                        ),\n                        singleLine = true\n                    )\n                    TextField(\n                        value = rules,\n                        onValueChange = { rules = it },\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(vertical = 8.dp),\n                        label = stringResource(id = R.string.profile_selinux_rules),\n                        borderColor = if (isRulesValid) {\n                            colorScheme.primary\n                        } else {\n                            Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f)\n                        },\n                        keyboardOptions = KeyboardOptions(\n                            keyboardType = KeyboardType.Ascii,\n                        ),\n                        singleLine = false\n                    )\n                }\n                Spacer(Modifier.height(12.dp))\n                Row(\n                    horizontalArrangement = Arrangement.SpaceBetween\n                ) {\n                    TextButton(\n                        onClick = { showDialog.value = false },\n                        text = stringResource(android.R.string.cancel),\n                        modifier = Modifier.weight(1f)\n                    )\n                    Spacer(modifier = Modifier.width(20.dp))\n                    TextButton(\n                        onClick = {\n                            onSELinuxChange(domain, rules)\n                            showDialog.value = false\n                        },\n                        text = stringResource(R.string.confirm),\n                        enabled = isDomainValid && isRulesValid,\n                        modifier = Modifier.weight(1f),\n                        colors = ButtonDefaults.textButtonColorsPrimary()\n                    )\n                }\n            }\n        }\n    )\n\n    SuperArrow(\n        enabled = enabled,\n        title = stringResource(R.string.profile_selinux_context),\n        summary = profile.context,\n        onClick = { showDialog.value = true }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfigMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.component.profile\n\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ReadMore\nimport androidx.compose.material.icons.filled.Create\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedListItem\nimport me.weishu.kernelsu.ui.component.profile.dialogs.SingleSelectDialog\nimport me.weishu.kernelsu.ui.util.listAppProfileTemplates\nimport me.weishu.kernelsu.ui.util.setSepolicy\nimport me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById\n\nprivate data class TemplateOption(\n    val id: String,\n    val name: String\n)\n\n@Composable\nfun TemplateConfigMaterial(\n    profile: Natives.Profile,\n    onViewTemplate: (id: String) -> Unit = {},\n    onManageTemplate: () -> Unit = {},\n    onProfileChange: (Natives.Profile) -> Unit\n) {\n    val showDialog = remember { mutableStateOf(false) }\n    val template = rememberSaveable { mutableStateOf(profile.rootTemplate ?: \"\") }\n    val profileTemplates = listAppProfileTemplates()\n    val noTemplates = profileTemplates.isEmpty()\n\n    val templateOptions = remember(profileTemplates) {\n        profileTemplates.map { tid ->\n            TemplateOption(tid, tid)\n        }\n    }\n\n    val selectedTemplate = remember(template.value, templateOptions) {\n        templateOptions.find { it.id == template.value } ?: templateOptions.firstOrNull()\n    }\n\n    if (showDialog.value && !noTemplates) {\n        SingleSelectDialog(\n            title = stringResource(R.string.profile_template),\n            items = templateOptions,\n            selectedItem = selectedTemplate ?: templateOptions.first(),\n            itemTitle = { it.name },\n            onConfirm = { selected ->\n                val tid = selected.id\n                val templateInfo = getTemplateInfoById(tid)\n                if (templateInfo != null && setSepolicy(tid, templateInfo.rules.joinToString(\"\\n\"))) {\n                    onProfileChange(\n                        profile.copy(\n                            rootTemplate = tid,\n                            rootUseDefault = false,\n                            uid = templateInfo.uid,\n                            gid = templateInfo.gid,\n                            groups = templateInfo.groups,\n                            capabilities = templateInfo.capabilities,\n                            context = templateInfo.context,\n                            namespace = templateInfo.namespace,\n                        )\n                    )\n                    template.value = tid\n                }\n                showDialog.value = false\n            },\n            onDismiss = { showDialog.value = false }\n        )\n    }\n\n    val selectedTemplateName = template.value.ifEmpty { \"None\" }\n\n    SegmentedColumn(\n        modifier = Modifier.padding(horizontal = 16.dp),\n        content = buildList {\n            add {\n                SegmentedListItem(\n                    headlineContent = { Text(stringResource(R.string.profile_template)) },\n                    supportingContent = { Text(selectedTemplateName) },\n                    trailingContent = {\n                        if (noTemplates) {\n                            IconButton(onClick = onManageTemplate) {\n                                Icon(Icons.Filled.Create, contentDescription = null)\n                            }\n                        }\n                    },\n                    onClick = {\n                        if (!noTemplates) {\n                            showDialog.value = true\n                        }\n                    }\n                )\n            }\n            if (template.value.isNotEmpty()) add {\n                SegmentedListItem(\n                    headlineContent = { Text(stringResource(R.string.app_profile_template_view)) },\n                    trailingContent = {\n                        Icon(Icons.AutoMirrored.Filled.ReadMore, contentDescription = null)\n                    },\n                    onClick = { onViewTemplate(template.value) }\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfigMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.component.profile\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Create\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.util.listAppProfileTemplates\nimport me.weishu.kernelsu.ui.util.setSepolicy\nimport me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.extra.SuperArrow\nimport top.yukonga.miuix.kmp.extra.SuperDropdown\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\n\n/**\n * @author weishu\n * @date 2023/10/21.\n */\n@Composable\nfun TemplateConfigMiuix(\n    modifier: Modifier = Modifier,\n    profile: Natives.Profile,\n    onViewTemplate: (id: String) -> Unit = {},\n    onManageTemplate: () -> Unit = {},\n    onProfileChange: (Natives.Profile) -> Unit\n) {\n    val profileTemplates = listAppProfileTemplates()\n    val noTemplates = profileTemplates.isEmpty()\n\n    if (noTemplates) {\n        SuperArrow(\n            modifier = modifier,\n            title = stringResource(R.string.app_profile_template_create),\n            startAction = {\n                Icon(\n                    Icons.Rounded.Create,\n                    null,\n                    modifier = Modifier.padding(end = 16.dp),\n                    tint = MiuixTheme.colorScheme.onBackground\n                )\n            },\n            onClick = onManageTemplate,\n        )\n    } else {\n        var template by rememberSaveable { mutableStateOf(profile.rootTemplate ?: profileTemplates[0]) }\n\n        Column(modifier = modifier) {\n            SuperDropdown(\n                title = stringResource(R.string.profile_template),\n                items = profileTemplates,\n                selectedIndex = profileTemplates.indexOf(template).takeIf { it >= 0 } ?: 0,\n                onSelectedIndexChange = { index ->\n                    if (index < 0 || index >= profileTemplates.size) return@SuperDropdown\n                    template = profileTemplates[index]\n                    val templateInfo = getTemplateInfoById(template)\n                    if (templateInfo != null && setSepolicy(template, templateInfo.rules.joinToString(\"\\n\"))) {\n                        onProfileChange(\n                            profile.copy(\n                                rootTemplate = template,\n                                rootUseDefault = false,\n                                uid = templateInfo.uid,\n                                gid = templateInfo.gid,\n                                groups = templateInfo.groups,\n                                capabilities = templateInfo.capabilities,\n                                context = templateInfo.context,\n                                namespace = templateInfo.namespace,\n                            )\n                        )\n                    }\n                },\n                maxHeight = 280.dp\n            )\n            SuperArrow(\n                title = stringResource(R.string.app_profile_template_view),\n                onClick = { onViewTemplate(template) }\n            )\n        }\n    }\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/dialogs/MultiSelectDialog.kt",
    "content": "package me.weishu.kernelsu.ui.component.profile.dialogs\n\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Search\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.ui.component.material.SegmentedCheckboxItem\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun <T> MultiSelectDialog(\n    title: String,\n    subtitle: String? = null,\n    items: List<T>,\n    selectedItems: Set<T>,\n    itemTitle: (T) -> String,\n    itemSubtitle: (T) -> String?,\n    maxSelection: Int = Int.MAX_VALUE,\n    onSelectionChange: (Set<T>) -> Unit,\n    onDismiss: () -> Unit\n) {\n    var searchQuery by remember { mutableStateOf(\"\") }\n    val filteredItems = remember(items, searchQuery) {\n        if (searchQuery.isBlank()) {\n            items\n        } else {\n            items.filter { item ->\n                itemTitle(item).contains(searchQuery, ignoreCase = true) ||\n                        itemSubtitle(item)?.contains(searchQuery, ignoreCase = true) == true\n            }\n        }\n    }\n\n    ModalBottomSheet(\n        onDismissRequest = onDismiss,\n        sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 24.dp)\n                .padding(bottom = 24.dp),\n            verticalArrangement = Arrangement.spacedBy(16.dp),\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier = Modifier.fillMaxWidth()\n            ) {\n                Column(modifier = Modifier.weight(1f)) {\n                    Text(\n                        text = title,\n                        style = MaterialTheme.typography.titleLarge\n                    )\n                    if (subtitle != null) {\n                        Text(\n                            text = subtitle,\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant\n                        )\n                    }\n                }\n                IconButton(onClick = onDismiss) {\n                    Icon(Icons.Filled.Close, contentDescription = null)\n                }\n            }\n\n            OutlinedTextField(\n                value = searchQuery,\n                onValueChange = { searchQuery = it },\n                modifier = Modifier.fillMaxWidth(),\n                leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) },\n                trailingIcon = {\n                    if (searchQuery.isNotEmpty()) {\n                        IconButton(onClick = { searchQuery = \"\" }) {\n                            Icon(Icons.Filled.Close, contentDescription = null)\n                        }\n                    }\n                },\n                singleLine = true\n            )\n\n            SegmentedColumn(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .weight(1f, fill = false)\n                    .verticalScroll(rememberScrollState()),\n                content = filteredItems.map { item ->\n                    {\n                        SegmentedCheckboxItem(\n                            title = itemTitle(item),\n                            summary = itemSubtitle(item),\n                            colors = ListItemDefaults.segmentedColors().copy(\n                                containerColor = MaterialTheme.colorScheme.surface,\n                                contentColor = Color.Unspecified\n                            ),\n                            checked = selectedItems.contains(item),\n                            onCheckedChange = { isChecked ->\n                                val newSelection = selectedItems.toMutableSet()\n                                if (isChecked && newSelection.size < maxSelection) {\n                                    newSelection.add(item)\n                                } else if (!isChecked) {\n                                    newSelection.remove(item)\n                                }\n                                onSelectionChange(newSelection)\n                            }\n                        )\n                    }\n                }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/dialogs/SingleSelectDialog.kt",
    "content": "package me.weishu.kernelsu.ui.component.profile.dialogs\n\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedRadioItem\n\n@Composable\nfun <T> SingleSelectDialog(\n    title: String,\n    items: List<T>,\n    selectedItem: T,\n    itemTitle: (T) -> String,\n    onConfirm: (T) -> Unit,\n    onDismiss: () -> Unit\n) {\n    var selected by remember { mutableStateOf(selectedItem) }\n\n    AlertDialog(\n        onDismissRequest = onDismiss,\n        title = { Text(title) },\n        text = {\n            SegmentedColumn(\n                modifier = Modifier.verticalScroll(rememberScrollState()),\n                content = items.map { item ->\n                    {\n                        SegmentedRadioItem(\n                            title = itemTitle(item),\n                            selected = selected == item,\n                            onClick = { selected = item }\n                        )\n                    }\n                }\n            )\n        },\n        confirmButton = {\n            TextButton(\n                onClick = {\n                    onConfirm(selected)\n                    onDismiss()\n                }\n            ) {\n                Text(stringResource(R.string.confirm))\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = onDismiss) {\n                Text(stringResource(android.R.string.cancel))\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/rebootlistpopup/RebootListPopup.kt",
    "content": "package me.weishu.kernelsu.ui.component.rebootlistpopup\n\nimport androidx.compose.runtime.Composable\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\n\n@Composable\nfun RebootListPopup() {\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> RebootListPopupMiuix()\n        UiMode.Material -> RebootListPopupMaterial()\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/rebootlistpopup/RebootListPopupMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.component.rebootlistpopup\n\nimport android.content.Context\nimport android.os.PowerManager\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.PowerSettingsNew\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.KsuIsValid\nimport me.weishu.kernelsu.ui.util.reboot\n\n@Composable\nfun RebootDropdownItems(onItemClick: (String) -> Unit) {\n    val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?\n\n    @Suppress(\"DEPRECATION\")\n    val isRebootingUserspaceSupported = pm?.isRebootingUserspaceSupported == true\n\n    val rebootOptions = mutableListOf(\n        Pair(R.string.reboot, \"\"),\n        Pair(R.string.reboot_soft, \"soft_reboot\"),\n        Pair(R.string.reboot_recovery, \"recovery\"),\n        Pair(R.string.reboot_bootloader, \"bootloader\"),\n        Pair(R.string.reboot_download, \"download\"),\n        Pair(R.string.reboot_edl, \"edl\")\n    )\n    if (isRebootingUserspaceSupported) {\n        rebootOptions.add(1, Pair(R.string.reboot_userspace, \"userspace\"))\n    }\n    rebootOptions.forEach { (id, reason) ->\n        DropdownMenuItem(\n            text = { Text(\"  \" + stringResource(id)) },\n            onClick = { onItemClick(reason) }\n        )\n    }\n}\n\n@Composable\nfun RebootListPopupMaterial() {\n    var expanded by remember { mutableStateOf(false) }\n\n    KsuIsValid {\n        IconButton(onClick = { expanded = true }) {\n            Icon(\n                imageVector = Icons.Filled.PowerSettingsNew,\n                contentDescription = stringResource(id = R.string.reboot)\n            )\n        }\n\n        DropdownMenu(\n            expanded = expanded,\n            onDismissRequest = { expanded = false }\n        ) {\n            RebootDropdownItems { reason ->\n                expanded = false\n                reboot(reason)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/rebootlistpopup/RebootListPopupMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.component.rebootlistpopup\n\nimport android.content.Context\nimport android.os.PowerManager\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.KsuIsValid\nimport me.weishu.kernelsu.ui.util.reboot\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.IconButton\nimport top.yukonga.miuix.kmp.basic.ListPopupColumn\nimport top.yukonga.miuix.kmp.basic.ListPopupDefaults\nimport top.yukonga.miuix.kmp.basic.PopupPositionProvider\nimport top.yukonga.miuix.kmp.extra.SuperListPopup\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.extended.Close2\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\n\n@Composable\nfun RebootListPopupMiuix(\n    modifier: Modifier = Modifier,\n    alignment: PopupPositionProvider.Align = PopupPositionProvider.Align.TopEnd\n) {\n    val showTopPopup = remember { mutableStateOf(false) }\n    KsuIsValid {\n        IconButton(\n            modifier = modifier,\n            onClick = { showTopPopup.value = true },\n            holdDownState = showTopPopup.value\n        ) {\n            Icon(\n                imageVector = MiuixIcons.Close2,\n                contentDescription = stringResource(id = R.string.reboot),\n                tint = colorScheme.onBackground\n            )\n        }\n        SuperListPopup(\n            show = showTopPopup.value,\n            popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,\n            alignment = alignment,\n            onDismissRequest = {\n                showTopPopup.value = false\n            },\n            content = {\n                val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager?\n\n                @Suppress(\"DEPRECATION\")\n                val isRebootingUserspaceSupported = pm?.isRebootingUserspaceSupported == true\n\n                ListPopupColumn {\n                    val rebootOptions = mutableListOf(\n                        Pair(R.string.reboot, \"\"),\n                        Pair(R.string.reboot_soft, \"soft_reboot\"),\n                        Pair(R.string.reboot_recovery, \"recovery\"),\n                        Pair(R.string.reboot_bootloader, \"bootloader\"),\n                        Pair(R.string.reboot_download, \"download\"),\n                        Pair(R.string.reboot_edl, \"edl\")\n                    )\n                    if (isRebootingUserspaceSupported) {\n                        rebootOptions.add(1, Pair(R.string.reboot_userspace, \"userspace\"))\n                    }\n                    rebootOptions.forEachIndexed { idx, (id, reason) ->\n                        RebootDropdownItem(\n                            id = id,\n                            reason = reason,\n                            showTopPopup = showTopPopup,\n                            optionSize = rebootOptions.size,\n                            index = idx\n                        )\n                    }\n                }\n            }\n        )\n    }\n}\n\n@Composable\nfun RebootDropdownItem(\n    id: Int,\n    reason: String = \"\",\n    showTopPopup: MutableState<Boolean>,\n    optionSize: Int,\n    index: Int,\n) {\n    me.weishu.kernelsu.ui.component.miuix.DropdownItem(\n        text = stringResource(id),\n        optionSize = optionSize,\n        onSelectedIndexChange = {\n            reboot(reason)\n            showTopPopup.value = false\n        },\n        index = index\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/statustag/StatusTag.kt",
    "content": "package me.weishu.kernelsu.ui.component.statustag\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\n\n@Composable\nfun StatusTag(\n    label: String,\n    modifier: Modifier = Modifier,\n    backgroundColor: Color,\n    contentColor: Color\n) {\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> StatusTagMiuix(label, backgroundColor, contentColor)\n        UiMode.Material -> StatusTagMaterial(label, modifier, backgroundColor, contentColor)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/statustag/StatusTagMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.component.statustag\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\n\n@Composable\nfun StatusTagMaterial(\n    label: String,\n    modifier: Modifier = Modifier,\n    backgroundColor: Color,\n    contentColor: Color\n) {\n    Box(\n        modifier = modifier\n            .padding(end = 4.dp)\n            .background(\n                color = backgroundColor,\n                shape = RoundedCornerShape(4.dp)\n            )\n    ) {\n        Text(\n            text = label,\n            modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),\n            style = TextStyle(\n                fontSize = MaterialTheme.typography.labelSmall.fontSize,\n                fontWeight = FontWeight.SemiBold,\n                color = contentColor,\n            )\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/statustag/StatusTagMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.component.statustag\n\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.kyant.capsule.ContinuousRoundedRectangle\nimport top.yukonga.miuix.kmp.basic.Text\n\n@Composable\nfun StatusTagMiuix(\n    label: String,\n    backgroundColor: Color,\n    contentColor: Color\n) {\n    Box(\n        modifier = Modifier\n            .background(\n                color = backgroundColor,\n                shape = ContinuousRoundedRectangle(6.dp)\n            )\n    ) {\n        Text(\n            modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp),\n            text = label,\n            color = contentColor,\n            fontSize = 9.sp,\n            fontWeight = FontWeight(750),\n            maxLines = 1,\n            softWrap = false\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/uninstalldialog/UninstallDialog.kt",
    "content": "package me.weishu.kernelsu.ui.component.uninstalldialog\n\nimport androidx.compose.runtime.Composable\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\n\n@Composable\nfun UninstallDialog(\n    show: Boolean,\n    onDismissRequest: () -> Unit\n) {\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> UninstallDialogMiuix(show, onDismissRequest)\n        UiMode.Material -> UninstallDialogMaterial(show, onDismissRequest)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/uninstalldialog/UninstallDialogMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.component.uninstalldialog\n\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedListItem\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.screen.flash.FlashIt\nimport me.weishu.kernelsu.ui.screen.flash.UninstallType\nimport me.weishu.kernelsu.ui.screen.flash.UninstallType.PERMANENT\nimport me.weishu.kernelsu.ui.screen.flash.UninstallType.RESTORE_STOCK_IMAGE\n\n@Composable\nfun UninstallDialogMaterial(\n    show: Boolean,\n    onDismissRequest: () -> Unit\n) {\n    val navigator = LocalNavigator.current\n    val options = listOf(\n        // TEMPORARY,\n        PERMANENT,\n        RESTORE_STOCK_IMAGE\n    )\n    val showConfirmDialog = remember { mutableStateOf(false) }\n    val runType = remember { mutableStateOf<UninstallType?>(null) }\n\n    val run = { type: UninstallType ->\n        when (type) {\n            PERMANENT -> navigator.push(Route.Flash(FlashIt.FlashUninstall))\n            RESTORE_STOCK_IMAGE -> navigator.push(Route.Flash(FlashIt.FlashRestore))\n            else -> Unit\n        }\n    }\n\n    if (show) {\n        AlertDialog(\n            onDismissRequest = onDismissRequest,\n            title = { Text(stringResource(R.string.settings_uninstall)) },\n            text = {\n                SegmentedColumn(\n                    modifier = Modifier,\n                    content = options.map { type ->\n                        {\n                            SegmentedListItem(\n                                onClick = {\n                                    showConfirmDialog.value = true\n                                    runType.value = type\n                                },\n                                headlineContent = { Text(stringResource(type.title)) },\n                                supportingContent = { Text(stringResource(type.message)) },\n                                leadingContent = {\n                                    Icon(\n                                        imageVector = type.icon,\n                                        contentDescription = null\n                                    )\n                                }\n                            )\n                        }\n                    }\n                )\n            },\n            confirmButton = {\n                TextButton(onClick = onDismissRequest) {\n                    Text(stringResource(android.R.string.cancel))\n                }\n            }\n        )\n    }\n\n    val confirmDialog = rememberConfirmDialog(\n        onConfirm = {\n            showConfirmDialog.value = false\n            onDismissRequest()\n            runType.value?.let { type ->\n                run(type)\n            }\n        },\n        onDismiss = {\n            showConfirmDialog.value = false\n        }\n    )\n\n    val dialogTitle = runType.value?.let { type ->\n        options.find { it == type }?.let { stringResource(it.title) }\n    } ?: \"\"\n    val dialogContent = runType.value?.let { type ->\n        options.find { it == type }?.let { stringResource(it.message) }\n    }\n\n    if (showConfirmDialog.value) {\n        confirmDialog.showConfirm(title = dialogTitle, content = dialogContent)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/component/uninstalldialog/UninstallDialogMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.component.uninstalldialog\n\nimport android.widget.Toast\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.unit.DpSize\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.screen.flash.FlashIt\nimport me.weishu.kernelsu.ui.screen.flash.UninstallType\nimport me.weishu.kernelsu.ui.screen.flash.UninstallType.NONE\nimport me.weishu.kernelsu.ui.screen.flash.UninstallType.PERMANENT\nimport me.weishu.kernelsu.ui.screen.flash.UninstallType.RESTORE_STOCK_IMAGE\nimport me.weishu.kernelsu.ui.screen.flash.UninstallType.TEMPORARY\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.extra.SuperArrow\nimport top.yukonga.miuix.kmp.extra.SuperDialog\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\n\n@Composable\nfun UninstallDialogMiuix(\n    show: Boolean,\n    onDismissRequest: () -> Unit\n) {\n    val context = LocalContext.current\n    val navigator = LocalNavigator.current\n    val options = listOf(\n        // TEMPORARY,\n        PERMANENT,\n        RESTORE_STOCK_IMAGE\n    )\n    val showTodo = {\n        Toast.makeText(context, \"TODO\", Toast.LENGTH_SHORT).show()\n    }\n    val showConfirmDialog = remember(show) { mutableStateOf(false) }\n    val runType = remember(show) { mutableStateOf<UninstallType?>(null) }\n\n    val run = { type: UninstallType ->\n        when (type) {\n            PERMANENT -> navigator.push(Route.Flash(FlashIt.FlashUninstall))\n\n            RESTORE_STOCK_IMAGE -> navigator.push(Route.Flash(FlashIt.FlashRestore))\n\n            TEMPORARY -> showTodo()\n            NONE -> Unit\n        }\n    }\n\n    SuperDialog(\n        show = show,\n        onDismissRequest = onDismissRequest,\n        insideMargin = DpSize(0.dp, 0.dp),\n        content = {\n            Text(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = 24.dp, bottom = 12.dp),\n                text = stringResource(R.string.uninstall),\n                fontSize = MiuixTheme.textStyles.title4.fontSize,\n                fontWeight = FontWeight.Medium,\n                textAlign = TextAlign.Center,\n                color = MiuixTheme.colorScheme.onSurface\n            )\n            options.forEach { type ->\n                SuperArrow(\n                    onClick = {\n                        showConfirmDialog.value = true\n                        runType.value = type\n                    },\n                    title = stringResource(type.title),\n                    startAction = {\n                        Icon(\n                            imageVector = type.icon,\n                            contentDescription = null,\n                            modifier = Modifier.padding(end = 16.dp),\n                            tint = MiuixTheme.colorScheme.onSurface\n                        )\n                    },\n                    insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp)\n                )\n            }\n            TextButton(\n                text = stringResource(id = android.R.string.cancel),\n                onClick = onDismissRequest,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(top = 12.dp, bottom = 24.dp)\n                    .padding(horizontal = 24.dp)\n            )\n        }\n    )\n    val confirmDialog = rememberConfirmDialog(\n        onConfirm = {\n            showConfirmDialog.value = false\n            onDismissRequest()\n            runType.value?.let { type ->\n                run(type)\n            }\n        },\n        onDismiss = {\n            showConfirmDialog.value = false\n        }\n    )\n    val dialogTitle = runType.value?.let { type ->\n        options.find { it == type }?.let { stringResource(it.title) }\n    } ?: \"\"\n    val dialogContent = runType.value?.let { type ->\n        options.find { it == type }?.let { stringResource(it.message) }\n    }\n    if (showConfirmDialog.value) {\n        confirmDialog.showConfirm(title = dialogTitle, content = dialogContent)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/modifier/DragGestureInspector.kt",
    "content": "package me.weishu.kernelsu.ui.modifier\n\nimport androidx.compose.foundation.gestures.awaitEachGesture\nimport androidx.compose.foundation.gestures.awaitFirstDown\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.input.pointer.AwaitPointerEventScope\nimport androidx.compose.ui.input.pointer.PointerEventPass\nimport androidx.compose.ui.input.pointer.PointerId\nimport androidx.compose.ui.input.pointer.PointerInputChange\nimport androidx.compose.ui.input.pointer.PointerInputScope\nimport androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed\nimport androidx.compose.ui.input.pointer.positionChange\nimport androidx.compose.ui.util.fastFirstOrNull\n\nsuspend fun PointerInputScope.inspectDragGestures(\n    onDragStart: (down: PointerInputChange) -> Unit = {},\n    onDragEnd: (change: PointerInputChange) -> Unit = {},\n    onDragCancel: () -> Unit = {},\n    onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit\n) {\n    awaitEachGesture {\n        val initialDown = awaitFirstDown(false, PointerEventPass.Initial)\n\n        val down = awaitFirstDown(false)\n\n        onDragStart(down)\n        onDrag(initialDown, Offset.Zero)\n        val upEvent =\n            drag(\n                pointerId = initialDown.id,\n                onDrag = { onDrag(it, it.positionChange()) }\n            )\n        if (upEvent == null) {\n            onDragCancel()\n        } else {\n            onDragEnd(upEvent)\n        }\n    }\n}\n\nprivate suspend inline fun AwaitPointerEventScope.drag(\n    pointerId: PointerId,\n    onDrag: (PointerInputChange) -> Unit\n): PointerInputChange? {\n    val isPointerUp = currentEvent.changes.fastFirstOrNull { it.id == pointerId }?.pressed != true\n    if (isPointerUp) {\n        return null\n    }\n    var pointer = pointerId\n    while (true) {\n        val change = awaitDragOrUp(pointer) ?: return null\n        if (change.isConsumed) {\n            return null\n        }\n        if (change.changedToUpIgnoreConsumed()) {\n            return change\n        }\n        onDrag(change)\n        pointer = change.id\n    }\n}\n\nprivate suspend inline fun AwaitPointerEventScope.awaitDragOrUp(\n    pointerId: PointerId\n): PointerInputChange? {\n    var pointer = pointerId\n    while (true) {\n        val event = awaitPointerEvent()\n        val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null\n        if (dragEvent.changedToUpIgnoreConsumed()) {\n            val otherDown = event.changes.fastFirstOrNull { it.pressed }\n            if (otherDown == null) {\n                return dragEvent\n            } else {\n                pointer = otherDown.id\n            }\n        } else {\n            val hasDragged = dragEvent.previousPosition != dragEvent.position\n            if (hasDragged) {\n                return dragEvent\n            }\n        }\n    }\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/navigation3/DeepLinkResolver.kt",
    "content": "package me.weishu.kernelsu.ui.navigation3\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.net.Uri\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.State\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalContext\n\n/**\n * Deep link resolution: maps external Intent/Uri to an initial back stack.\n * Call resolve(intent) at Activity start to seed the back stack.\n */\nobject DeepLinkResolver {\n    fun resolve(intent: Intent?): List<Route> {\n        if (intent == null) return emptyList()\n        val shortcutType = intent.getStringExtra(\"shortcut_type\")\n        return when (shortcutType) {\n            \"module_action\" -> {\n                val moduleId = intent.getStringExtra(\"module_id\") ?: return emptyList()\n                listOf(Route.Main, Route.ExecuteModuleAction(moduleId, fromShortcut = true))\n            }\n\n            else -> emptyList()\n        }\n    }\n\n    fun resolve(uri: Uri?): List<Route> {\n        return emptyList()\n    }\n}\n\n/**\n * Composable that handles deep link intents and updates the back stack accordingly.\n * Should be placed at the root of the NavHost.\n */\n@Composable\nfun HandleDeepLink(\n    intentState: State<Int>,\n) {\n    val context = LocalContext.current\n    val activity = context as? Activity\n    val currentIntentId by intentState\n    val navigator = LocalNavigator.current\n    var lastHandledIntentId by rememberSaveable { mutableIntStateOf(-1) }\n\n    LaunchedEffect(currentIntentId) {\n        if (currentIntentId != lastHandledIntentId) {\n            val intent = activity?.intent\n            val initialStack = DeepLinkResolver.resolve(intent)\n            if (initialStack.isNotEmpty()) {\n                navigator.replaceAll(initialStack)\n                intent?.removeExtra(\"shortcut_type\")\n                intent?.removeExtra(\"module_id\")\n            }\n            lastHandledIntentId = currentIntentId\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/navigation3/Navigator.kt",
    "content": "package me.weishu.kernelsu.ui.navigation3\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.saveable.Saver\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.snapshots.SnapshotStateList\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.MutableSharedFlow\nimport kotlinx.coroutines.flow.SharedFlow\n\n/**\n * Simple navigation helper that owns a back stack and result channels.\n * Supports push/replace/pop/popUntil and result APIs: navigateForResult/setResult/observeResult/clearResult.\n */\nclass Navigator(\n    initialKey: NavKey\n) {\n    val backStack: SnapshotStateList<NavKey> = mutableStateListOf(initialKey)\n\n    private val resultBus = mutableMapOf<String, MutableSharedFlow<Any>>()\n\n    /**\n     * Push a key onto the back stack.\n     */\n    fun push(key: NavKey) {\n        backStack.add(key)\n    }\n\n    /**\n     * Replace the top key, or push if the stack is empty.\n     */\n    fun replace(key: NavKey) {\n        if (backStack.isNotEmpty()) {\n            backStack[backStack.lastIndex] = key\n        } else {\n            backStack.add(key)\n        }\n    }\n\n    /**\n     * Replace the backstack with a new list of keys if the stack is not empty.\n     */\n    fun replaceAll(keys: List<NavKey>) {\n        if (keys.isEmpty()) {\n            return\n        }\n        if (backStack.isNotEmpty()) {\n            backStack.clear()\n            backStack.addAll(keys)\n        }\n    }\n\n\n    /**\n     * Pop the top key if present.\n     */\n    fun pop() {\n        backStack.removeLastOrNull()\n    }\n\n    /**\n     * Pop until predicate matches the top key.\n     */\n    fun popUntil(predicate: (NavKey) -> Boolean) {\n        while (backStack.isNotEmpty() && !predicate(backStack.last())) {\n            backStack.removeAt(backStack.lastIndex)\n        }\n    }\n\n    /**\n     * Navigate expecting a result. Caller should subscribe via observeResult(requestKey).\n     */\n    fun navigateForResult(route: Route, requestKey: String) {\n        ensureChannel(requestKey)\n        push(route)\n    }\n\n    /**\n     * Set a result for the given request and then pop.\n     */\n    fun <T : Any> setResult(requestKey: String, value: T) {\n        ensureChannel(requestKey).tryEmit(value)\n        pop()\n    }\n\n    /**\n     * Observe results for a given request key as a SharedFlow.\n     */\n    @Suppress(\"UNCHECKED_CAST\")\n    fun <T : Any> observeResult(requestKey: String): SharedFlow<T> {\n        return ensureChannel(requestKey) as SharedFlow<T>\n    }\n\n    /**\n     * Clear the last emitted result for the request key.\n     */\n    @OptIn(ExperimentalCoroutinesApi::class)\n    fun clearResult(requestKey: String) {\n        ensureChannel(requestKey).resetReplayCache()\n    }\n\n    /**\n     * Get current NavKey on the back stack.\n     */\n    fun current(): NavKey? {\n        return backStack.lastOrNull()\n    }\n\n    /**\n     * Get current size of back stack.\n     */\n    fun backStackSize(): Int {\n        return backStack.size\n    }\n\n    private fun ensureChannel(key: String): MutableSharedFlow<Any> {\n        return resultBus.getOrPut(key) { MutableSharedFlow(replay = 1, extraBufferCapacity = 0) }\n    }\n\n    companion object {\n        val Saver: Saver<Navigator, Any> = listSaver(save = { navigator ->\n            navigator.backStack.toList()\n        }, restore = { savedList ->\n            val initialKey = savedList.firstOrNull() ?: Route.Home\n            val navigator = Navigator(initialKey)\n            navigator.backStack.clear()\n            navigator.backStack.addAll(savedList)\n            navigator\n        })\n    }\n}\n\n\n@Composable\nfun rememberNavigator(startRoute: NavKey): Navigator {\n    return rememberSaveable(startRoute, saver = Navigator.Saver) {\n        Navigator(startRoute)\n    }\n}\n\nval LocalNavigator = staticCompositionLocalOf<Navigator> {\n    error(\"LocalNavigator not provided\")\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/navigation3/Routes.kt",
    "content": "package me.weishu.kernelsu.ui.navigation3\n\nimport android.os.Parcelable\nimport androidx.navigation3.runtime.NavKey\nimport kotlinx.parcelize.Parcelize\nimport kotlinx.serialization.Serializable\nimport me.weishu.kernelsu.ui.screen.flash.FlashIt\nimport me.weishu.kernelsu.ui.screen.modulerepo.RepoModuleArg\nimport me.weishu.kernelsu.ui.util.FlashItSerializer\nimport me.weishu.kernelsu.ui.util.RepoModuleArgSerializer\nimport me.weishu.kernelsu.ui.util.TemplateInfoSerializer\nimport me.weishu.kernelsu.ui.viewmodel.TemplateViewModel\n\n/**\n * Type-safe navigation keys for Navigation3.\n * Each destination is a NavKey (data object/data class) and can be saved/restored in the back stack.\n */\nsealed interface Route : NavKey, Parcelable {\n    @Parcelize\n    @Serializable\n    data object Main : Route\n\n    @Parcelize\n    @Serializable\n    data object Home : Route\n\n    @Parcelize\n    @Serializable\n    data object SuperUser : Route\n\n    @Parcelize\n    @Serializable\n    data object Module : Route\n\n    @Parcelize\n    @Serializable\n    data object Settings : Route\n\n    @Parcelize\n    @Serializable\n    data object About : Route\n\n    @Parcelize\n    @Serializable\n    data object ColorPalette : Route\n\n    @Parcelize\n    @Serializable\n    data object AppProfileTemplate : Route\n\n    @Parcelize\n    @Serializable\n    data class TemplateEditor(\n        @Serializable(with = TemplateInfoSerializer::class) val template: TemplateViewModel.TemplateInfo,\n        val readOnly: Boolean\n    ) : Route\n\n    @Parcelize\n    @Serializable\n    data class AppProfile(val uid: Int) : Route\n\n    @Parcelize\n    @Serializable\n    data object Install : Route\n\n    @Parcelize\n    @Serializable\n    data class ModuleRepoDetail(@Serializable(with = RepoModuleArgSerializer::class) val module: RepoModuleArg) : Route\n\n    @Parcelize\n    @Serializable\n    data object ModuleRepo : Route\n\n    @Parcelize\n    @Serializable\n    data class Flash(@Serializable(with = FlashItSerializer::class) val flashIt: FlashIt) : Route\n\n    @Parcelize\n    @Serializable\n    data class ExecuteModuleAction(val moduleId: String, val fromShortcut: Boolean = false) : Route\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/about/AboutMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.about\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeFlexibleTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.layout.FixedScale\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedListItem\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun AboutScreenMaterial(\n    state: AboutUiState,\n    actions: AboutScreenActions,\n) {\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    Scaffold(\n        topBar = {\n            LargeFlexibleTopAppBar(\n                title = { Text(state.title) },\n                navigationIcon = {\n                    IconButton(\n                        onClick = actions.onBack\n                    ) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                            contentDescription = null\n                        )\n                    }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surface,\n                    scrolledContainerColor = MaterialTheme.colorScheme.surface\n                ),\n                scrollBehavior = scrollBehavior\n            )\n        },\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(innerPadding)\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n        ) {\n            item {\n                Column(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 12.dp)\n                        .padding(vertical = 48.dp),\n                    horizontalAlignment = Alignment.CenterHorizontally\n                ) {\n                    Box(\n                        contentAlignment = Alignment.Center,\n                        modifier = Modifier\n                            .size(80.dp)\n                            .clip(RoundedCornerShape(16.dp))\n                            .background(Color.White)\n                    ) {\n                        Image(\n                            painter = painterResource(id = R.drawable.ic_launcher_foreground),\n                            contentDescription = null,\n                            contentScale = FixedScale(1f)\n                        )\n                    }\n                    Text(\n                        modifier = Modifier.padding(top = 12.dp),\n                        text = state.appName,\n                        fontWeight = FontWeight.Medium,\n                        fontSize = MaterialTheme.typography.headlineMedium.fontSize\n                    )\n                    Text(\n                        text = state.versionName,\n                        fontSize = MaterialTheme.typography.bodyMedium.fontSize\n                    )\n                }\n            }\n            item {\n                SegmentedColumn(\n                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                    content = state.links.map { linkInfo ->\n                        {\n                            SegmentedListItem(\n                                onClick = { actions.onOpenLink(linkInfo.url) },\n                                headlineContent = { Text(linkInfo.fullText) }\n                            )\n                        }\n                    }\n                )\n                Spacer(\n                    Modifier.height(\n                        WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                                WindowInsets.captionBar.asPaddingValues().calculateBottomPadding()\n                    )\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/about/AboutMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.about\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.layout.FixedScale\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.kyant.capsule.ContinuousRoundedRectangle\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport top.yukonga.miuix.kmp.basic.Card\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.IconButton\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TopAppBar\nimport top.yukonga.miuix.kmp.extra.SuperArrow\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.extended.Back\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\n\n@Composable\nfun AboutScreenMiuix(\n    state: AboutUiState,\n    actions: AboutScreenActions,\n) {\n    val scrollBehavior = MiuixScrollBehavior()\n    val enableBlur = LocalEnableBlur.current\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                modifier = if (enableBlur) {\n                    Modifier.defaultHazeEffect(hazeState, hazeStyle)\n                } else {\n                    Modifier\n                },\n                color = if (enableBlur) Color.Transparent else colorScheme.surface,\n                title = state.title,\n                navigationIcon = {\n                    IconButton(\n                        modifier = Modifier.padding(start = 16.dp),\n                        onClick = actions.onBack\n                    ) {\n                        val layoutDirection = LocalLayoutDirection.current\n                        Icon(\n                            modifier = Modifier.graphicsLayer {\n                                if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\n                            },\n                            imageVector = MiuixIcons.Back,\n                            contentDescription = null,\n                            tint = colorScheme.onBackground\n                        )\n                    }\n                },\n                scrollBehavior = scrollBehavior\n            )\n        },\n        popupHost = { },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxHeight()\n                .overScrollVertical()\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .let { if (enableBlur) it.hazeSource(state = hazeState) else it }\n                .padding(horizontal = 12.dp),\n            contentPadding = innerPadding,\n            overscrollEffect = null,\n        ) {\n            item {\n                Column(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 12.dp)\n                        .padding(vertical = 48.dp),\n                    horizontalAlignment = Alignment.CenterHorizontally\n                ) {\n                    Box(\n                        contentAlignment = Alignment.Center,\n                        modifier = Modifier\n                            .size(80.dp)\n                            .clip(ContinuousRoundedRectangle(16.dp))\n                            .background(Color.White)\n                    ) {\n                        Image(\n                            painter = painterResource(id = R.drawable.ic_launcher_foreground),\n                            contentDescription = null,\n                            contentScale = FixedScale(1f)\n                        )\n                    }\n                    Text(\n                        modifier = Modifier.padding(top = 12.dp),\n                        text = state.appName,\n                        fontWeight = FontWeight.Medium,\n                        fontSize = 26.sp\n                    )\n                    Text(\n                        text = state.versionName,\n                        fontSize = 14.sp\n                    )\n                }\n            }\n            item {\n                Card(\n                    modifier = Modifier.padding(bottom = 12.dp)\n                ) {\n                    state.links.forEach {\n                        SuperArrow(\n                            title = it.fullText,\n                            onClick = {\n                                actions.onOpenLink(it.url)\n                            }\n                        )\n                    }\n                }\n                Spacer(\n                    Modifier.height(\n                        WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                                WindowInsets.captionBar.asPaddingValues().calculateBottomPadding()\n                    )\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/about/AboutScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.about\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.compose.dropUnlessResumed\nimport me.weishu.kernelsu.BuildConfig\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\n\n@Composable\nfun AboutScreen() {\n    val navigator = LocalNavigator.current\n    val uriHandler = LocalUriHandler.current\n    val htmlString = stringResource(\n        id = R.string.about_source_code,\n        \"<b><a href=\\\"https://github.com/tiann/KernelSU\\\">GitHub</a></b>\",\n        \"<b><a href=\\\"https://t.me/KernelSU\\\">Telegram</a></b>\"\n    )\n    val state = AboutUiState(\n        title = stringResource(R.string.about),\n        appName = stringResource(R.string.app_name),\n        versionName = BuildConfig.VERSION_NAME,\n        links = extractLinks(htmlString),\n    )\n    val actions = AboutScreenActions(\n        onBack = dropUnlessResumed { navigator.pop() },\n        onOpenLink = uriHandler::openUri,\n    )\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> AboutScreenMiuix(state, actions)\n        UiMode.Material -> AboutScreenMaterial(state, actions)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/about/AboutUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.about\n\nimport androidx.compose.runtime.Immutable\n\n@Immutable\ndata class AboutUiState(\n    val title: String,\n    val appName: String,\n    val versionName: String,\n    val links: List<LinkInfo>,\n)\n\n@Immutable\ndata class AboutScreenActions(\n    val onBack: () -> Unit,\n    val onOpenLink: (String) -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/about/AboutUtils.kt",
    "content": "package me.weishu.kernelsu.ui.screen.about\n\nimport android.util.Log\nimport androidx.compose.runtime.Immutable\n\n@Immutable\ndata class LinkInfo(\n    val fullText: String,\n    val url: String\n)\n\nfun extractLinks(html: String): List<LinkInfo> {\n    val regex = Regex(\n        \"\"\"([^<>\\n\\r]+?)\\s*<b>\\s*<a\\b[^>]*\\bhref\\s*=\\s*(['\"]?)([^'\"\\s>]+)\\2[^>]*>([^<]+)</a>\\s*</b>\\s*(.*?)\\s*(?=<br|\\n|$)\"\"\",\n        RegexOption.MULTILINE\n    )\n\n    return regex.findAll(html).mapNotNull { match ->\n        try {\n            val before = match.groupValues[1].trim()\n            val url = match.groupValues[3].trim()\n            val title = match.groupValues[4].trim()\n            val after = match.groupValues[5].trim()\n\n            val fullText = \"$before $title $after\"\n            LinkInfo(fullText, url)\n        } catch (e: Exception) {\n            Log.e(\"AboutState\", \"extractLinks failed: ${e.message}\")\n            null\n        }\n    }.toList()\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/appprofile/AppProfileMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.appprofile\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.Crossfade\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.AccountCircle\nimport androidx.compose.material.icons.filled.Android\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material.icons.filled.Security\nimport androidx.compose.material3.ButtonGroupDefaults\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeFlexibleTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.ToggleButton\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.role\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.AppIconImage\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedListItem\nimport me.weishu.kernelsu.ui.component.material.SegmentedSwitchItem\nimport me.weishu.kernelsu.ui.component.profile.AppProfileConfig\nimport me.weishu.kernelsu.ui.component.profile.RootProfileConfig\nimport me.weishu.kernelsu.ui.component.profile.TemplateConfig\nimport me.weishu.kernelsu.ui.component.statustag.StatusTag\nimport me.weishu.kernelsu.ui.util.LocalSnackbarHost\nimport me.weishu.kernelsu.ui.util.ownerNameForUid\nimport me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel\n\n/**\n * @author weishu\n * @date 2023/5/16.\n */\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun AppProfileScreenMaterial(\n    state: AppProfileUiState,\n    actions: AppProfileActions,\n) {\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n    val snackBarHost = LocalSnackbarHost.current\n\n    LaunchedEffect(Unit) {\n        scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit\n    }\n\n    Scaffold(\n        topBar = {\n            TopBar(\n                onBack = actions.onBack,\n                scrollBehavior = scrollBehavior,\n                isUidGroup = state.isUidGroup,\n                packageName = state.packageName,\n                userId = state.uid / 100000,\n                onLaunchApp = actions.onLaunchApp,\n                onForceStopApp = actions.onForceStopApp,\n                onRestartApp = actions.onRestartApp,\n            )\n        },\n        snackbarHost = { SnackbarHost(hostState = snackBarHost) },\n        contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)\n    ) { paddingValues ->\n        AppProfileInner(\n            modifier = Modifier\n                .padding(paddingValues)\n                .fillMaxHeight()\n                .imePadding()\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .verticalScroll(rememberScrollState()),\n            packageName = if (state.isUidGroup) \"\" else state.appGroup.primary.packageName,\n            appLabel = if (state.isUidGroup) ownerNameForUid(state.appGroup.primary.uid) else state.appGroup.primary.label,\n            appIcon = {\n                AppIconImage(\n                    packageInfo = state.appGroup.primary.packageInfo,\n                    label = state.appGroup.primary.label,\n                    modifier = Modifier\n                        .padding(top = 4.dp)\n                        .size(48.dp)\n                )\n            },\n            appUid = state.uid,\n            sharedUserId = if (state.isUidGroup) state.sharedUserId else \"\",\n            appVersionName = if (state.isUidGroup) \"\" else (state.appGroup.primary.packageInfo.versionName ?: \"\"),\n            appVersionCode = if (state.isUidGroup) 0L else state.appGroup.primary.packageInfo.longVersionCode,\n            profile = state.profile,\n            isUidGroup = state.isUidGroup,\n            affectedApps = state.appGroup.apps,\n            onViewTemplate = actions.onViewTemplate,\n            onManageTemplate = actions.onManageTemplate,\n            onProfileChange = actions.onProfileChange,\n        )\n    }\n}\n\n@Composable\nprivate fun AppProfileInner(\n    modifier: Modifier = Modifier,\n    packageName: String,\n    appLabel: String,\n    appIcon: @Composable (() -> Unit),\n    appUid: Int,\n    sharedUserId: String = \"\",\n    appVersionName: String,\n    appVersionCode: Long,\n    profile: Natives.Profile,\n    isUidGroup: Boolean = false,\n    affectedApps: List<SuperUserViewModel.AppInfo> = emptyList(),\n    onViewTemplate: (id: String) -> Unit = {},\n    onManageTemplate: () -> Unit = {},\n    onProfileChange: (Natives.Profile) -> Unit,\n) {\n    val isRootGranted = profile.allowSu\n    val userId = appUid / 100000\n    val appId = appUid % 100000\n\n    val initialRootMode = if (profile.rootUseDefault) {\n        Mode.Default\n    } else if (profile.rootTemplate != null) {\n        Mode.Template\n    } else {\n        Mode.Custom\n    }\n    var rootMode by rememberSaveable(profile) {\n        mutableStateOf(initialRootMode)\n    }\n    val nonRootMode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom\n    val mode = if (isRootGranted) rootMode else nonRootMode\n\n    Column(modifier = modifier) {\n        SegmentedColumn(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(horizontal = 16.dp),\n            content = listOf(\n                {\n                    SegmentedListItem(\n                        headlineContent = { Text(appLabel) },\n                        supportingContent = {\n                            Column {\n                                if (!isUidGroup) {\n                                    Text(\"$appVersionName ($appVersionCode)\", color = MaterialTheme.colorScheme.outline)\n                                    Text(packageName, color = MaterialTheme.colorScheme.outline)\n                                } else {\n                                    if (sharedUserId.isNotEmpty()) {\n                                        Text(text = sharedUserId, color = MaterialTheme.colorScheme.outline)\n                                    }\n                                    Text(\n                                        text = stringResource(R.string.group_contains_apps, affectedApps.size),\n                                        color = MaterialTheme.colorScheme.outline\n                                    )\n                                }\n                            }\n                        },\n                        leadingContent = appIcon,\n                        trailingContent = {\n                            Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {\n                                if (userId != 0) {\n                                    StatusTag(\n                                        label = \"USER $userId\",\n                                        contentColor = MaterialTheme.colorScheme.onTertiary,\n                                        backgroundColor = MaterialTheme.colorScheme.tertiary\n                                    )\n                                    StatusTag(\n                                        label = \"UID $appId\",\n                                        contentColor = MaterialTheme.colorScheme.onTertiary,\n                                        backgroundColor = MaterialTheme.colorScheme.tertiary\n                                    )\n                                } else {\n                                    StatusTag(\n                                        label = \"UID $appUid\",\n                                        contentColor = MaterialTheme.colorScheme.onTertiary,\n                                        backgroundColor = MaterialTheme.colorScheme.tertiary\n                                    )\n                                }\n                            }\n                        }\n                    )\n                },\n                {\n                    SegmentedSwitchItem(\n                        icon = Icons.Filled.Security,\n                        title = stringResource(id = R.string.superuser),\n                        checked = isRootGranted,\n                        onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },\n                    )\n                },\n                {\n                    SegmentedListItem(\n                        headlineContent = { Text(stringResource(R.string.profile)) },\n                        supportingContent = { Text(mode.text, color = MaterialTheme.colorScheme.outline) },\n                        leadingContent = { Icon(Icons.Filled.AccountCircle, null) },\n                    )\n                }\n            )\n        )\n\n        Crossfade(targetState = isRootGranted, label = \"\") { current ->\n            Column(\n                modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */)\n            ) {\n                if (current) {\n                    ProfileBox(mode, true) {\n                        // template mode shouldn't change profile here!\n                        if (it == Mode.Default || it == Mode.Custom) {\n                            onProfileChange(\n                                profile.copy(\n                                    rootUseDefault = it == Mode.Default,\n                                    rootTemplate = null\n                                )\n                            )\n                        }\n                        rootMode = it\n                    }\n                    AnimatedVisibility(\n                        visible = mode == Mode.Template,\n                        enter = expandVertically() + fadeIn(),\n                        exit = shrinkVertically() + fadeOut()\n                    ) {\n                        TemplateConfig(\n                            profile = profile,\n                            onViewTemplate = onViewTemplate,\n                            onManageTemplate = onManageTemplate,\n                            onProfileChange = onProfileChange\n                        )\n                    }\n                    AnimatedVisibility(\n                        visible = mode == Mode.Custom,\n                        enter = expandVertically() + fadeIn(),\n                        exit = shrinkVertically() + fadeOut()\n                    ) {\n                        RootProfileConfig(\n                            fixedName = true,\n                            enabled = mode == Mode.Custom,\n                            profile = profile,\n                            onProfileChange = onProfileChange\n                        )\n                    }\n                } else {\n                    ProfileBox(mode, false) {\n                        onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default)))\n                    }\n                    AnimatedVisibility(\n                        visible = true,\n                        enter = expandVertically() + fadeIn(),\n                        exit = shrinkVertically() + fadeOut()\n                    ) {\n                        AppProfileConfig(\n                            fixedName = true,\n                            profile = profile,\n                            enabled = mode == Mode.Custom,\n                            onProfileChange = onProfileChange\n                        )\n                    }\n                }\n                if (isUidGroup) {\n                    val appItems = affectedApps.map<SuperUserViewModel.AppInfo, @Composable () -> Unit> { app ->\n                        {\n                            SegmentedListItem(\n                                headlineContent = { Text(app.label) },\n                                supportingContent = { Text(app.packageName) },\n                                leadingContent = {\n                                    AppIconImage(\n                                        packageInfo = app.packageInfo,\n                                        label = app.label,\n                                        modifier = Modifier.size(36.dp)\n                                    )\n                                }\n                            )\n                        }\n                    }\n                    SegmentedColumn(\n                        modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                        title = stringResource(R.string.app_profile_affects_following_apps),\n                        content = appItems\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun TopBar(\n    onBack: () -> Unit,\n    scrollBehavior: TopAppBarScrollBehavior? = null,\n    isUidGroup: Boolean = false,\n    packageName: String = \"\",\n    userId: Int = 0,\n    onLaunchApp: (String, Int) -> Unit,\n    onForceStopApp: (String, Int) -> Unit,\n    onRestartApp: (String, Int) -> Unit,\n) {\n    LargeFlexibleTopAppBar(\n        title = { Text(stringResource(R.string.profile)) },\n        navigationIcon = {\n            IconButton(\n                onClick = onBack\n            ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }\n        },\n        actions = {\n            if (!isUidGroup) {\n                var showDropdown by remember { mutableStateOf(false) }\n\n                IconButton(\n                    onClick = { showDropdown = true },\n                ) {\n                    Icon(\n                        imageVector = Icons.Filled.MoreVert,\n                        contentDescription = stringResource(id = R.string.settings)\n                    )\n                    DropdownMenu(\n                        expanded = showDropdown,\n                        onDismissRequest = { showDropdown = false }\n                    ) {\n                        DropdownMenuItem(\n                            text = { Text(stringResource(id = R.string.launch_app)) },\n                            onClick = {\n                                showDropdown = false\n                                onLaunchApp(packageName, userId)\n                            },\n                        )\n                        DropdownMenuItem(\n                            text = { Text(stringResource(id = R.string.force_stop_app)) },\n                            onClick = {\n                                showDropdown = false\n                                onForceStopApp(packageName, userId)\n                            },\n                        )\n                        DropdownMenuItem(\n                            text = { Text(stringResource(id = R.string.restart_app)) },\n                            onClick = {\n                                showDropdown = false\n                                onRestartApp(packageName, userId)\n                            },\n                        )\n                    }\n                }\n            }\n        },\n        colors = TopAppBarDefaults.topAppBarColors(\n            containerColor = MaterialTheme.colorScheme.surface,\n            scrolledContainerColor = MaterialTheme.colorScheme.surface\n        ),\n        windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),\n        scrollBehavior = scrollBehavior\n    )\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ProfileBox(\n    mode: Mode,\n    hasTemplate: Boolean,\n    onModeChange: (Mode) -> Unit,\n) {\n    Row(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(horizontal = 16.dp, vertical = 8.dp),\n        horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),\n    ) {\n        val options = listOf(\n            Mode.Default to stringResource(R.string.profile_default),\n            Mode.Template to stringResource(R.string.profile_template),\n            Mode.Custom to stringResource(R.string.profile_custom),\n        )\n\n        options.forEachIndexed { index, (m, label) ->\n            ToggleButton(\n                checked = mode == m,\n                onCheckedChange = {\n                    if (m != Mode.Template || hasTemplate) onModeChange(m)\n                },\n                enabled = if (m == Mode.Template) hasTemplate else true,\n                modifier = Modifier\n                    .weight(1f)\n                    .semantics { role = Role.RadioButton },\n                shapes = when (index) {\n                    0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()\n                    options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()\n                    else -> ButtonGroupDefaults.connectedMiddleButtonShapes()\n                },\n            ) {\n                Text(label, maxLines = 1, overflow = TextOverflow.Ellipsis)\n            }\n        }\n    }\n}\n\n@Preview\n@Composable\nprivate fun AppProfilePreview() {\n    var profile by remember { mutableStateOf(Natives.Profile(\"\")) }\n    AppProfileInner(\n        packageName = \"icu.nullptr.test\",\n        appLabel = \"Test\",\n        appIcon = { Icon(Icons.Filled.Android, null) },\n        appUid = 1,\n        appVersionName = \"v1.0.0\",\n        appVersionCode = 12345,\n        profile = profile,\n        onProfileChange = {\n            profile = it\n        },\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/appprofile/AppProfileMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.appprofile\r\n\r\nimport androidx.compose.animation.AnimatedVisibility\r\nimport androidx.compose.animation.expandVertically\r\nimport androidx.compose.animation.fadeIn\r\nimport androidx.compose.animation.fadeOut\r\nimport androidx.compose.animation.shrinkVertically\r\nimport androidx.compose.foundation.basicMarquee\r\nimport androidx.compose.foundation.layout.Arrangement\r\nimport androidx.compose.foundation.layout.Column\r\nimport androidx.compose.foundation.layout.PaddingValues\r\nimport androidx.compose.foundation.layout.Row\r\nimport androidx.compose.foundation.layout.Spacer\r\nimport androidx.compose.foundation.layout.WindowInsets\r\nimport androidx.compose.foundation.layout.WindowInsetsSides\r\nimport androidx.compose.foundation.layout.add\r\nimport androidx.compose.foundation.layout.asPaddingValues\r\nimport androidx.compose.foundation.layout.captionBar\r\nimport androidx.compose.foundation.layout.displayCutout\r\nimport androidx.compose.foundation.layout.fillMaxHeight\r\nimport androidx.compose.foundation.layout.fillMaxWidth\r\nimport androidx.compose.foundation.layout.height\r\nimport androidx.compose.foundation.layout.navigationBars\r\nimport androidx.compose.foundation.layout.only\r\nimport androidx.compose.foundation.layout.padding\r\nimport androidx.compose.foundation.layout.size\r\nimport androidx.compose.foundation.layout.systemBars\r\nimport androidx.compose.foundation.lazy.LazyColumn\r\nimport androidx.compose.material.icons.Icons\r\nimport androidx.compose.material.icons.rounded.AccountCircle\r\nimport androidx.compose.material.icons.rounded.Security\r\nimport androidx.compose.runtime.Composable\r\nimport androidx.compose.runtime.getValue\r\nimport androidx.compose.runtime.mutableStateOf\r\nimport androidx.compose.runtime.remember\r\nimport androidx.compose.runtime.saveable.rememberSaveable\r\nimport androidx.compose.runtime.setValue\r\nimport androidx.compose.ui.Alignment\r\nimport androidx.compose.ui.Modifier\r\nimport androidx.compose.ui.graphics.Color\r\nimport androidx.compose.ui.graphics.graphicsLayer\r\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\r\nimport androidx.compose.ui.platform.LocalLayoutDirection\r\nimport androidx.compose.ui.res.stringResource\r\nimport androidx.compose.ui.text.font.FontWeight\r\nimport androidx.compose.ui.unit.LayoutDirection\r\nimport androidx.compose.ui.unit.dp\r\nimport androidx.compose.ui.unit.sp\r\nimport dev.chrisbanes.haze.HazeState\r\nimport dev.chrisbanes.haze.HazeStyle\r\nimport dev.chrisbanes.haze.HazeTint\r\nimport dev.chrisbanes.haze.hazeSource\r\nimport me.weishu.kernelsu.Natives\r\nimport me.weishu.kernelsu.R\r\nimport me.weishu.kernelsu.ui.component.AppIconImage\r\nimport me.weishu.kernelsu.ui.component.miuix.DropdownItem\r\nimport me.weishu.kernelsu.ui.component.profile.AppProfileConfig\r\nimport me.weishu.kernelsu.ui.component.profile.RootProfileConfig\r\nimport me.weishu.kernelsu.ui.component.profile.TemplateConfig\r\nimport me.weishu.kernelsu.ui.component.statustag.StatusTag\r\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\r\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\r\nimport me.weishu.kernelsu.ui.util.listAppProfileTemplates\r\nimport me.weishu.kernelsu.ui.util.ownerNameForUid\r\nimport me.weishu.kernelsu.ui.util.setSepolicy\r\nimport me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel\r\nimport top.yukonga.miuix.kmp.basic.BasicComponent\r\nimport top.yukonga.miuix.kmp.basic.Card\r\nimport top.yukonga.miuix.kmp.basic.Icon\r\nimport top.yukonga.miuix.kmp.basic.IconButton\r\nimport top.yukonga.miuix.kmp.basic.ListPopupColumn\r\nimport top.yukonga.miuix.kmp.basic.ListPopupDefaults\r\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\r\nimport top.yukonga.miuix.kmp.basic.PopupPositionProvider\r\nimport top.yukonga.miuix.kmp.basic.Scaffold\r\nimport top.yukonga.miuix.kmp.basic.ScrollBehavior\r\nimport top.yukonga.miuix.kmp.basic.SmallTitle\r\nimport top.yukonga.miuix.kmp.basic.Text\r\nimport top.yukonga.miuix.kmp.basic.TopAppBar\r\nimport top.yukonga.miuix.kmp.extra.SuperDropdown\r\nimport top.yukonga.miuix.kmp.extra.SuperListPopup\r\nimport top.yukonga.miuix.kmp.extra.SuperSwitch\r\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\r\nimport top.yukonga.miuix.kmp.icon.extended.Back\r\nimport top.yukonga.miuix.kmp.icon.extended.MoreCircle\r\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\r\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\r\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\r\n\r\n/**\r\n * @author weishu\r\n * @date 2023/5/16.\r\n */\r\n@Composable\r\nfun AppProfileScreenMiuix(\r\n    state: AppProfileUiState,\r\n    actions: AppProfileActions,\r\n) {\r\n    val enableBlur = LocalEnableBlur.current\r\n    val scrollBehavior = MiuixScrollBehavior()\r\n    val hazeState = remember { HazeState() }\r\n    val hazeStyle = if (enableBlur) {\r\n        HazeStyle(\r\n            backgroundColor = colorScheme.surface,\r\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\r\n        )\r\n    } else {\r\n        HazeStyle.Unspecified\r\n    }\r\n    Scaffold(\r\n        topBar = {\r\n            TopBar(\r\n                onBack = actions.onBack,\r\n                showActions = !state.isUidGroup,\r\n                packageName = state.packageName,\r\n                userId = state.uid / 100000,\r\n                onLaunchApp = actions.onLaunchApp,\r\n                onForceStopApp = actions.onForceStopApp,\r\n                onRestartApp = actions.onRestartApp,\r\n                scrollBehavior = scrollBehavior,\r\n                hazeState = hazeState,\r\n                hazeStyle = hazeStyle,\r\n                enableBlur = enableBlur,\r\n            )\r\n        },\r\n        popupHost = { },\r\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\r\n    ) { innerPadding ->\r\n        LazyColumn(\r\n            modifier = Modifier\r\n                .fillMaxHeight()\r\n                .padding(top = 16.dp)\r\n                .scrollEndHaptic()\r\n                .overScrollVertical()\r\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\r\n                .let { if (enableBlur) it.hazeSource(state = hazeState) else it },\r\n            contentPadding = innerPadding,\r\n            overscrollEffect = null\r\n        ) {\r\n            item {\r\n                AppProfileInner(\r\n                    packageName = if (state.isUidGroup) \"\" else state.appGroup.primary.packageName,\r\n                    appLabel = if (state.isUidGroup) ownerNameForUid(state.appGroup.primary.uid) else state.appGroup.primary.label,\r\n                    appIcon = {\r\n                        AppIconImage(\r\n                            packageInfo = state.appGroup.primary.packageInfo,\r\n                            label = state.appGroup.primary.label,\r\n                            modifier = Modifier.size(54.dp)\r\n                        )\r\n                    },\r\n                    appUid = state.uid,\r\n                    sharedUserId = if (state.isUidGroup) state.sharedUserId else \"\",\r\n                    appVersionName = if (state.isUidGroup) \"\" else (state.appGroup.primary.packageInfo.versionName ?: \"\"),\r\n                    appVersionCode = if (state.isUidGroup) 0L else state.appGroup.primary.packageInfo.longVersionCode,\r\n                    profile = state.profile,\r\n                    isUidGroup = state.isUidGroup,\r\n                    affectedApps = state.appGroup.apps,\r\n                    onViewTemplate = actions.onViewTemplate,\r\n                    onManageTemplate = actions.onManageTemplate,\r\n                    onProfileChange = actions.onProfileChange,\r\n                )\r\n                Spacer(\r\n                    Modifier.height(\r\n                        WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\r\n                                WindowInsets.captionBar.asPaddingValues().calculateBottomPadding()\r\n                    )\r\n                )\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n@Composable\r\nprivate fun AppProfileInner(\r\n    modifier: Modifier = Modifier,\r\n    packageName: String,\r\n    appLabel: String,\r\n    appIcon: @Composable (() -> Unit),\r\n    appUid: Int,\r\n    sharedUserId: String = \"\",\r\n    appVersionName: String,\r\n    appVersionCode: Long,\r\n    profile: Natives.Profile,\r\n    isUidGroup: Boolean = false,\r\n    affectedApps: List<SuperUserViewModel.AppInfo> = emptyList(),\r\n    onViewTemplate: (id: String) -> Unit = {},\r\n    onManageTemplate: () -> Unit = {},\r\n    onProfileChange: (Natives.Profile) -> Unit,\r\n) {\r\n    val isRootGranted = profile.allowSu\r\n    val userId = appUid / 100000\r\n    val appId = appUid % 100000\r\n    val templates = remember { listAppProfileTemplates() }\r\n\r\n    Column(\r\n        modifier = modifier\r\n    ) {\r\n        Card(\r\n            modifier = Modifier\r\n                .fillMaxWidth()\r\n                .padding(horizontal = 12.dp)\r\n                .padding(bottom = 12.dp),\r\n            insideMargin = PaddingValues(horizontal = 16.dp, vertical = 14.dp)\r\n        ) {\r\n            Row(\r\n                verticalAlignment = Alignment.CenterVertically\r\n            ) {\r\n                appIcon()\r\n                Column(\r\n                    modifier = Modifier\r\n                        .padding(start = 16.dp, end = 8.dp)\r\n                        .weight(1f),\r\n                ) {\r\n                    Text(\r\n                        text = appLabel,\r\n                        color = colorScheme.onSurface,\r\n                        fontWeight = FontWeight(550),\r\n                        modifier = Modifier\r\n                            .basicMarquee(),\r\n                        maxLines = 1,\r\n                        softWrap = false\r\n                    )\r\n                    if (!isUidGroup) {\r\n                        Text(\r\n                            text = \"$appVersionName ($appVersionCode)\",\r\n                            fontSize = 12.sp,\r\n                            color = colorScheme.onSurfaceVariantSummary,\r\n                            fontWeight = FontWeight.Medium,\r\n                            modifier = Modifier\r\n                                .basicMarquee(),\r\n                            maxLines = 1,\r\n                            softWrap = false\r\n                        )\r\n                        Text(\r\n                            text = packageName,\r\n                            fontSize = 12.sp,\r\n                            color = colorScheme.onSurfaceVariantSummary,\r\n                            fontWeight = FontWeight.Medium,\r\n                            modifier = Modifier\r\n                                .basicMarquee(),\r\n                            maxLines = 1,\r\n                            softWrap = false\r\n                        )\r\n                    } else {\r\n                        if (sharedUserId.isNotEmpty()) {\r\n                            Text(\r\n                                text = sharedUserId,\r\n                                fontSize = 12.sp,\r\n                                color = colorScheme.onSurfaceVariantSummary,\r\n                                fontWeight = FontWeight.Medium,\r\n                                modifier = Modifier\r\n                                    .basicMarquee(),\r\n                                maxLines = 1,\r\n                                softWrap = false\r\n                            )\r\n                        }\r\n                        Text(\r\n                            text = stringResource(R.string.group_contains_apps, affectedApps.size),\r\n                            fontSize = 12.sp,\r\n                            color = colorScheme.onSurfaceVariantSummary,\r\n                            fontWeight = FontWeight.Medium,\r\n                            modifier = Modifier\r\n                                .basicMarquee(),\r\n                            maxLines = 1,\r\n                            softWrap = false\r\n                        )\r\n                    }\r\n                }\r\n                Column(\r\n                    modifier = Modifier,\r\n                    horizontalAlignment = Alignment.End,\r\n                    verticalArrangement = Arrangement.spacedBy(6.dp)\r\n                ) {\r\n                    if (userId != 0) {\r\n                        StatusTag(\r\n                            label = \"USER $userId\",\r\n                            backgroundColor = colorScheme.primary.copy(alpha = 0.8f),\r\n                            contentColor = colorScheme.onPrimary\r\n                        )\r\n                        StatusTag(\r\n                            label = \"UID $appId\",\r\n                            backgroundColor = colorScheme.primary.copy(alpha = 0.8f),\r\n                            contentColor = colorScheme.onPrimary\r\n                        )\r\n                    } else {\r\n                        StatusTag(\r\n                            label = \"UID $appUid\",\r\n                            backgroundColor = colorScheme.primary.copy(alpha = 0.8f),\r\n                            contentColor = colorScheme.onPrimary\r\n                        )\r\n                    }\r\n                }\r\n            }\r\n        }\r\n\r\n        Card(\r\n            modifier = Modifier\r\n                .fillMaxWidth()\r\n                .padding(horizontal = 12.dp)\r\n                .padding(bottom = 12.dp),\r\n        ) {\r\n            SuperSwitch(\r\n                startAction = {\r\n                    Icon(\r\n                        imageVector = Icons.Rounded.Security,\r\n                        contentDescription = null,\r\n                        modifier = Modifier.padding(end = 16.dp),\r\n                        tint = colorScheme.onBackground\r\n                    )\r\n                },\r\n                title = stringResource(id = R.string.superuser),\r\n                checked = isRootGranted,\r\n                onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) },\r\n            )\r\n        }\r\n\r\n        val initialRootMode = if (profile.rootUseDefault) {\r\n            Mode.Default\r\n        } else if (profile.rootTemplate != null) {\r\n            Mode.Template\r\n        } else {\r\n            Mode.Custom\r\n        }\r\n        var rootMode by rememberSaveable {\r\n            mutableStateOf(initialRootMode)\r\n        }\r\n        val nonRootMode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom\r\n        val dropdownMode = if (isRootGranted) rootMode else nonRootMode\r\n        ProfileBox(dropdownMode, isRootGranted) { mode ->\r\n            if (isRootGranted) {\r\n                when (mode) {\r\n                    Mode.Default, Mode.Custom -> {\r\n                        onProfileChange(\r\n                            profile.copy(\r\n                                rootUseDefault = mode == Mode.Default,\r\n                                rootTemplate = null\r\n                            )\r\n                        )\r\n                        rootMode = mode\r\n                    }\r\n\r\n                    Mode.Template -> {\r\n                        if (templates.isNotEmpty()) {\r\n                            val selected = profile.rootTemplate ?: templates[0]\r\n                            val info = me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById(selected)\r\n                            if (info != null && setSepolicy(selected, info.rules.joinToString(\"\\n\"))) {\r\n                                onProfileChange(\r\n                                    profile.copy(\r\n                                        rootUseDefault = false,\r\n                                        rootTemplate = selected,\r\n                                        uid = info.uid,\r\n                                        gid = info.gid,\r\n                                        groups = info.groups,\r\n                                        capabilities = info.capabilities,\r\n                                        context = info.context,\r\n                                        namespace = info.namespace,\r\n                                    )\r\n                                )\r\n                            } else if (profile.rootTemplate != selected || profile.rootUseDefault) {\r\n                                onProfileChange(\r\n                                    profile.copy(\r\n                                        rootUseDefault = false,\r\n                                        rootTemplate = selected\r\n                                    )\r\n                                )\r\n                            }\r\n                            rootMode = Mode.Template\r\n                        }\r\n                    }\r\n                }\r\n            } else {\r\n                onProfileChange(profile.copy(nonRootUseDefault = (mode == Mode.Default)))\r\n            }\r\n        }\r\n        Spacer(Modifier.height(12.dp))\r\n\r\n        AnimatedVisibility(\r\n            visible = isRootGranted,\r\n            enter = fadeIn() + expandVertically(),\r\n            exit = fadeOut() + shrinkVertically()\r\n        ) {\r\n            Card(\r\n                modifier = Modifier\r\n                    .fillMaxWidth()\r\n                    .padding(horizontal = 12.dp)\r\n                    .padding(bottom = if (rootMode != Mode.Default) 12.dp else 0.dp),\r\n            ) {\r\n                AnimatedVisibility(\r\n                    visible = rootMode == Mode.Template,\r\n                    enter = fadeIn() + expandVertically(),\r\n                    exit = fadeOut() + shrinkVertically()\r\n                ) {\r\n                    TemplateConfig(\r\n                        profile = profile,\r\n                        onViewTemplate = onViewTemplate,\r\n                        onManageTemplate = onManageTemplate,\r\n                        onProfileChange = onProfileChange\r\n                    )\r\n                }\r\n                AnimatedVisibility(\r\n                    visible = rootMode == Mode.Custom,\r\n                    enter = fadeIn() + expandVertically(),\r\n                    exit = fadeOut() + shrinkVertically()\r\n                ) {\r\n                    RootProfileConfig(\r\n                        fixedName = true,\r\n                        enabled = rootMode == Mode.Custom,\r\n                        profile = profile,\r\n                        onProfileChange = onProfileChange\r\n                    )\r\n                }\r\n            }\r\n        }\r\n        AnimatedVisibility(\r\n            visible = !isRootGranted,\r\n            enter = fadeIn() + expandVertically(),\r\n            exit = fadeOut() + shrinkVertically()\r\n        ) {\r\n            Card(\r\n                modifier = Modifier\r\n                    .fillMaxWidth()\r\n                    .padding(horizontal = 12.dp)\r\n                    .padding(bottom = if (nonRootMode != Mode.Default) 12.dp else 0.dp),\r\n            ) {\r\n                AnimatedVisibility(\r\n                    visible = nonRootMode == Mode.Custom,\r\n                    enter = fadeIn() + expandVertically(),\r\n                    exit = fadeOut() + shrinkVertically()\r\n                ) {\r\n                    AppProfileConfig(\r\n                        fixedName = true,\r\n                        profile = profile,\r\n                        enabled = true,\r\n                        onProfileChange = onProfileChange\r\n                    )\r\n                }\r\n            }\r\n        }\r\n\r\n        if (isUidGroup) {\r\n            SmallTitle(\r\n                text = stringResource(R.string.app_profile_affects_following_apps),\r\n                modifier = Modifier.padding(top = 4.dp)\r\n            )\r\n            Card(\r\n                modifier = Modifier\r\n                    .fillMaxWidth()\r\n                    .padding(horizontal = 12.dp)\r\n                    .padding(bottom = 12.dp),\r\n            ) {\r\n                Spacer(Modifier.height(3.dp))\r\n                affectedApps.forEach { app ->\r\n                    BasicComponent(\r\n                        startAction = {\r\n                            AppIconImage(\r\n                                packageInfo = app.packageInfo,\r\n                                label = app.label,\r\n                                modifier = Modifier\r\n                                    .padding(end = 12.dp)\r\n                                    .size(40.dp)\r\n                            )\r\n                        },\r\n                        title = app.label,\r\n                        summary = app.packageName,\r\n                        insideMargin = PaddingValues(horizontal = 16.dp, vertical = 12.dp)\r\n                    )\r\n                }\r\n                Spacer(Modifier.height(3.dp))\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n@Composable\r\nprivate fun TopBar(\r\n    onBack: () -> Unit,\r\n    showActions: Boolean = true,\r\n    packageName: String,\r\n    userId: Int,\r\n    onLaunchApp: (String, Int) -> Unit = { _, _ -> },\r\n    onForceStopApp: (String, Int) -> Unit = { _, _ -> },\r\n    onRestartApp: (String, Int) -> Unit = { _, _ -> },\r\n    scrollBehavior: ScrollBehavior,\r\n    hazeState: HazeState,\r\n    hazeStyle: HazeStyle,\r\n    enableBlur: Boolean\r\n) {\r\n    TopAppBar(\r\n        modifier = if (enableBlur) {\r\n            Modifier.defaultHazeEffect(hazeState, hazeStyle)\r\n        } else {\r\n            Modifier\r\n        },\r\n        color = if (enableBlur) Color.Transparent else colorScheme.surface,\r\n        title = stringResource(R.string.profile),\r\n        navigationIcon = {\r\n            IconButton(\r\n                modifier = Modifier.padding(start = 16.dp),\r\n                onClick = onBack\r\n            ) {\r\n                val layoutDirection = LocalLayoutDirection.current\r\n                Icon(\r\n                    modifier = Modifier.graphicsLayer {\r\n                        if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\r\n                    },\r\n                    imageVector = MiuixIcons.Back,\r\n                    contentDescription = null,\r\n                    tint = colorScheme.onBackground\r\n                )\r\n            }\r\n        },\r\n        actions = {\r\n            if (showActions) {\r\n                val showTopPopup = remember { mutableStateOf(false) }\r\n                IconButton(\r\n                    modifier = Modifier.padding(end = 16.dp),\r\n                    onClick = { showTopPopup.value = true },\r\n                    holdDownState = showTopPopup.value\r\n                ) {\r\n                    Icon(\r\n                        imageVector = MiuixIcons.MoreCircle,\r\n                        tint = colorScheme.onSurface,\r\n                        contentDescription = stringResource(id = R.string.settings)\r\n                    )\r\n                }\r\n                SuperListPopup(\r\n                    show = showTopPopup.value,\r\n                    popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,\r\n                    alignment = PopupPositionProvider.Align.TopEnd,\r\n                    onDismissRequest = { showTopPopup.value = false },\r\n                    content = {\r\n                        ListPopupColumn {\r\n                            val items = listOf(\r\n                                stringResource(id = R.string.launch_app),\r\n                                stringResource(id = R.string.force_stop_app),\r\n                                stringResource(id = R.string.restart_app)\r\n                            )\r\n\r\n                            items.forEachIndexed { index, text ->\r\n                                DropdownItem(\r\n                                    text = text,\r\n                                    optionSize = items.size,\r\n                                    index = index,\r\n                                    onSelectedIndexChange = { selectedIndex ->\r\n                                        when (selectedIndex) {\r\n                                            0 -> onLaunchApp(packageName, userId)\r\n                                            1 -> onForceStopApp(packageName, userId)\r\n                                            2 -> onRestartApp(packageName, userId)\r\n                                        }\r\n                                        showTopPopup.value = false\r\n                                    }\r\n                                )\r\n                            }\r\n                        }\r\n                    }\r\n                )\r\n            }\r\n        },\r\n        scrollBehavior = scrollBehavior\r\n    )\r\n}\r\n\r\n@Composable\r\nprivate fun ProfileBox(\r\n    mode: Mode,\r\n    hasTemplate: Boolean,\r\n    onModeChange: (Mode) -> Unit,\r\n) {\r\n    val defaultText = stringResource(R.string.profile_default)\r\n    val templateText = stringResource(R.string.profile_template)\r\n    val customText = stringResource(R.string.profile_custom)\r\n    val list =\r\n        remember(hasTemplate, defaultText, templateText, customText) {\r\n            buildList {\r\n                add(defaultText)\r\n                if (hasTemplate) {\r\n                    add(templateText)\r\n                }\r\n                add(customText)\r\n            }\r\n        }\r\n\r\n    val modesAndTitles = remember(hasTemplate, defaultText, templateText, customText) {\r\n        buildList {\r\n            add(Mode.Default to defaultText)\r\n            if (hasTemplate) {\r\n                add(Mode.Template to templateText)\r\n            }\r\n            add(Mode.Custom to customText)\r\n        }\r\n    }\r\n    val selectedIndex = modesAndTitles.indexOfFirst { it.first == mode }\r\n    Card(\r\n        modifier = Modifier\r\n            .fillMaxWidth()\r\n            .padding(horizontal = 12.dp),\r\n    ) {\r\n        SuperDropdown(\r\n            title = stringResource(R.string.profile),\r\n            items = list,\r\n            startAction = {\r\n                Icon(\r\n                    Icons.Rounded.AccountCircle,\r\n                    modifier = Modifier.padding(end = 16.dp),\r\n                    contentDescription = null,\r\n                    tint = colorScheme.onBackground\r\n                )\r\n            },\r\n            selectedIndex = if (selectedIndex == -1) 0 else selectedIndex,\r\n        ) {\r\n            onModeChange(modesAndTitles[it].first)\r\n        }\r\n    }\r\n}\r\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/appprofile/AppProfileScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.appprofile\n\nimport android.widget.Toast\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.compose.dropUnlessResumed\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.util.LocalSnackbarHost\nimport me.weishu.kernelsu.ui.util.forceStopApp\nimport me.weishu.kernelsu.ui.util.getSepolicy\nimport me.weishu.kernelsu.ui.util.launchApp\nimport me.weishu.kernelsu.ui.util.restartApp\nimport me.weishu.kernelsu.ui.util.setSepolicy\nimport me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel\nimport me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById\n\n@Composable\nfun AppProfileScreen(uid: Int) {\n    val uiMode = LocalUiMode.current\n    val navigator = LocalNavigator.current\n    val context = LocalContext.current\n    val snackbarHost = LocalSnackbarHost.current\n    val scope = rememberCoroutineScope()\n    val viewModel: SuperUserViewModel = viewModel()\n    val appGroupState = remember(uid) {\n        derivedStateOf {\n            viewModel.uiState.value.groupedApps.find { it.uid == uid } ?: SuperUserViewModel.getGroupedApp(uid)\n        }\n    }\n    val appGroup = appGroupState.value\n    val primaryAppInfo = appGroup?.primary\n    if (primaryAppInfo == null) {\n        LaunchedEffect(Unit) {\n            navigator.pop()\n        }\n        return\n    }\n\n    val packageName = primaryAppInfo.packageName\n    val sharedUserId = remember(uid) {\n        primaryAppInfo.packageInfo.sharedUserId\n            ?: appGroup.apps.firstOrNull { it.packageInfo.sharedUserId != null }?.packageInfo?.sharedUserId\n            ?: \"\"\n    }\n\n    val initialProfile = remember(uid, packageName) {\n        Natives.getAppProfile(packageName, uid).also {\n            if (it.allowSu) {\n                it.rules = getSepolicy(packageName)\n            }\n        }\n    }\n    var profile by rememberSaveable(uid, packageName) {\n        mutableStateOf(initialProfile)\n    }\n\n    val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(primaryAppInfo.label)\n    val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(primaryAppInfo.label)\n    val suNotAllowed = stringResource(R.string.su_not_allowed).format(primaryAppInfo.label)\n\n    fun showMessage(message: String) {\n        scope.launch {\n            if (uiMode == UiMode.Material) {\n                snackbarHost.showSnackbar(message)\n            } else {\n                Toast.makeText(context, message, Toast.LENGTH_SHORT).show()\n            }\n        }\n    }\n\n    val state = AppProfileUiState(\n        uid = uid,\n        packageName = packageName,\n        profile = profile,\n        appGroup = appGroup,\n        sharedUserId = sharedUserId,\n    )\n\n    val actions = AppProfileActions(\n        onBack = dropUnlessResumed { navigator.pop() },\n        onLaunchApp = ::launchApp,\n        onForceStopApp = ::forceStopApp,\n        onRestartApp = ::restartApp,\n        onViewTemplate = { templateId ->\n            getTemplateInfoById(templateId)?.let { info ->\n                navigator.push(Route.TemplateEditor(info, true))\n            }\n        },\n        onManageTemplate = {\n            navigator.push(Route.AppProfileTemplate)\n        },\n        onProfileChange = { updatedProfile ->\n            scope.launch {\n                if (updatedProfile.allowSu) {\n                    if (uid < 2000 && uid != 1000) {\n                        showMessage(suNotAllowed)\n                        return@launch\n                    }\n                    if (!updatedProfile.rootUseDefault\n                        && updatedProfile.rules.isNotEmpty()\n                        && !setSepolicy(profile.name, updatedProfile.rules)\n                    ) {\n                        showMessage(failToUpdateSepolicy)\n                        return@launch\n                    }\n                }\n                if (!Natives.setAppProfile(updatedProfile)) {\n                    showMessage(failToUpdateAppProfile)\n                } else {\n                    profile = updatedProfile\n                    if (uiMode == UiMode.Material) {\n                        viewModel.loadAppList()\n                    }\n                }\n            }\n        },\n    )\n\n    when (uiMode) {\n        UiMode.Miuix -> AppProfileScreenMiuix(\n            state = state,\n            actions = actions,\n        )\n\n        UiMode.Material -> AppProfileScreenMaterial(\n            state = state,\n            actions = actions,\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/appprofile/AppProfileUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.appprofile\n\nimport androidx.compose.runtime.Immutable\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.ui.screen.superuser.GroupedApps\n\n@Immutable\ndata class AppProfileUiState(\n    val uid: Int,\n    val packageName: String,\n    val profile: Natives.Profile,\n    val appGroup: GroupedApps,\n    val sharedUserId: String,\n) {\n    val isUidGroup get() = appGroup.apps.size > 1\n}\n\n@Immutable\ndata class AppProfileActions(\n    val onBack: () -> Unit,\n    val onLaunchApp: (String, Int) -> Unit,\n    val onForceStopApp: (String, Int) -> Unit,\n    val onRestartApp: (String, Int) -> Unit,\n    val onViewTemplate: (String) -> Unit,\n    val onManageTemplate: () -> Unit,\n    val onProfileChange: (Natives.Profile) -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/appprofile/AppProfileUtils.kt",
    "content": "package me.weishu.kernelsu.ui.screen.appprofile\n\nimport androidx.annotation.StringRes\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.res.stringResource\nimport me.weishu.kernelsu.R\n\nenum class Mode(@field:StringRes private val res: Int) {\n    Default(R.string.profile_default),\n    Template(R.string.profile_template),\n    Custom(R.string.profile_custom);\n\n    val text: String\n        @Composable get() = stringResource(res)\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/colorpalette/ColorPaletteScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.colorpalette\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.lifecycle.compose.dropUnlessResumed\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport com.materialkolor.PaletteStyle\nimport com.materialkolor.dynamiccolor.ColorSpec\nimport me.weishu.kernelsu.KernelSUApplication\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.theme.ColorMode\nimport me.weishu.kernelsu.ui.viewmodel.SettingsViewModel\n\n@Composable\nfun ColorPaletteScreen() {\n    val navigator = LocalNavigator.current\n    val context = LocalContext.current\n    val activity = LocalActivity.current\n    val viewModel = viewModel<SettingsViewModel>()\n    val uiState by viewModel.uiState.collectAsState()\n    val currentPaletteStyle = try {\n        PaletteStyle.valueOf(uiState.colorStyle)\n    } catch (_: Exception) {\n        PaletteStyle.TonalSpot\n    }\n    val currentColorSpec = try {\n        ColorSpec.SpecVersion.valueOf(uiState.colorSpec)\n    } catch (_: Exception) {\n        ColorSpec.SpecVersion.Default\n    }\n    val state = ColorPaletteUiState(\n        uiState = uiState,\n        currentColorMode = ColorMode.fromValue(uiState.themeMode),\n        currentPaletteStyle = currentPaletteStyle,\n        currentColorSpec = currentColorSpec,\n    )\n    val actions = ColorPaletteScreenActions(\n        onBack = dropUnlessResumed { navigator.pop() },\n        onSetThemeMode = viewModel::setThemeMode,\n        onSetMiuixMonet = viewModel::setMiuixMonet,\n        onSetKeyColor = viewModel::setKeyColor,\n        onSetColorMode = viewModel::setColorMode,\n        onSetColorStyle = viewModel::setColorStyle,\n        onSetColorSpec = viewModel::setColorSpec,\n        onSetEnableBlur = viewModel::setEnableBlur,\n        onSetEnableFloatingBottomBar = viewModel::setEnableFloatingBottomBar,\n        onSetEnableFloatingBottomBarBlur = viewModel::setEnableFloatingBottomBarBlur,\n        onSetEnablePredictiveBack = {\n            viewModel.setEnablePredictiveBack(it)\n            KernelSUApplication.setEnableOnBackInvokedCallback(context.applicationInfo, it)\n            activity?.recreate()\n        },\n        onSetPageScale = viewModel::setPageScale,\n    )\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> ColorPaletteScreenMiuix(state, actions)\n        UiMode.Material -> ColorPaletteScreenMaterial(state, actions)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/colorpalette/ColorPaletteScreenMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.colorpalette\n\nimport android.annotation.SuppressLint\nimport android.os.Build\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.scaleIn\nimport androidx.compose.animation.scaleOut\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.Canvas\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Brightness1\nimport androidx.compose.material.icons.filled.Brightness3\nimport androidx.compose.material.icons.filled.Brightness4\nimport androidx.compose.material.icons.filled.Brightness7\nimport androidx.compose.material.icons.filled.Home\nimport androidx.compose.material.icons.rounded.Adb\nimport androidx.compose.material.icons.rounded.AspectRatio\nimport androidx.compose.material.icons.rounded.Check\nimport androidx.compose.material.icons.rounded.DesignServices\nimport androidx.compose.material.icons.rounded.Style\nimport androidx.compose.material3.ButtonGroupDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeFlexibleTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Slider\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.ToggleButton\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.role\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.unit.dp\nimport com.materialkolor.PaletteStyle\nimport com.materialkolor.dynamiccolor.ColorSpec\nimport com.materialkolor.rememberDynamicColorScheme\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedDropdownItem\nimport me.weishu.kernelsu.ui.component.material.SegmentedSwitchItem\nimport me.weishu.kernelsu.ui.screen.home.TonalCard\nimport me.weishu.kernelsu.ui.theme.ColorMode\nimport me.weishu.kernelsu.ui.theme.keyColorOptions\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun ColorPaletteScreenMaterial(\n    state: ColorPaletteUiState,\n    actions: ColorPaletteScreenActions,\n) {\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n    val uiState = state.uiState\n    val currentColorMode = state.currentColorMode\n    val currentKeyColor = uiState.keyColor\n    val colorStyle = state.currentPaletteStyle\n    val colorSpec = state.currentColorSpec\n\n    LaunchedEffect(Unit) {\n        scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit\n    }\n\n    Scaffold(\n        topBar = {\n            LargeFlexibleTopAppBar(\n                navigationIcon = {\n                    IconButton(\n                        onClick = actions.onBack\n                    ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) }\n                },\n                title = { Text(stringResource(R.string.settings_theme)) },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surface,\n                    scrolledContainerColor = MaterialTheme.colorScheme.surface\n                ),\n                windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),\n                scrollBehavior = scrollBehavior\n            )\n        },\n        contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)\n    ) { paddingValues ->\n        val navBars = WindowInsets.navigationBars.asPaddingValues()\n        val captionBar = WindowInsets.captionBar.asPaddingValues()\n\n        Column(\n            modifier = Modifier\n                .padding(paddingValues)\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .verticalScroll(rememberScrollState()),\n            verticalArrangement = Arrangement.spacedBy(8.dp)\n        ) {\n            val isDark = currentColorMode.isDark || currentColorMode.isSystem && isSystemInDarkTheme()\n            ThemePreviewCard(\n                keyColor = currentKeyColor,\n                isDark = isDark,\n                paletteStyle = colorStyle,\n                colorSpec = colorSpec,\n            )\n\n            Spacer(modifier = Modifier.height(8.dp))\n\n            LazyRow(\n                modifier = Modifier.fillMaxWidth(),\n                contentPadding = PaddingValues(horizontal = 16.dp),\n                horizontalArrangement = Arrangement.spacedBy(16.dp),\n            ) {\n                item {\n                    ColorButtonMaterial(\n                        color = Color.Unspecified,\n                        isSelected = currentKeyColor == 0,\n                        isDark = isDark,\n                        paletteStyle = colorStyle,\n                        colorSpec = colorSpec,\n                        onClick = {\n                            actions.onSetKeyColor(0)\n                        }\n                    )\n                }\n\n                items(keyColorOptions) { color ->\n                    ColorButtonMaterial(\n                        color = Color(color),\n                        isSelected = currentKeyColor == color,\n                        isDark = isDark,\n                        paletteStyle = colorStyle,\n                        colorSpec = colorSpec,\n                        onClick = {\n                            actions.onSetKeyColor(color)\n                        }\n                    )\n                }\n            }\n\n            Column(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp),\n                verticalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                val options = listOf(\n                    listOf(ColorMode.SYSTEM) to stringResource(R.string.settings_theme_mode_system),\n                    listOf(ColorMode.LIGHT) to stringResource(R.string.settings_theme_mode_light),\n                    listOf(ColorMode.DARK) to stringResource(R.string.settings_theme_mode_dark),\n                    listOf(ColorMode.DARK_AMOLED) to stringResource(R.string.settings_theme_mode_dark)\n                )\n\n                options.chunked(4).forEach { rowOptions ->\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween)\n                    ) {\n                        rowOptions.forEachIndexed { index, (modes, label) ->\n                            ToggleButton(\n                                checked = currentColorMode in modes,\n                                onCheckedChange = {\n                                    if (it) {\n                                        actions.onSetColorMode(modes.first())\n                                    }\n                                },\n                                modifier = Modifier\n                                    .weight(1f)\n                                    .semantics { role = Role.RadioButton },\n                                shapes = when (index) {\n                                    0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()\n                                    rowOptions.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()\n                                    else -> ButtonGroupDefaults.connectedMiddleButtonShapes()\n                                },\n                            ) {\n                                Icon(\n                                    imageVector = when (modes.first()) {\n                                        ColorMode.SYSTEM -> Icons.Filled.Brightness4\n                                        ColorMode.LIGHT -> Icons.Filled.Brightness7\n                                        ColorMode.DARK -> Icons.Filled.Brightness3\n                                        ColorMode.DARK_AMOLED -> Icons.Filled.Brightness1\n                                        else -> Icons.Filled.Brightness4\n                                    },\n                                    contentDescription = label\n                                )\n                            }\n                        }\n                    }\n                }\n\n                SegmentedColumn(\n                    modifier = Modifier.padding(top = 4.dp),\n                    content = listOf(\n                        {\n                            val styles = PaletteStyle.entries\n                            SegmentedDropdownItem(\n                                icon = Icons.Rounded.Style,\n                                title = stringResource(R.string.settings_color_style),\n                                items = styles.map { it.name },\n                                selectedIndex = styles.indexOf(colorStyle),\n                                onItemSelected = { index ->\n                                    actions.onSetColorStyle(styles[index].name)\n                                }\n                            )\n                        },\n                        {\n                            val specs = ColorSpec.SpecVersion.entries\n                            SegmentedDropdownItem(\n                                icon = Icons.Rounded.DesignServices,\n                                title = stringResource(R.string.settings_color_spec),\n                                items = specs.map { it.name },\n                                selectedIndex = specs.indexOf(colorSpec).coerceAtLeast(0),\n                                onItemSelected = { index ->\n                                    actions.onSetColorSpec(specs[index].name)\n                                }\n                            )\n                        }\n                    )\n                )\n\n                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {\n                    SegmentedColumn(\n                        modifier = Modifier.padding(top = 4.dp),\n                        content = listOf(\n                            {\n                                SegmentedSwitchItem(\n                                    icon = Icons.Rounded.Adb,\n                                    title = stringResource(id = R.string.settings_enable_predictive_back),\n                                    summary = stringResource(id = R.string.settings_enable_predictive_back_summary),\n                                    checked = uiState.enablePredictiveBack,\n                                    onCheckedChange = actions.onSetEnablePredictiveBack\n                                )\n                            }\n                        )\n                    )\n                }\n\n                TonalCard(modifier = Modifier.padding(top = 4.dp)) {\n                    var sliderValue by remember(uiState.pageScale) { mutableFloatStateOf(uiState.pageScale) }\n\n                    Column(\n                        modifier = Modifier.padding(16.dp),\n                        verticalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            verticalAlignment = Alignment.CenterVertically\n                        ) {\n                            Icon(\n                                Icons.Rounded.AspectRatio,\n                                contentDescription = stringResource(id = R.string.settings_page_scale),\n                                tint = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                            Spacer(modifier = Modifier.width(12.dp))\n                            Column(\n                                modifier = Modifier.weight(1f)\n                            ) {\n                                Text(\n                                    text = stringResource(R.string.settings_page_scale),\n                                    style = MaterialTheme.typography.titleMedium,\n                                    color = MaterialTheme.colorScheme.onSurface\n                                )\n                                Text(\n                                    text = stringResource(id = R.string.settings_page_scale_summary),\n                                    style = MaterialTheme.typography.bodyMedium,\n                                    color = MaterialTheme.colorScheme.outline\n                                )\n                            }\n                            Text(\n                                text = \"${(sliderValue * 100).toInt()}%\",\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.onSurfaceVariant\n                            )\n                        }\n\n                        Slider(\n                            value = sliderValue,\n                            onValueChange = { sliderValue = it },\n                            onValueChangeFinished = { actions.onSetPageScale(sliderValue) },\n                            valueRange = 0.8f..1.1f,\n                            modifier = Modifier.fillMaxWidth()\n                        )\n                    }\n                }\n            }\n\n            Spacer(modifier = Modifier.height(16.dp + navBars.calculateBottomPadding() + captionBar.calculateBottomPadding()))\n        }\n    }\n}\n\n@SuppressLint(\"ConfigurationScreenWidthHeight\")\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ThemePreviewCard(\n    keyColor: Int,\n    isDark: Boolean,\n    paletteStyle: PaletteStyle = PaletteStyle.TonalSpot,\n    colorSpec: ColorSpec.SpecVersion = ColorSpec.SpecVersion.SPEC_2021,\n) {\n    val context = LocalContext.current\n    val configuration = LocalConfiguration.current\n    val screenWidth = configuration.screenWidthDp.toFloat()\n    val screenHeight = configuration.screenHeightDp.toFloat()\n    val screenRatio = screenWidth / screenHeight\n    val dynamicColor = keyColor == 0\n\n    val colorScheme = if (dynamicColor) {\n        val baseScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n        rememberDynamicColorScheme(\n            seedColor = Color.Unspecified,\n            isDark = isDark,\n            style = paletteStyle,\n            specVersion = colorSpec,\n            primary = baseScheme.primary,\n            secondary = baseScheme.secondary,\n            tertiary = baseScheme.tertiary,\n            neutral = baseScheme.surface,\n            neutralVariant = baseScheme.surfaceVariant,\n            error = baseScheme.error\n        )\n    } else {\n        rememberDynamicColorScheme(\n            seedColor = Color(keyColor),\n            isDark = isDark,\n            style = paletteStyle,\n            specVersion = colorSpec,\n        )\n\n    }\n\n    Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {\n        Surface(\n            modifier = Modifier\n                .fillMaxWidth(0.4f)\n                .aspectRatio(screenRatio),\n            color = colorScheme.background,\n            shape = RoundedCornerShape(20.dp),\n            border = BorderStroke(1.dp, color = MaterialTheme.colorScheme.outlineVariant)\n        ) {\n            Column {\n                // top bar\n                Box(\n                    modifier = Modifier\n                        .height(48.dp)\n                        .fillMaxWidth(),\n                    contentAlignment = Alignment.TopStart\n                ) {\n                    Row(\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .padding(start = 12.dp, top = 16.dp, bottom = 8.dp),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Text(\n                            text = stringResource(id = R.string.app_name),\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = colorScheme.onSurface\n                        )\n                    }\n                }\n\n                Box(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .weight(1f),\n                    contentAlignment = Alignment.TopStart\n                ) {\n                    Column(\n                        modifier = Modifier.padding(horizontal = 8.dp),\n                        verticalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        TonalCard(\n                            containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .height(40.dp),\n                            shape = RoundedCornerShape(12.dp),\n                            content = { }\n                        )\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            horizontalArrangement = Arrangement.spacedBy(8.dp)\n                        ) {\n                            TonalCard(\n                                modifier = Modifier\n                                    .weight(1f)\n                                    .height(32.dp),\n                                shape = RoundedCornerShape(12.dp),\n                                content = { }\n                            )\n                            TonalCard(\n                                modifier = Modifier\n                                    .weight(1f)\n                                    .height(32.dp),\n                                shape = RoundedCornerShape(12.dp),\n                                content = { }\n                            )\n                        }\n                        TonalCard(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .height(96.dp),\n                            shape = RoundedCornerShape(12.dp),\n                            content = { }\n                        )\n                    }\n                }\n\n                // bottom bar\n                Surface(\n                    color = colorScheme.surfaceContainer,\n                    modifier = Modifier.fillMaxWidth()\n                ) {\n                    Row(\n                        modifier = Modifier\n                            .height(40.dp)\n                            .fillMaxWidth()\n                            .padding(horizontal = 8.dp),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {\n                            Icon(Icons.Filled.Home, null, tint = colorScheme.primary)\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ColorButtonMaterial(\n    color: Color,\n    isSelected: Boolean,\n    isDark: Boolean,\n    paletteStyle: PaletteStyle = PaletteStyle.TonalSpot,\n    colorSpec: ColorSpec.SpecVersion = ColorSpec.SpecVersion.SPEC_2021,\n    onClick: () -> Unit\n) {\n    val context = LocalContext.current\n    val colorScheme = if (color == Color.Unspecified) {\n        val baseScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n        rememberDynamicColorScheme(\n            seedColor = Color.Unspecified,\n            isDark = isDark,\n            style = paletteStyle,\n            specVersion = colorSpec,\n            primary = baseScheme.primary,\n            secondary = baseScheme.secondary,\n            tertiary = baseScheme.tertiary,\n            neutral = baseScheme.surface,\n            neutralVariant = baseScheme.surfaceVariant,\n            error = baseScheme.error\n        )\n    } else {\n        rememberDynamicColorScheme(\n            seedColor = color,\n            isDark = isDark,\n            style = paletteStyle,\n            specVersion = colorSpec,\n        )\n    }\n\n    Surface(\n        onClick = onClick,\n        shape = RoundedCornerShape(20.dp),\n        color = colorScheme.surfaceContainer,\n        modifier = Modifier.size(72.dp)\n    ) {\n        Box(contentAlignment = Alignment.Center) {\n            Canvas(modifier = Modifier.size(48.dp)) {\n                drawArc(\n                    color = colorScheme.primaryContainer,\n                    startAngle = 180f,\n                    sweepAngle = 180f,\n                    useCenter = true\n                )\n                drawArc(\n                    color = colorScheme.tertiaryContainer,\n                    startAngle = 0f,\n                    sweepAngle = 180f,\n                    useCenter = true\n                )\n            }\n\n            val scale by animateFloatAsState(targetValue = if (isSelected) 1.1f else 1.0f)\n            Box(\n                modifier = Modifier.graphicsLayer {\n                    scaleX = scale\n                    scaleY = scale\n                },\n                contentAlignment = Alignment.Center\n            ) {\n                AnimatedVisibility(\n                    visible = isSelected,\n                    enter = fadeIn() + scaleIn(initialScale = 0.8f),\n                    exit = fadeOut() + scaleOut(targetScale = 0.8f)\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .size(56.dp)\n                            .border(2.dp, colorScheme.primary, CircleShape),\n                        contentAlignment = Alignment.Center\n                    ) {\n                        Box(\n                            modifier = Modifier\n                                .size(24.dp)\n                                .clip(CircleShape)\n                                .background(colorScheme.primary, CircleShape)\n                        ) {\n                            Icon(\n                                imageVector = Icons.Rounded.Check,\n                                contentDescription = null,\n                                tint = colorScheme.onPrimary,\n                                modifier = Modifier\n                                    .align(Alignment.Center)\n                                    .size(16.dp)\n                            )\n                        }\n                    }\n                }\n                AnimatedVisibility(\n                    visible = !isSelected,\n                    enter = fadeIn() + scaleIn(initialScale = 0.8f),\n                    exit = fadeOut() + scaleOut(targetScale = 0.8f)\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .size(20.dp)\n                            .background(colorScheme.primary, CircleShape)\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/colorpalette/ColorPaletteScreenMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.colorpalette\n\nimport android.annotation.SuppressLint\nimport android.os.Build\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.aspectRatio\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Adb\nimport androidx.compose.material.icons.rounded.AspectRatio\nimport androidx.compose.material.icons.rounded.BlurOn\nimport androidx.compose.material.icons.rounded.CallToAction\nimport androidx.compose.material.icons.rounded.Colorize\nimport androidx.compose.material.icons.rounded.DesignServices\nimport androidx.compose.material.icons.rounded.Style\nimport androidx.compose.material.icons.rounded.Wallpaper\nimport androidx.compose.material.icons.rounded.WaterDrop\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.kyant.capsule.ContinuousRoundedRectangle\nimport com.materialkolor.PaletteStyle\nimport com.materialkolor.dynamiccolor.ColorSpec\nimport com.materialkolor.rememberDynamicColorScheme\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.miuix.ScaleDialog\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.theme.keyColorOptions\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport top.yukonga.miuix.kmp.basic.Card\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.IconButton\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.Slider\nimport top.yukonga.miuix.kmp.basic.SliderDefaults\nimport top.yukonga.miuix.kmp.basic.TabRow\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TopAppBar\nimport top.yukonga.miuix.kmp.extra.SuperArrow\nimport top.yukonga.miuix.kmp.extra.SuperDropdown\nimport top.yukonga.miuix.kmp.extra.SuperSwitch\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.extended.Back\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\n\n@Composable\nfun ColorPaletteScreenMiuix(\n    state: ColorPaletteUiState,\n    actions: ColorPaletteScreenActions,\n) {\n    val scrollBehavior = MiuixScrollBehavior()\n    val enableBlurState = LocalEnableBlur.current\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlurState) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n    val uiState = state.uiState\n    val currentColorMode = state.currentColorMode\n    val isDark = currentColorMode.isDark || currentColorMode.isSystem && isSystemInDarkTheme()\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                modifier = if (enableBlurState) {\n                    Modifier.defaultHazeEffect(hazeState, hazeStyle)\n                } else {\n                    Modifier\n                },\n                color = if (enableBlurState) Color.Transparent else colorScheme.surface,\n                title = stringResource(R.string.settings_theme),\n                navigationIcon = {\n                    IconButton(\n                        modifier = Modifier.padding(start = 16.dp),\n                        onClick = actions.onBack\n                    ) {\n                        val layoutDirection = LocalLayoutDirection.current\n                        Icon(\n                            modifier = Modifier.graphicsLayer {\n                                if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\n                            },\n                            imageVector = MiuixIcons.Back,\n                            contentDescription = null,\n                            tint = colorScheme.onBackground\n                        )\n                    }\n                },\n                scrollBehavior = scrollBehavior\n            )\n        },\n        popupHost = { },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        val showScaleDialog = rememberSaveable { mutableStateOf(false) }\n\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxHeight()\n                .overScrollVertical()\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .let { if (enableBlurState) it.hazeSource(state = hazeState) else it }\n                .padding(horizontal = 12.dp),\n            contentPadding = innerPadding,\n            overscrollEffect = null,\n        ) {\n            item {\n                Spacer(modifier = Modifier.height(32.dp))\n                ThemePreviewCardMiuix(\n                    keyColor = uiState.keyColor,\n                    isDark = isDark,\n                    miuixMonet = uiState.miuixMonet,\n                    enableFloatingBottomBar = uiState.enableFloatingBottomBar,\n                    enableFloatingBottomBarBlur = uiState.enableFloatingBottomBarBlur,\n                    paletteStyle = state.currentPaletteStyle,\n                    colorSpec = state.currentColorSpec,\n                )\n                Spacer(modifier = Modifier.height(72.dp))\n\n                val themeItems = listOf(\n                    stringResource(id = R.string.settings_theme_mode_system),\n                    stringResource(id = R.string.settings_theme_mode_light),\n                    stringResource(id = R.string.settings_theme_mode_dark),\n                )\n                TabRow(\n                    tabs = themeItems,\n                    selectedTabIndex = (if (uiState.themeMode >= 3) uiState.themeMode - 3 else uiState.themeMode).coerceIn(0, 2),\n                    onTabSelected = { index ->\n                        actions.onSetThemeMode(index)\n                    },\n                    height = 48.dp,\n                )\n\n                Card(\n                    modifier = Modifier\n                        .padding(top = 12.dp)\n                        .fillMaxWidth(),\n                ) {\n                    SuperSwitch(\n                        title = stringResource(id = R.string.settings_monet),\n                        startAction = {\n                            Icon(\n                                Icons.Rounded.Wallpaper,\n                                modifier = Modifier.padding(end = 6.dp),\n                                contentDescription = stringResource(id = R.string.settings_monet),\n                                tint = colorScheme.onBackground\n                            )\n                        },\n                        checked = uiState.miuixMonet,\n                        onCheckedChange = {\n                            actions.onSetMiuixMonet(it)\n                        }\n                    )\n\n                    AnimatedVisibility(\n                        visible = uiState.miuixMonet\n                    ) {\n                        Column {\n                            val colorItems = listOf(\n                                stringResource(id = R.string.settings_key_color_default),\n                                stringResource(id = R.string.color_red),\n                                stringResource(id = R.string.color_pink),\n                                stringResource(id = R.string.color_purple),\n                                stringResource(id = R.string.color_deep_purple),\n                                stringResource(id = R.string.color_indigo),\n                                stringResource(id = R.string.color_blue),\n                                stringResource(id = R.string.color_cyan),\n                                stringResource(id = R.string.color_teal),\n                                stringResource(id = R.string.color_green),\n                                stringResource(id = R.string.color_yellow),\n                                stringResource(id = R.string.color_amber),\n                                stringResource(id = R.string.color_orange),\n                                stringResource(id = R.string.color_brown),\n                                stringResource(id = R.string.color_blue_grey),\n                                stringResource(id = R.string.color_sakura),\n                            )\n                            val colorValues = listOf(0) + keyColorOptions\n                            SuperDropdown(\n                                title = stringResource(id = R.string.settings_key_color),\n                                items = colorItems,\n                                startAction = {\n                                    Icon(\n                                        Icons.Rounded.Colorize,\n                                        modifier = Modifier.padding(end = 6.dp),\n                                        contentDescription = stringResource(id = R.string.settings_key_color),\n                                        tint = colorScheme.onBackground\n                                    )\n                                },\n                                selectedIndex = colorValues.indexOf(uiState.keyColor).takeIf { it >= 0 } ?: 0,\n                                onSelectedIndexChange = { index ->\n                                    actions.onSetKeyColor(colorValues[index])\n                                }\n                            )\n\n                            AnimatedVisibility(\n                                visible = uiState.keyColor != 0\n                            ) {\n                                Column {\n                                    val styles = PaletteStyle.entries\n                                    SuperDropdown(\n                                        title = stringResource(R.string.settings_color_style),\n                                        startAction = {\n                                            Icon(\n                                                Icons.Rounded.Style,\n                                                modifier = Modifier.padding(end = 6.dp),\n                                                contentDescription = stringResource(id = R.string.settings_color_style),\n                                                tint = colorScheme.onBackground\n                                            )\n                                        },\n                                        items = styles.map { it.name },\n                                        selectedIndex = styles.indexOfFirst { it.name == uiState.colorStyle }.coerceAtLeast(0),\n                                        onSelectedIndexChange = { index ->\n                                            actions.onSetColorStyle(styles[index].name)\n                                        }\n                                    )\n\n                                    val specs = ColorSpec.SpecVersion.entries\n                                    SuperDropdown(\n                                        title = stringResource(R.string.settings_color_spec),\n                                        startAction = {\n                                            Icon(\n                                                Icons.Rounded.DesignServices,\n                                                modifier = Modifier.padding(end = 6.dp),\n                                                contentDescription = stringResource(id = R.string.settings_color_spec),\n                                                tint = colorScheme.onBackground\n                                            )\n                                        },\n                                        items = specs.map { it.name },\n                                        selectedIndex = specs.indexOfFirst { it.name == uiState.colorSpec }.coerceAtLeast(0),\n                                        onSelectedIndexChange = { index ->\n                                            actions.onSetColorSpec(specs[index].name)\n                                        }\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n\n                Card(\n                    modifier = Modifier\n                        .padding(top = 12.dp)\n                        .fillMaxWidth(),\n                ) {\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                        SuperSwitch(\n                            title = stringResource(id = R.string.settings_enable_blur),\n                            summary = stringResource(id = R.string.settings_enable_blur_summary),\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.BlurOn,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = stringResource(id = R.string.settings_enable_blur),\n                                    tint = colorScheme.onBackground\n                                )\n                            },\n                            checked = uiState.enableBlur,\n                            onCheckedChange = {\n                                actions.onSetEnableBlur(it)\n                            }\n                        )\n                    }\n                    SuperSwitch(\n                        title = stringResource(id = R.string.settings_floating_bottom_bar),\n                        summary = stringResource(id = R.string.settings_floating_bottom_bar_summary),\n                        startAction = {\n                            Icon(\n                                Icons.Rounded.CallToAction,\n                                modifier = Modifier.padding(end = 6.dp),\n                                contentDescription = stringResource(id = R.string.settings_floating_bottom_bar),\n                                tint = colorScheme.onBackground\n                            )\n                        },\n                        checked = uiState.enableFloatingBottomBar,\n                        onCheckedChange = {\n                            actions.onSetEnableFloatingBottomBar(it)\n                        }\n                    )\n                    AnimatedVisibility(visible = uiState.enableFloatingBottomBar && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n                        SuperSwitch(\n                            title = stringResource(id = R.string.settings_enable_glass),\n                            summary = stringResource(id = R.string.settings_enable_glass_summary),\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.WaterDrop,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = stringResource(id = R.string.settings_enable_glass),\n                                    tint = colorScheme.onBackground\n                                )\n                            },\n                            checked = uiState.enableFloatingBottomBarBlur,\n                            onCheckedChange = {\n                                actions.onSetEnableFloatingBottomBarBlur(it)\n                            }\n                        )\n                    }\n                }\n\n                Card(\n                    modifier = Modifier\n                        .padding(top = 12.dp)\n                        .fillMaxWidth(),\n                ) {\n                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {\n                        SuperSwitch(\n                            title = stringResource(id = R.string.settings_enable_predictive_back),\n                            summary = stringResource(id = R.string.settings_enable_predictive_back_summary),\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.Adb,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = stringResource(id = R.string.settings_enable_predictive_back),\n                                    tint = colorScheme.onBackground\n                                )\n                            },\n                            checked = uiState.enablePredictiveBack,\n                            onCheckedChange = {\n                                actions.onSetEnablePredictiveBack(it)\n                            }\n                        )\n                    }\n\n                    var sliderValue by remember(uiState.pageScale) { mutableFloatStateOf(uiState.pageScale) }\n                    SuperArrow(\n                        title = stringResource(id = R.string.settings_page_scale),\n                        summary = stringResource(id = R.string.settings_page_scale_summary),\n                        startAction = {\n                            Icon(\n                                Icons.Rounded.AspectRatio,\n                                modifier = Modifier.padding(end = 6.dp),\n                                contentDescription = stringResource(id = R.string.settings_page_scale),\n                                tint = colorScheme.onBackground\n                            )\n                        },\n                        endActions = {\n                            Text(\n                                text = \"${(sliderValue * 100).toInt()}%\",\n                                color = colorScheme.onSurfaceVariantActions,\n                            )\n                        },\n                        onClick = { showScaleDialog.value = !showScaleDialog.value },\n                        holdDownState = showScaleDialog.value,\n                        bottomAction = {\n                            Slider(\n                                value = sliderValue,\n                                onValueChange = {\n                                    sliderValue = it\n                                },\n                                onValueChangeFinished = {\n                                    actions.onSetPageScale(sliderValue)\n                                },\n                                valueRange = 0.8f..1.1f,\n                                showKeyPoints = true,\n                                keyPoints = listOf(0.8f, 0.9f, 1f, 1.1f),\n                                magnetThreshold = 0.01f,\n                                hapticEffect = SliderDefaults.SliderHapticEffect.Step,\n                            )\n                        },\n                    )\n                    ScaleDialog(\n                        show = showScaleDialog.value,\n                        onDismissRequest = { showScaleDialog.value = false },\n                        volumeState = { uiState.pageScale },\n                        onVolumeChange = {\n                            actions.onSetPageScale(it)\n                        }\n                    )\n                }\n            }\n            item {\n                Spacer(\n                    Modifier.height(\n                        WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                                WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() +\n                                12.dp\n                    )\n                )\n            }\n        }\n    }\n}\n\n@SuppressLint(\"ConfigurationScreenWidthHeight\")\n@Composable\nprivate fun ThemePreviewCardMiuix(\n    keyColor: Int,\n    isDark: Boolean,\n    miuixMonet: Boolean,\n    enableFloatingBottomBar: Boolean = false,\n    enableFloatingBottomBarBlur: Boolean = false,\n    paletteStyle: PaletteStyle = PaletteStyle.TonalSpot,\n    colorSpec: ColorSpec.SpecVersion = ColorSpec.SpecVersion.SPEC_2021,\n) {\n    val configuration = LocalConfiguration.current\n    val screenWidth = configuration.screenWidthDp.toFloat()\n    val screenHeight = configuration.screenHeightDp.toFloat()\n    val screenRatio = screenWidth / screenHeight\n\n    val seedColor = if (keyColor == 0) colorScheme.primary else Color(keyColor)\n    val effectiveStyle = if (keyColor == 0) PaletteStyle.TonalSpot else paletteStyle\n    val effectiveSpec = if (keyColor == 0) ColorSpec.SpecVersion.Default else colorSpec\n    val dynamicCs = rememberDynamicColorScheme(\n        seedColor = seedColor,\n        isDark = isDark,\n        style = effectiveStyle,\n        specVersion = effectiveSpec,\n    )\n\n    val bgColor = if (miuixMonet) dynamicCs.background else colorScheme.surface\n    val textColor = if (miuixMonet) dynamicCs.onSurface else colorScheme.onBackground\n    val accentCardColor = when {\n        miuixMonet -> dynamicCs.secondaryContainer\n        isDark -> Color(0xFF1A3825)\n        else -> Color(0xFFDFFAE4)\n    }\n    val cardColor = if (miuixMonet) dynamicCs.surfaceContainerHighest else colorScheme.surfaceVariant\n    val navBarColor = if (miuixMonet) dynamicCs.surfaceContainer else colorScheme.surface\n    val iconColor = if (miuixMonet) dynamicCs.primary else colorScheme.primary\n    val navSelectedColor = colorScheme.onSurfaceContainer\n    val navUnselectedColor = colorScheme.onSurfaceContainer.copy(alpha = 0.5f)\n\n    Box(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(top = 12.dp),\n        contentAlignment = Alignment.TopCenter\n    ) {\n        Box(\n            modifier = Modifier\n                .fillMaxWidth(0.4f)\n                .aspectRatio(screenRatio)\n                .clip(ContinuousRoundedRectangle(20.dp))\n                .background(bgColor)\n                .border(1.dp, colorScheme.outline, ContinuousRoundedRectangle(20.dp))\n        ) {\n            Column {\n                Row(\n                    modifier = Modifier\n                        .height(48.dp)\n                        .fillMaxWidth()\n                        .padding(start = 12.dp, top = 24.dp),\n                    verticalAlignment = Alignment.CenterVertically\n                ) {\n                    Text(\n                        text = stringResource(id = R.string.app_name),\n                        fontSize = 12.sp,\n                        color = textColor\n                    )\n                }\n\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(65.dp)\n                        .padding(horizontal = 8.dp),\n                    horizontalArrangement = Arrangement.spacedBy(6.dp)\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .weight(1f)\n                            .fillMaxHeight()\n                            .clip(RoundedCornerShape(6.dp))\n                            .background(accentCardColor)\n                    )\n                    Column(\n                        modifier = Modifier\n                            .weight(1f)\n                            .fillMaxHeight(),\n                        verticalArrangement = Arrangement.spacedBy(6.dp)\n                    ) {\n                        Box(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .weight(1f)\n                                .clip(RoundedCornerShape(6.dp))\n                                .background(cardColor)\n                        )\n                        Box(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .weight(1f)\n                                .clip(RoundedCornerShape(6.dp))\n                                .background(cardColor)\n                        )\n                    }\n                }\n\n                Column(\n                    modifier = Modifier\n                        .weight(1f)\n                        .padding(horizontal = 8.dp, vertical = 6.dp),\n                    verticalArrangement = Arrangement.spacedBy(6.dp)\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .weight(0.8f)\n                            .clip(RoundedCornerShape(6.dp))\n                            .background(cardColor)\n                    )\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .weight(.1f)\n                            .clip(RoundedCornerShape(6.dp))\n                            .background(cardColor)\n                    )\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .weight(.1f)\n                            .clip(RoundedCornerShape(6.dp))\n                            .background(cardColor)\n                    )\n                }\n\n            }\n\n            if (enableFloatingBottomBar) {\n                Box(\n                    modifier = Modifier\n                        .align(Alignment.BottomCenter)\n                        .padding(bottom = 8.dp),\n                ) {\n                    Row(\n                        modifier = Modifier\n                            .height(28.dp)\n                            .clip(RoundedCornerShape(14.dp))\n                            .background(\n                                if (enableFloatingBottomBarBlur) navBarColor.copy(alpha = 0.5f)\n                                else navBarColor\n                            )\n                            .border(0.5.dp, textColor.copy(alpha = 0.1f), RoundedCornerShape(14.dp))\n                            .padding(horizontal = 12.dp),\n                        horizontalArrangement = Arrangement.spacedBy(10.dp),\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        repeat(4) {\n                            Box(\n                                modifier = Modifier\n                                    .size(13.dp)\n                                    .clip(RoundedCornerShape(2.dp))\n                                    .background(if (it == 0) iconColor else textColor)\n                            )\n                        }\n                    }\n                }\n            } else {\n                Column(\n                    modifier = Modifier\n                        .align(Alignment.BottomCenter)\n                        .fillMaxWidth()\n                ) {\n                    Box(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .height(0.5.dp)\n                            .background(textColor.copy(alpha = 0.1f))\n                    )\n                    Row(\n                        modifier = Modifier\n                            .height(36.dp)\n                            .fillMaxWidth()\n                            .background(navBarColor)\n                            .padding(top = 2.dp, bottom = 8.dp),\n                        horizontalArrangement = Arrangement.SpaceEvenly,\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        repeat(4) {\n                            Box(\n                                modifier = Modifier\n                                    .size(15.dp)\n                                    .clip(RoundedCornerShape(3.dp))\n                                    .background(if (it == 0) navSelectedColor else navUnselectedColor)\n                            )\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/colorpalette/ColorPaletteUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.colorpalette\n\nimport androidx.compose.runtime.Immutable\nimport com.materialkolor.PaletteStyle\nimport com.materialkolor.dynamiccolor.ColorSpec\nimport me.weishu.kernelsu.ui.screen.settings.SettingsUiState\nimport me.weishu.kernelsu.ui.theme.ColorMode\n\n@Immutable\ndata class ColorPaletteUiState(\n    val uiState: SettingsUiState,\n    val currentColorMode: ColorMode,\n    val currentPaletteStyle: PaletteStyle,\n    val currentColorSpec: ColorSpec.SpecVersion,\n)\n\n@Immutable\ndata class ColorPaletteScreenActions(\n    val onBack: () -> Unit,\n    val onSetThemeMode: (Int) -> Unit,\n    val onSetMiuixMonet: (Boolean) -> Unit,\n    val onSetKeyColor: (Int) -> Unit,\n    val onSetColorMode: (ColorMode) -> Unit,\n    val onSetColorStyle: (String) -> Unit,\n    val onSetColorSpec: (String) -> Unit,\n    val onSetEnableBlur: (Boolean) -> Unit,\n    val onSetEnableFloatingBottomBar: (Boolean) -> Unit,\n    val onSetEnableFloatingBottomBarBlur: (Boolean) -> Unit,\n    val onSetEnablePredictiveBack: (Boolean) -> Unit,\n    val onSetPageScale: (Float) -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/executemoduleaction/ExecuteModuleActionMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.executemoduleaction\n\nimport android.annotation.SuppressLint\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.KeyEventBlocker\n\n@SuppressLint(\"LocalContextGetResourceValueCall\")\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun ExecuteModuleActionScreenMaterial(\n    state: ExecuteModuleActionUiState,\n    actions: ExecuteModuleActionScreenActions,\n) {\n    val scrollState = rememberScrollState()\n\n    BackHandler { }\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = { Text(stringResource(R.string.action)) },\n                navigationIcon = {\n                    IconButton(onClick = actions.onBack) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                            contentDescription = null\n                        )\n                    }\n                },\n                actions = {\n                    IconButton(onClick = actions.onSaveLog) {\n                        Icon(\n                            imageVector = Icons.Filled.Save,\n                            contentDescription = stringResource(R.string.save_log)\n                        )\n                    }\n                }\n            )\n        },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout)\n            .only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        val layoutDirection = LocalLayoutDirection.current\n        val navBars = WindowInsets.navigationBars.asPaddingValues()\n        val captionBar = WindowInsets.captionBar.asPaddingValues()\n        KeyEventBlocker {\n            it.key == Key.VolumeDown || it.key == Key.VolumeUp\n        }\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(\n                    start = innerPadding.calculateStartPadding(layoutDirection),\n                    end = innerPadding.calculateEndPadding(layoutDirection),\n                )\n                .verticalScroll(scrollState)\n        ) {\n            LaunchedEffect(state.text) {\n                scrollState.animateScrollTo(scrollState.maxValue)\n            }\n            Spacer(Modifier.height(innerPadding.calculateTopPadding()))\n            Text(\n                modifier = Modifier.padding(8.dp),\n                text = state.text,\n                style = MaterialTheme.typography.bodySmall,\n                fontFamily = FontFamily.Monospace,\n            )\n            Spacer(\n                Modifier.height(\n                    16.dp + navBars.calculateBottomPadding() + captionBar.calculateBottomPadding()\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/executemoduleaction/ExecuteModuleActionMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.executemoduleaction\n\nimport androidx.activity.compose.BackHandler\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.KeyEventBlocker\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.IconButton\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.SmallTopAppBar\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.extended.Back\nimport top.yukonga.miuix.kmp.icon.extended.Download\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\n\n@Composable\nfun ExecuteModuleActionScreenMiuix(\n    state: ExecuteModuleActionUiState,\n    actions: ExecuteModuleActionScreenActions,\n) {\n    val scrollState = rememberScrollState()\n    val enableBlur = LocalEnableBlur.current\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n\n    BackHandler { }\n\n    Scaffold(\n        topBar = {\n            TopBar(\n                onBack = actions.onBack,\n                onSave = actions.onSaveLog,\n                hazeState = hazeState,\n                hazeStyle = hazeStyle,\n                enableBlur = enableBlur,\n            )\n        },\n        popupHost = { },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout)\n            .only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        val layoutDirection = LocalLayoutDirection.current\n        KeyEventBlocker {\n            it.key == Key.VolumeDown || it.key == Key.VolumeUp\n        }\n        Column(\n            modifier = Modifier\n                .fillMaxSize(1f)\n                .scrollEndHaptic()\n                .let { if (enableBlur) it.hazeSource(state = hazeState) else it }\n                .padding(\n                    start = innerPadding.calculateStartPadding(layoutDirection),\n                    end = innerPadding.calculateEndPadding(layoutDirection),\n                )\n                .verticalScroll(scrollState),\n        ) {\n            LaunchedEffect(state.text) {\n                scrollState.animateScrollTo(scrollState.maxValue)\n            }\n            Spacer(Modifier.height(innerPadding.calculateTopPadding()))\n            Text(\n                modifier = Modifier.padding(8.dp),\n                text = state.text,\n                fontSize = 12.sp,\n                fontFamily = FontFamily.Monospace,\n            )\n            Spacer(\n                Modifier.height(\n                    12.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                            WindowInsets.captionBar.asPaddingValues().calculateBottomPadding()\n                )\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun TopBar(\n    onBack: () -> Unit = {},\n    onSave: () -> Unit = {},\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n    enableBlur: Boolean\n) {\n    SmallTopAppBar(\n        modifier = if (enableBlur) {\n            Modifier.defaultHazeEffect(hazeState, hazeStyle)\n        } else {\n            Modifier\n        },\n        title = stringResource(R.string.action),\n        navigationIcon = {\n            IconButton(\n                modifier = Modifier.padding(start = 16.dp),\n                onClick = onBack\n            ) {\n                val layoutDirection = LocalLayoutDirection.current\n                Icon(\n                    modifier = Modifier.graphicsLayer {\n                        if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\n                    },\n                    imageVector = MiuixIcons.Back,\n                    contentDescription = null,\n                    tint = colorScheme.onBackground\n                )\n            }\n        },\n        actions = {\n            IconButton(\n                modifier = Modifier.padding(end = 16.dp),\n                onClick = onSave\n            ) {\n                Icon(\n                    imageVector = MiuixIcons.Download,\n                    contentDescription = stringResource(id = R.string.save_log),\n                    tint = colorScheme.onBackground\n                )\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/executemoduleaction/ExecuteModuleActionScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.executemoduleaction\n\nimport androidx.activity.compose.LocalActivity\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.lifecycle.compose.dropUnlessResumed\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\n\n@Composable\nfun ExecuteModuleActionScreen(moduleId: String, fromShortcut: Boolean = false) {\n    val navigator = LocalNavigator.current\n    val context = LocalContext.current\n    val activity = LocalActivity.current\n    val scope = rememberCoroutineScope()\n    var text by rememberSaveable { mutableStateOf(\"\") }\n    val logContent = rememberSaveable { StringBuilder() }\n\n    val exitExecute = {\n        if (fromShortcut && activity != null) {\n            activity.finishAndRemoveTask()\n        } else {\n            navigator.pop()\n        }\n    }\n\n    ExecuteModuleActionEffect(\n        moduleId = moduleId,\n        text = text,\n        logContent = logContent,\n        fromShortcut = fromShortcut,\n        onTextUpdate = { text = it },\n        onExit = exitExecute\n    )\n\n    val state = ExecuteModuleActionUiState(\n        text = text,\n    )\n    val actions = ExecuteModuleActionScreenActions(\n        onBack = dropUnlessResumed { navigator.pop() },\n        onSaveLog = saveLog(logContent, context, scope),\n    )\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> ExecuteModuleActionScreenMiuix(state, actions)\n        UiMode.Material -> ExecuteModuleActionScreenMaterial(state, actions)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/executemoduleaction/ExecuteModuleActionUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.executemoduleaction\n\nimport androidx.compose.runtime.Immutable\n\n@Immutable\ndata class ExecuteModuleActionUiState(\n    val text: String,\n)\n\n@Immutable\ndata class ExecuteModuleActionScreenActions(\n    val onBack: () -> Unit,\n    val onSaveLog: () -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/executemoduleaction/ExecuteModuleActionUtils.kt",
    "content": "package me.weishu.kernelsu.ui.screen.executemoduleaction\n\nimport android.content.Context\nimport android.os.Environment\nimport android.os.Handler\nimport android.os.Looper\nimport android.widget.Toast\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.data.repository.ModuleRepositoryImpl\nimport me.weishu.kernelsu.ui.util.runModuleAction\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\n@Composable\nfun ExecuteModuleActionEffect(\n    moduleId: String,\n    text: String,\n    logContent: StringBuilder,\n    fromShortcut: Boolean,\n    onTextUpdate: (String) -> Unit,\n    onExit: () -> Unit\n) {\n    val context = LocalContext.current\n    val noModule = stringResource(R.string.no_such_module)\n    val moduleUnavailable = stringResource(R.string.module_unavailable)\n    val moduleActionSuccess = stringResource(R.string.module_action_success)\n\n    LaunchedEffect(Unit) {\n        if (text.isNotEmpty()) {\n            return@LaunchedEffect\n        }\n        val repo = ModuleRepositoryImpl()\n        val modules = repo.getModules().getOrDefault(emptyList())\n        val moduleInfo = modules.find { info -> info.id == moduleId }\n        if (moduleInfo == null) {\n            Toast.makeText(context, noModule.format(moduleId), Toast.LENGTH_SHORT).show()\n            onExit()\n            return@LaunchedEffect\n        }\n        if (!moduleInfo.hasActionScript) {\n            onExit()\n            return@LaunchedEffect\n        }\n        if (!moduleInfo.enabled || moduleInfo.update || moduleInfo.remove) {\n            Toast.makeText(context, moduleUnavailable.format(moduleInfo.name), Toast.LENGTH_SHORT).show()\n            onExit()\n            return@LaunchedEffect\n        }\n        var actionResult: Boolean\n        var currentText = text\n        val mainHandler = Handler(Looper.getMainLooper())\n        withContext(Dispatchers.IO) {\n            runModuleAction(\n                moduleId = moduleId,\n                onStdout = {\n                    val tempText = \"$it\\n\"\n                    if (tempText.startsWith(\"\u001b[H\u001b[J\")) { // clear command\n                        currentText = tempText.substring(6)\n                    } else {\n                        currentText += tempText\n                    }\n                    mainHandler.post {\n                        onTextUpdate(currentText)\n                    }\n                    logContent.append(it).append(\"\\n\")\n                },\n                onStderr = {\n                    logContent.append(it).append(\"\\n\")\n                }\n            ).let {\n                actionResult = it\n            }\n        }\n        if (actionResult) {\n            if (fromShortcut) {\n                Toast.makeText(\n                    context,\n                    moduleActionSuccess,\n                    Toast.LENGTH_SHORT\n                ).show()\n            }\n            onExit()\n        }\n    }\n}\n\nfun saveLog(\n    logContent: StringBuilder,\n    context: Context,\n    scope: CoroutineScope\n): () -> Unit {\n    return {\n        scope.launch {\n            val format = SimpleDateFormat(\"yyyy-MM-dd-HH-mm-ss\", Locale.getDefault())\n            val date = format.format(Date())\n            val file = File(\n                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),\n                \"KernelSU_module_action_log_${date}.log\"\n            )\n            file.writeText(logContent.toString())\n            Toast.makeText(context, \"Log saved to ${file.absolutePath}\", Toast.LENGTH_SHORT).show()\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/flash/FlashMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.flash\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Refresh\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExtendedFloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBar\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.KeyEventBlocker\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun FlashScreenMaterial(\n    state: FlashUiState,\n    actions: FlashScreenActions,\n) {\n    val scrollState = rememberScrollState()\n    if (state.showJailbreakWarning) {\n        JailbreakFlashWarningDialog(\n            onConfirm = actions.onConfirmJailbreakWarning,\n            onDismiss = actions.onDismissJailbreakWarning,\n        )\n    }\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                title = {\n                    Text(\n                        stringResource(\n                            when (state.flashingStatus) {\n                                FlashingStatus.FLASHING -> R.string.flashing\n                                FlashingStatus.SUCCESS -> R.string.flash_success\n                                FlashingStatus.FAILED -> R.string.flash_failed\n                            }\n                        )\n                    )\n                },\n                navigationIcon = {\n                    IconButton(onClick = actions.onBack) {\n                        Icon(Icons.AutoMirrored.Filled.ArrowBack, null)\n                    }\n                },\n                actions = {\n                    IconButton(onClick = actions.onSaveLog) {\n                        Icon(Icons.Filled.Save, stringResource(R.string.save_log))\n                    }\n                }\n            )\n        },\n        floatingActionButton = {\n            if (state.showRebootAction) {\n                ExtendedFloatingActionButton(\n                    onClick = actions.onReboot,\n                    icon = { Icon(Icons.Filled.Refresh, null) },\n                    text = { Text(stringResource(R.string.reboot)) },\n                    modifier = Modifier.padding(\n                        bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                                WindowInsets.captionBar.asPaddingValues().calculateBottomPadding(),\n                    )\n                )\n            }\n        },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        val layoutDirection = LocalLayoutDirection.current\n        val navBars = WindowInsets.navigationBars.asPaddingValues()\n        val captionBar = WindowInsets.captionBar.asPaddingValues()\n        KeyEventBlocker {\n            it.key == Key.VolumeDown || it.key == Key.VolumeUp\n        }\n\n        Column(\n            modifier = Modifier\n                .fillMaxSize()\n                .padding(\n                    start = innerPadding.calculateStartPadding(layoutDirection),\n                    end = innerPadding.calculateEndPadding(layoutDirection),\n                )\n                .verticalScroll(scrollState)\n        ) {\n            LaunchedEffect(state.text) {\n                scrollState.animateScrollTo(scrollState.maxValue)\n            }\n            Spacer(Modifier.height(innerPadding.calculateTopPadding()))\n            Text(\n                modifier = Modifier.padding(8.dp),\n                text = state.text,\n                style = MaterialTheme.typography.bodySmall,\n                fontFamily = FontFamily.Monospace,\n            )\n            Spacer(\n                Modifier.height(\n                    16.dp + 54.dp + navBars.calculateBottomPadding() + captionBar.calculateBottomPadding()\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/flash/FlashMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.flash\n\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Refresh\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.key.Key\nimport androidx.compose.ui.input.key.key\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.KeyEventBlocker\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport top.yukonga.miuix.kmp.basic.FloatingActionButton\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.IconButton\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.SmallTopAppBar\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.extended.Back\nimport top.yukonga.miuix.kmp.icon.extended.Share\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\n\n/**\n * @author weishu\n * @date 2023/1/1.\n */\n\n\n// Lets you flash modules sequentially when mutiple zipUris are selected\n@Composable\nfun FlashScreenMiuix(\n    state: FlashUiState,\n    actions: FlashScreenActions,\n) {\n    val enableBlur = LocalEnableBlur.current\n    val scrollState = rememberScrollState()\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n\n    if (state.showJailbreakWarning) {\n        JailbreakFlashWarningDialog(\n            onConfirm = actions.onConfirmJailbreakWarning,\n            onDismiss = actions.onDismissJailbreakWarning,\n        )\n    }\n\n    Scaffold(\n        topBar = {\n            TopBar(\n                state.flashingStatus,\n                onBack = actions.onBack,\n                onSave = actions.onSaveLog,\n                hazeState = hazeState,\n                hazeStyle = hazeStyle,\n                enableBlur = enableBlur,\n            )\n        },\n        floatingActionButton = {\n            if (state.showRebootAction) {\n                val reboot = stringResource(id = R.string.reboot)\n                FloatingActionButton(\n                    modifier = Modifier\n                        .padding(\n                            bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                                    WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + 20.dp,\n                            end = 20.dp\n                        )\n                        .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape),\n                    onClick = actions.onReboot,\n                    shadowElevation = 0.dp,\n                    content = {\n                        Icon(\n                            Icons.Rounded.Refresh,\n                            reboot,\n                            Modifier.size(40.dp),\n                            tint = colorScheme.onPrimary\n                        )\n                    },\n                )\n            }\n        },\n        popupHost = { },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        val layoutDirection = LocalLayoutDirection.current\n        KeyEventBlocker {\n            it.key == Key.VolumeDown || it.key == Key.VolumeUp\n        }\n\n        Column(\n            modifier = Modifier\n                .fillMaxSize(1f)\n                .scrollEndHaptic()\n                .let { if (enableBlur) it.hazeSource(state = hazeState) else it }\n                .padding(\n                    start = innerPadding.calculateStartPadding(layoutDirection),\n                    end = innerPadding.calculateEndPadding(layoutDirection),\n                )\n                .verticalScroll(scrollState),\n        ) {\n            LaunchedEffect(state.text) {\n                scrollState.animateScrollTo(scrollState.maxValue)\n            }\n            Spacer(Modifier.height(innerPadding.calculateTopPadding()))\n            Text(\n                modifier = Modifier.padding(8.dp),\n                text = state.text,\n                fontSize = 12.sp,\n                fontFamily = FontFamily.Monospace,\n            )\n            Spacer(\n                Modifier.height(\n                    12.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                            WindowInsets.captionBar.asPaddingValues().calculateBottomPadding()\n                )\n            )\n        }\n    }\n}\n\n\n@Composable\nprivate fun TopBar(\n    status: FlashingStatus,\n    onBack: () -> Unit = {},\n    onSave: () -> Unit = {},\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n    enableBlur: Boolean\n) {\n    SmallTopAppBar(\n        modifier = if (enableBlur) {\n            Modifier.defaultHazeEffect(hazeState, hazeStyle)\n        } else {\n            Modifier\n        },\n        title = stringResource(\n            when (status) {\n                FlashingStatus.FLASHING -> R.string.flashing\n                FlashingStatus.SUCCESS -> R.string.flash_success\n                FlashingStatus.FAILED -> R.string.flash_failed\n            }\n        ),\n        color = if (enableBlur) Color.Transparent else colorScheme.surface,\n        navigationIcon = {\n            IconButton(\n                modifier = Modifier.padding(start = 16.dp),\n                onClick = onBack\n            ) {\n                val layoutDirection = LocalLayoutDirection.current\n                Icon(\n                    modifier = Modifier.graphicsLayer {\n                        if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\n                    },\n                    imageVector = MiuixIcons.Back,\n                    contentDescription = null,\n                    tint = colorScheme.onBackground\n                )\n            }\n        },\n        actions = {\n            IconButton(\n                modifier = Modifier.padding(end = 16.dp),\n                onClick = onSave\n            ) {\n                Icon(\n                    imageVector = MiuixIcons.Share,\n                    contentDescription = stringResource(id = R.string.save_log),\n                    tint = colorScheme.onBackground\n                )\n            }\n        },\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/flash/FlashScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.flash\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.lifecycle.compose.dropUnlessResumed\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.util.reboot\n\n@Composable\nfun FlashScreen(flashIt: FlashIt) {\n    val navigator = LocalNavigator.current\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    var text by rememberSaveable { mutableStateOf(\"\") }\n    val logContent = rememberSaveable { StringBuilder() }\n    var showRebootAction by rememberSaveable { mutableStateOf(false) }\n    var flashingStatus by rememberSaveable { mutableStateOf(FlashingStatus.FLASHING) }\n    val needJailbreakWarning = flashIt is FlashIt.FlashBoot && Natives.isLateLoadMode\n    var flashingEnabled by rememberSaveable { mutableStateOf(!needJailbreakWarning) }\n\n    FlashEffect(\n        flashIt = flashIt,\n        text = text,\n        logContent = logContent,\n        onTextUpdate = { text = it },\n        onShowRebootChange = { showRebootAction = it },\n        onFlashingStatusChange = { flashingStatus = it },\n        enabled = flashingEnabled,\n    )\n\n    val state = FlashUiState(\n        text = text,\n        showRebootAction = showRebootAction,\n        flashingStatus = flashingStatus,\n        showJailbreakWarning = needJailbreakWarning && !flashingEnabled,\n    )\n    val actions = FlashScreenActions(\n        onBack = dropUnlessResumed { navigator.pop() },\n        onSaveLog = saveLog(logContent, context, scope),\n        onReboot = {\n            scope.launch {\n                withContext(Dispatchers.IO) {\n                    reboot()\n                }\n            }\n        },\n        onConfirmJailbreakWarning = { flashingEnabled = true },\n        onDismissJailbreakWarning = dropUnlessResumed { navigator.pop() },\n    )\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> FlashScreenMiuix(state, actions)\n        UiMode.Material -> FlashScreenMaterial(state, actions)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/flash/FlashUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.flash\n\nimport androidx.compose.runtime.Immutable\n\n@Immutable\ndata class FlashUiState(\n    val text: String,\n    val showRebootAction: Boolean,\n    val flashingStatus: FlashingStatus,\n    val showJailbreakWarning: Boolean,\n)\n\n@Immutable\ndata class FlashScreenActions(\n    val onBack: () -> Unit,\n    val onSaveLog: () -> Unit,\n    val onReboot: () -> Unit,\n    val onConfirmJailbreakWarning: () -> Unit,\n    val onDismissJailbreakWarning: () -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/flash/FlashUtils.kt",
    "content": "package me.weishu.kernelsu.ui.screen.flash\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Environment\nimport android.os.Handler\nimport android.os.Looper\nimport android.os.Parcelable\nimport android.widget.Toast\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Adb\nimport androidx.compose.material.icons.rounded.DeleteForever\nimport androidx.compose.material.icons.rounded.RemoveModerator\nimport androidx.compose.material.icons.rounded.RestartAlt\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.res.stringResource\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport kotlinx.parcelize.Parcelize\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.util.FlashResult\nimport me.weishu.kernelsu.ui.util.LkmSelection\nimport me.weishu.kernelsu.ui.util.flashModule\nimport me.weishu.kernelsu.ui.util.installBoot\nimport me.weishu.kernelsu.ui.util.restoreBoot\nimport me.weishu.kernelsu.ui.util.uninstallPermanently\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\nenum class FlashingStatus {\n    FLASHING,\n    SUCCESS,\n    FAILED\n}\n\nenum class UninstallType(val icon: ImageVector, val title: Int, val message: Int) {\n    TEMPORARY(\n        Icons.Rounded.RemoveModerator,\n        R.string.settings_uninstall_temporary,\n        R.string.settings_uninstall_temporary_message\n    ),\n    PERMANENT(\n        Icons.Rounded.DeleteForever,\n        R.string.settings_uninstall_permanent,\n        R.string.settings_uninstall_permanent_message\n    ),\n    RESTORE_STOCK_IMAGE(\n        Icons.Rounded.RestartAlt,\n        R.string.settings_restore_stock_image,\n        R.string.settings_restore_stock_image_message\n    ),\n    NONE(Icons.Rounded.Adb, 0, 0)\n}\n\n@Parcelize\nsealed class FlashIt : Parcelable {\n    @Parcelize\n    data class FlashBoot(\n        val boot: Uri? = null,\n        val lkm: LkmSelection,\n        val ota: Boolean,\n        val partition: String? = null,\n        val allowShell: Boolean = false,\n        val enableAdb: Boolean = false,\n    ) : FlashIt()\n\n    @Parcelize\n    data class FlashModules(val uris: List<Uri>) : FlashIt()\n\n    @Parcelize\n    data object FlashRestore : FlashIt()\n\n    @Parcelize\n    data object FlashUninstall : FlashIt()\n}\n\nfun flashModulesSequentially(\n    uris: List<Uri>,\n    onStdout: (String) -> Unit,\n    onStderr: (String) -> Unit\n): FlashResult {\n    for (uri in uris) {\n        flashModule(uri, onStdout, onStderr).apply {\n            if (code != 0) {\n                return FlashResult(code, err, showReboot)\n            }\n        }\n    }\n    return FlashResult(0, \"\", true)\n}\n\nfun flashIt(\n    flashIt: FlashIt,\n    onStdout: (String) -> Unit,\n    onStderr: (String) -> Unit\n): FlashResult {\n    return when (flashIt) {\n        is FlashIt.FlashBoot -> installBoot(\n            flashIt.boot,\n            flashIt.lkm,\n            flashIt.ota,\n            flashIt.partition,\n            flashIt.allowShell,\n            flashIt.enableAdb,\n            onStdout,\n            onStderr\n        )\n\n        is FlashIt.FlashModules -> {\n            flashModulesSequentially(flashIt.uris, onStdout, onStderr)\n        }\n\n        FlashIt.FlashRestore -> restoreBoot(onStdout, onStderr)\n        FlashIt.FlashUninstall -> uninstallPermanently(onStdout, onStderr)\n    }\n}\n\n@Composable\nfun FlashEffect(\n    flashIt: FlashIt,\n    text: String,\n    logContent: StringBuilder,\n    onTextUpdate: (String) -> Unit,\n    onShowRebootChange: (Boolean) -> Unit,\n    onFlashingStatusChange: (FlashingStatus) -> Unit,\n    enabled: Boolean = true\n) {\n    LaunchedEffect(enabled) {\n        if (!enabled || text.isNotEmpty()) {\n            return@LaunchedEffect\n        }\n        var currentText = text\n        val mainHandler = Handler(Looper.getMainLooper())\n        withContext(Dispatchers.IO) {\n            flashIt(flashIt, onStdout = {\n                val tempText = \"$it\\n\"\n                if (tempText.startsWith(\"\u001b[H\u001b[J\")) { // clear command\n                    currentText = tempText.substring(6)\n                } else {\n                    currentText += tempText\n                }\n                mainHandler.post {\n                    onTextUpdate(currentText)\n                }\n                logContent.append(it).append(\"\\n\")\n            }, onStderr = {\n                logContent.append(it).append(\"\\n\")\n            }).apply {\n                if (code != 0) {\n                    currentText += \"Error code: $code.\\n $err Please save and check the log.\\n\"\n                    mainHandler.post {\n                        onTextUpdate(currentText)\n                    }\n                }\n                if (showReboot) {\n                    currentText += \"\\n\\n\\n\"\n                    mainHandler.post {\n                        onTextUpdate(currentText)\n                        onShowRebootChange(true)\n                    }\n                }\n                mainHandler.post {\n                    onFlashingStatusChange(if (code == 0) FlashingStatus.SUCCESS else FlashingStatus.FAILED)\n                }\n            }\n        }\n    }\n}\n\nfun saveLog(\n    logContent: StringBuilder,\n    context: Context,\n    scope: CoroutineScope\n): () -> Unit {\n    return {\n        scope.launch {\n            val format = SimpleDateFormat(\"yyyy-MM-dd-HH-mm-ss\", Locale.getDefault())\n            val date = format.format(Date())\n            val file = File(\n                Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),\n                \"KernelSU_install_log_${date}.log\"\n            )\n            file.writeText(logContent.toString())\n            Toast.makeText(context, \"Log saved to ${file.absolutePath}\", Toast.LENGTH_SHORT).show()\n        }\n    }\n}\n\nprivate const val JAILBREAK_WARNING_COUNTDOWN = 10\n\n@Composable\nfun JailbreakFlashWarningDialog(\n    onConfirm: () -> Unit,\n    onDismiss: () -> Unit\n) {\n    var countdown by remember { mutableIntStateOf(JAILBREAK_WARNING_COUNTDOWN) }\n\n    LaunchedEffect(Unit) {\n        while (countdown > 0) {\n            delay(1000)\n            countdown--\n        }\n    }\n\n    AlertDialog(\n        onDismissRequest = onDismiss,\n        title = { Text(stringResource(android.R.string.dialog_alert_title)) },\n        text = {\n            Text(\n                stringResource(R.string.jailbreak_flash_warning),\n                style = MaterialTheme.typography.bodyMedium\n            )\n        },\n        confirmButton = {\n            TextButton(\n                onClick = onConfirm,\n                enabled = countdown == 0\n            ) {\n                Text(\n                    if (countdown > 0)\n                        stringResource(R.string.jailbreak_flash_warning_countdown, countdown)\n                    else\n                        stringResource(R.string.install_next)\n                )\n            }\n        },\n        dismissButton = {\n            TextButton(onClick = onDismiss) {\n                Text(stringResource(android.R.string.cancel))\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/home/HomeMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.home\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Block\nimport androidx.compose.material.icons.outlined.CheckCircle\nimport androidx.compose.material.icons.outlined.Warning\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Card\nimport androidx.compose.material3.CardDefaults\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LargeFlexibleTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.material3.surfaceColorAtElevation\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.platform.UriHandler\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.KernelVersion\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.component.rebootlistpopup.RebootListPopup\nimport me.weishu.kernelsu.ui.component.statustag.StatusTag\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun HomePagerMaterial(\n    state: HomeUiState,\n    actions: HomeActions,\n    bottomInnerPadding: Dp,\n) {\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    Scaffold(\n        topBar = { TopBar(scrollBehavior = scrollBehavior) },\n        contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .padding(innerPadding)\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .verticalScroll(rememberScrollState())\n                .padding(horizontal = 16.dp),\n            verticalArrangement = Arrangement.spacedBy(16.dp)\n        ) {\n            StatusCard(\n                state = state,\n                actions = actions,\n            )\n            if (state.showManagerPrBuildWarning) {\n                WarningCard(stringResource(id = R.string.home_pr_build_warning))\n            } else if (state.showKernelPrBuildWarning) {\n                WarningCard(stringResource(id = R.string.home_pr_kernel_warning))\n            }\n            if (state.showVersionMismatchWarning) {\n                WarningCard(\n                    stringResource(id = R.string.home_version_mismatch).format(\n                        state.currentManagerVersionCode,\n                        state.ksuVersion\n                    )\n                )\n            }\n            if (state.showGkiWarning) {\n                WarningCard(stringResource(id = R.string.home_gki_warning))\n            }\n            if (state.showRequireKernelWarning) {\n                WarningCard(\n                    stringResource(id = R.string.require_kernel_version).format(\n                        state.ksuVersion,\n                        me.weishu.kernelsu.Natives.MINIMAL_SUPPORTED_KERNEL\n                    )\n                )\n            }\n            if (state.showRootWarning) {\n                WarningCard(stringResource(id = R.string.grant_root_failed))\n            }\n            if (state.checkUpdateEnabled) {\n                UpdateCard(state = state, actions = actions)\n            }\n            InfoCard(systemInfo = state.systemInfo)\n            DonateCard(onOpenUrl = actions.onOpenUrl)\n            LearnMoreCard(onOpenUrl = actions.onOpenUrl)\n            Spacer(Modifier.height(bottomInnerPadding))\n        }\n    }\n}\n\n@Composable\nprivate fun UpdateCard(\n    state: HomeUiState,\n    actions: HomeActions,\n) {\n    val newVersion = state.latestVersionInfo\n    val title = stringResource(id = R.string.module_changelog)\n    val updateText = stringResource(id = R.string.module_update)\n\n    AnimatedVisibility(\n        visible = state.hasUpdate,\n        enter = fadeIn() + expandVertically(),\n        exit = shrinkVertically() + fadeOut()\n    ) {\n        val updateDialog = rememberConfirmDialog(onConfirm = { actions.onOpenUrl(newVersion.downloadUrl) })\n        WarningCard(\n            message = stringResource(id = R.string.new_version_available).format(newVersion.versionCode),\n            MaterialTheme.colorScheme.outlineVariant\n        ) {\n            if (newVersion.changelog.isEmpty()) {\n                actions.onOpenUrl(newVersion.downloadUrl)\n            } else {\n                updateDialog.showConfirm(\n                    title = title,\n                    content = newVersion.changelog,\n                    markdown = true,\n                    confirm = updateText\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun TopBar(\n    scrollBehavior: TopAppBarScrollBehavior? = null\n) {\n    LargeFlexibleTopAppBar(\n        title = { Text(stringResource(R.string.app_name)) },\n        actions = { RebootListPopup() },\n        colors = TopAppBarDefaults.topAppBarColors(\n            containerColor = MaterialTheme.colorScheme.surface,\n            scrolledContainerColor = MaterialTheme.colorScheme.surface\n        ),\n        windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),\n        scrollBehavior = scrollBehavior\n    )\n}\n\n@Composable\nprivate fun StatusCard(\n    state: HomeUiState,\n    actions: HomeActions,\n) {\n    Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {\n        TonalCard(\n            containerColor = if (state.ksuVersion != null) {\n                MaterialTheme.colorScheme.secondaryContainer\n            } else {\n                MaterialTheme.colorScheme.errorContainer\n            }\n        ) {\n            Row(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .clickable(enabled = !state.isLateLoadMode) { actions.onInstallClick() }\n                    .padding(24.dp),\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                when {\n                    state.ksuVersion != null -> {\n                        val workingMode = when (state.lkmMode) {\n                            null -> \"\"\n                            true -> \"LKM\"\n                            else -> \"GKI\"\n                        }\n\n                        Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working))\n                        Column(Modifier.padding(start = 20.dp)) {\n                            Row(verticalAlignment = Alignment.CenterVertically) {\n                                Text(\n                                    text = stringResource(id = R.string.home_working),\n                                    style = MaterialTheme.typography.titleMedium\n                                )\n                                if (workingMode.isNotEmpty()) {\n                                    Spacer(Modifier.width(8.dp))\n                                    StatusTag(\n                                        label = workingMode,\n                                        contentColor = MaterialTheme.colorScheme.onPrimary,\n                                        backgroundColor = MaterialTheme.colorScheme.primary\n                                    )\n                                }\n                                if (state.isSafeMode) {\n                                    Spacer(Modifier.width(8.dp))\n                                    StatusTag(\n                                        label = stringResource(id = R.string.safe_mode),\n                                        contentColor = MaterialTheme.colorScheme.onErrorContainer,\n                                        backgroundColor = MaterialTheme.colorScheme.errorContainer\n                                    )\n                                }\n                                if (state.isLateLoadMode) {\n                                    Spacer(Modifier.width(8.dp))\n                                    StatusTag(\n                                        label = stringResource(id = R.string.jailbreak_mode),\n                                        contentColor = MaterialTheme.colorScheme.onErrorContainer,\n                                        backgroundColor = MaterialTheme.colorScheme.errorContainer\n                                    )\n                                }\n                            }\n                            Spacer(Modifier.height(4.dp))\n                            Text(\n                                text = stringResource(R.string.home_working_version, state.ksuVersion),\n                                style = MaterialTheme.typography.bodyMedium\n                            )\n                        }\n                    }\n\n                    state.kernelVersion.isGKI() -> {\n                        Icon(Icons.Outlined.Warning, stringResource(R.string.home_not_installed))\n                        Column(\n                            modifier = Modifier\n                                .padding(start = 20.dp)\n                                .weight(1f)\n                        ) {\n                            Text(\n                                text = stringResource(R.string.home_not_installed),\n                                style = MaterialTheme.typography.titleMedium\n                            )\n                            Spacer(Modifier.height(4.dp))\n                            Text(\n                                text = stringResource(R.string.home_click_to_install),\n                                style = MaterialTheme.typography.bodyMedium\n                            )\n                        }\n                        if (state.isSELinuxPermissive) {\n                            Button(\n                                onClick = actions.onJailbreakClick,\n                                colors = ButtonDefaults.buttonColors(\n                                    containerColor = MaterialTheme.colorScheme.error,\n                                    contentColor = MaterialTheme.colorScheme.onError\n                                )\n                            ) {\n                                Text(stringResource(R.string.home_jailbreak))\n                            }\n                        }\n                    }\n\n                    else -> {\n                        Icon(Icons.Outlined.Block, stringResource(R.string.home_unsupported))\n                        Column(Modifier.padding(start = 20.dp)) {\n                            Text(\n                                text = stringResource(R.string.home_unsupported),\n                                style = MaterialTheme.typography.titleMedium\n                            )\n                            Spacer(Modifier.height(4.dp))\n                            Text(\n                                text = stringResource(R.string.home_unsupported_reason),\n                                style = MaterialTheme.typography.bodyMedium\n                            )\n                        }\n                    }\n                }\n            }\n        }\n        if (state.isFullFeatured) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.spacedBy(16.dp)\n            ) {\n                TonalCard(modifier = Modifier.weight(1f)) {\n                    Column(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .clickable { actions.onSuperuserClick() }\n                            .padding(horizontal = 24.dp, vertical = 16.dp)\n                    ) {\n                        Text(\n                            text = stringResource(R.string.superuser),\n                            style = MaterialTheme.typography.bodyLarge,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                        Spacer(Modifier.height(4.dp))\n                        Text(\n                            text = state.superuserCount.toString(),\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.outline\n                        )\n                    }\n                }\n                TonalCard(modifier = Modifier.weight(1f)) {\n                    Column(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .clickable { actions.onModuleClick() }\n                            .padding(horizontal = 24.dp, vertical = 16.dp)\n                    ) {\n                        Text(\n                            text = stringResource(R.string.module),\n                            style = MaterialTheme.typography.bodyLarge,\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                        Spacer(Modifier.height(4.dp))\n                        Text(\n                            text = state.moduleCount.toString(),\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.outline\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun WarningCard(\n    message: String,\n    color: Color = MaterialTheme.colorScheme.error,\n    onClick: (() -> Unit)? = null\n) {\n    TonalCard(containerColor = color) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .then(onClick?.let { Modifier.clickable { it() } } ?: Modifier)\n                .padding(24.dp)\n        ) {\n            Text(text = message, style = MaterialTheme.typography.bodyMedium)\n        }\n    }\n}\n\n@Composable\nfun TonalCard(\n    modifier: Modifier = Modifier,\n    containerColor: Color = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp),\n    shape: Shape = MaterialTheme.shapes.large,\n    content: @Composable () -> Unit\n) {\n    Card(\n        modifier = modifier,\n        colors = CardDefaults.cardColors(containerColor = containerColor),\n        shape = shape\n    ) {\n        content()\n    }\n}\n\n@Composable\nprivate fun LearnMoreCard(onOpenUrl: (String) -> Unit) {\n    val url = stringResource(R.string.home_learn_kernelsu_url)\n    TonalCard {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable { onOpenUrl(url) }\n                .padding(24.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Column {\n                Text(text = stringResource(R.string.home_learn_kernelsu), style = MaterialTheme.typography.titleSmall)\n                Spacer(Modifier.height(4.dp))\n                Text(\n                    text = stringResource(R.string.home_click_to_learn_kernelsu),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.outline\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun DonateCard(onOpenUrl: (String) -> Unit) {\n    TonalCard {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .clickable { onOpenUrl(\"https://patreon.com/weishu\") }\n                .padding(24.dp),\n            verticalAlignment = Alignment.CenterVertically\n        ) {\n            Column {\n                Text(text = stringResource(R.string.home_support_title), style = MaterialTheme.typography.titleSmall)\n                Spacer(Modifier.height(4.dp))\n                Text(\n                    text = stringResource(R.string.home_support_content),\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.outline\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun InfoCard(systemInfo: SystemInfo) {\n    TonalCard {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp)\n        ) {\n            @Composable\n            fun InfoCardItem(label: String, content: String) {\n                Text(text = label, style = MaterialTheme.typography.bodyLarge)\n                Text(\n                    text = content,\n                    style = MaterialTheme.typography.bodyMedium,\n                    color = MaterialTheme.colorScheme.outline\n                )\n            }\n\n            InfoCardItem(stringResource(R.string.home_kernel), systemInfo.kernelVersion)\n            Spacer(Modifier.height(16.dp))\n            InfoCardItem(stringResource(R.string.home_manager_version), systemInfo.managerVersion)\n            Spacer(Modifier.height(16.dp))\n            InfoCardItem(stringResource(R.string.home_fingerprint), systemInfo.fingerprint)\n            Spacer(Modifier.height(16.dp))\n            InfoCardItem(stringResource(R.string.home_selinux_status), systemInfo.selinuxStatus)\n        }\n    }\n}\n\n@Preview(name = \"Activated\")\n@Composable\nprivate fun StatusCardActivatedPreview() {\n    StatusCard(\n        state = previewHomeScreenState(ksuVersion = 12345, lkmMode = true, superuserCount = 5, moduleCount = 10),\n        actions = HomeActions({}, {}, {}, {})\n    )\n}\n\n@Preview(name = \"Not Activated\")\n@Composable\nprivate fun StatusCardNotActivatedPreview() {\n    StatusCard(state = previewHomeScreenState(ksuVersion = null, lkmMode = null), actions = HomeActions({}, {}, {}, {}))\n}\n\n@Preview(name = \"Permissive\")\n@Composable\nprivate fun StatusCardPermissivePreview() {\n    StatusCard(\n        state = previewHomeScreenState(ksuVersion = null, lkmMode = null, isSELinuxPermissive = true),\n        actions = HomeActions({}, {}, {}, {})\n    )\n}\n\n@Preview(name = \"Jailbreak\")\n@Composable\nprivate fun StatusCardJailbreakPreview() {\n    StatusCard(\n        state = previewHomeScreenState(ksuVersion = 12345, lkmMode = true, isLateLoadMode = true, superuserCount = 5, moduleCount = 10),\n        actions = HomeActions({}, {}, {}, {})\n    )\n}\n\nprivate val previewSystemInfo = SystemInfo(\n    kernelVersion = \"6.1.0-android14-0-g1234567\",\n    managerVersion = \"1.0.0 (10000)\",\n    fingerprint = \"google/raven/raven:14/AP1A.240305.019:user/release-keys\",\n    selinuxStatus = \"Enforcing\"\n)\n\nprivate val previewUriHandler = object : UriHandler {\n    override fun openUri(uri: String) {}\n}\n\n@Composable\nprivate fun HomeScreenPreviewContent(\n    ksuVersion: Int?,\n    lkmMode: Boolean?,\n    isSafeMode: Boolean = false,\n    isLateLoadMode: Boolean = false,\n    isSELinuxPermissive: Boolean = false,\n    superuserCount: Int = 0,\n    moduleCount: Int = 0,\n    selinuxStatus: String = \"Enforcing\",\n) {\n    CompositionLocalProvider(LocalUriHandler provides previewUriHandler) {\n        Column(\n            modifier = Modifier.padding(horizontal = 16.dp),\n            verticalArrangement = Arrangement.spacedBy(16.dp)\n        ) {\n            val actions = HomeActions({}, {}, {}, {})\n            StatusCard(\n                state = previewHomeScreenState(\n                    ksuVersion = ksuVersion,\n                    lkmMode = lkmMode,\n                    isSafeMode = isSafeMode,\n                    isLateLoadMode = isLateLoadMode,\n                    isSELinuxPermissive = isSELinuxPermissive,\n                    superuserCount = superuserCount,\n                    moduleCount = moduleCount,\n                    selinuxStatus = selinuxStatus,\n                ),\n                actions = actions\n            )\n            InfoCard(previewSystemInfo.copy(selinuxStatus = selinuxStatus))\n            DonateCard(onOpenUrl = {})\n            LearnMoreCard(onOpenUrl = {})\n        }\n    }\n}\n\n@Preview(name = \"Home Activated\", showBackground = true)\n@Composable\nprivate fun HomeScreenActivatedPreview() {\n    HomeScreenPreviewContent(ksuVersion = 12345, lkmMode = true, superuserCount = 5, moduleCount = 10)\n}\n\n@Preview(name = \"Home Not Activated\", showBackground = true)\n@Composable\nprivate fun HomeScreenNotActivatedPreview() {\n    HomeScreenPreviewContent(ksuVersion = null, lkmMode = null)\n}\n\n@Preview(name = \"Home Permissive\", showBackground = true)\n@Composable\nprivate fun HomeScreenPermissivePreview() {\n    HomeScreenPreviewContent(ksuVersion = null, lkmMode = null, isSELinuxPermissive = true, selinuxStatus = \"Permissive\")\n}\n\n@Preview(name = \"Home Jailbreak\", showBackground = true)\n@Composable\nprivate fun HomeScreenJailbreakPreview() {\n    HomeScreenPreviewContent(ksuVersion = 12345, lkmMode = true, isLateLoadMode = true, superuserCount = 5, moduleCount = 10)\n}\n\nprivate fun previewHomeScreenState(\n    ksuVersion: Int?,\n    lkmMode: Boolean?,\n    isSafeMode: Boolean = false,\n    isLateLoadMode: Boolean = false,\n    isSELinuxPermissive: Boolean = false,\n    superuserCount: Int = 0,\n    moduleCount: Int = 0,\n    selinuxStatus: String = \"Enforcing\",\n) = HomeUiState(\n    kernelVersion = KernelVersion(6, 1, 0),\n    ksuVersion = ksuVersion,\n    lkmMode = lkmMode,\n    isManager = true,\n    isManagerPrBuild = false,\n    isKernelPrBuild = false,\n    requiresNewKernel = false,\n    isRootAvailable = ksuVersion != null,\n    isSafeMode = isSafeMode,\n    isLateLoadMode = isLateLoadMode,\n    isSELinuxPermissive = isSELinuxPermissive,\n    checkUpdateEnabled = false,\n    latestVersionInfo = me.weishu.kernelsu.ui.util.module.LatestVersionInfo(),\n    currentManagerVersionCode = 10000,\n    superuserCount = superuserCount,\n    moduleCount = moduleCount,\n    systemInfo = previewSystemInfo.copy(selinuxStatus = selinuxStatus),\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/home/HomeMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.home\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.IntrinsicSize\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.CheckCircleOutline\nimport androidx.compose.material.icons.rounded.ErrorOutline\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.platform.UriHandler\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.tooling.preview.Preview\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport me.weishu.kernelsu.KernelVersion\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.component.rebootlistpopup.RebootListPopupMiuix\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.theme.isInDarkTheme\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport top.yukonga.miuix.kmp.basic.BasicComponent\nimport top.yukonga.miuix.kmp.basic.ButtonDefaults\nimport top.yukonga.miuix.kmp.basic.Card\nimport top.yukonga.miuix.kmp.basic.CardDefaults\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.ScrollBehavior\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.basic.TopAppBar\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.extended.Link\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.isDynamicColor\nimport top.yukonga.miuix.kmp.utils.PressFeedbackType\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\n\n@Composable\nfun HomePagerMiuix(\n    state: HomeUiState,\n    actions: HomeActions,\n    bottomInnerPadding: Dp,\n) {\n    val scrollBehavior = MiuixScrollBehavior()\n    val enableBlur = LocalEnableBlur.current\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n    Scaffold(\n        topBar = {\n            TopBar(\n                scrollBehavior = scrollBehavior,\n                hazeState = hazeState,\n                hazeStyle = hazeStyle,\n                enableBlur = enableBlur,\n            )\n        },\n        popupHost = { },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxHeight()\n                .scrollEndHaptic()\n                .overScrollVertical()\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .padding(horizontal = 12.dp)\n                .let { if (enableBlur) it.hazeSource(state = hazeState) else it },\n            contentPadding = innerPadding,\n            overscrollEffect = null,\n        ) {\n            item {\n                Column(\n                    modifier = Modifier.padding(vertical = 12.dp),\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    verticalArrangement = Arrangement.spacedBy(12.dp),\n                ) {\n                    if (state.showManagerPrBuildWarning) {\n                        WarningCard(stringResource(id = R.string.home_pr_build_warning))\n                    } else if (state.showKernelPrBuildWarning) {\n                        WarningCard(stringResource(id = R.string.home_pr_kernel_warning))\n                    }\n                    if (state.showVersionMismatchWarning) {\n                        WarningCard(\n                            stringResource(id = R.string.home_version_mismatch).format(\n                                state.currentManagerVersionCode,\n                                state.ksuVersion\n                            )\n                        )\n                    }\n                    if (state.showGkiWarning) {\n                        WarningCard(stringResource(id = R.string.home_gki_warning))\n                    }\n                    if (state.showRequireKernelWarning) {\n                        WarningCard(\n                            stringResource(id = R.string.require_kernel_version)\n                                .format(state.ksuVersion, me.weishu.kernelsu.Natives.MINIMAL_SUPPORTED_KERNEL),\n                        )\n                    }\n                    if (state.showRootWarning) {\n                        WarningCard(stringResource(id = R.string.grant_root_failed))\n                    }\n                    StatusCard(\n                        state = state,\n                        actions = actions,\n                    )\n\n                    if (state.checkUpdateEnabled) {\n                        UpdateCard(state = state, actions = actions)\n                    }\n                    InfoCard(systemInfo = state.systemInfo)\n                    DonateCard(onOpenUrl = actions.onOpenUrl)\n                    LearnMoreCard(onOpenUrl = actions.onOpenUrl)\n                }\n                Spacer(Modifier.height(bottomInnerPadding))\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun UpdateCard(\n    state: HomeUiState,\n    actions: HomeActions,\n) {\n    val newVersion = state.latestVersionInfo\n    val title = stringResource(id = R.string.module_changelog)\n    val updateText = stringResource(id = R.string.module_update)\n\n    AnimatedVisibility(\n        visible = state.hasUpdate,\n        enter = fadeIn() + expandVertically(),\n        exit = shrinkVertically() + fadeOut()\n    ) {\n        val updateDialog = rememberConfirmDialog(onConfirm = { actions.onOpenUrl(newVersion.downloadUrl) })\n        WarningCard(\n            message = stringResource(id = R.string.new_version_available).format(newVersion.versionCode),\n            colorScheme.outline\n        ) {\n            if (newVersion.changelog.isEmpty()) {\n                actions.onOpenUrl(newVersion.downloadUrl)\n            } else {\n                updateDialog.showConfirm(\n                    title = title,\n                    content = newVersion.changelog,\n                    markdown = true,\n                    confirm = updateText\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun TopBar(\n    scrollBehavior: ScrollBehavior,\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n    enableBlur: Boolean,\n) {\n    TopAppBar(\n        modifier = if (enableBlur) {\n            Modifier.defaultHazeEffect(hazeState, hazeStyle)\n        } else {\n            Modifier\n        },\n        color = if (enableBlur) Color.Transparent else colorScheme.surface,\n        title = stringResource(R.string.app_name),\n        actions = {\n            RebootListPopupMiuix(modifier = Modifier.padding(end = 16.dp))\n        },\n        scrollBehavior = scrollBehavior\n    )\n}\n\n@Composable\nprivate fun StatusCard(\n    state: HomeUiState,\n    actions: HomeActions,\n) {\n    Column {\n        when {\n            state.ksuVersion != null -> {\n                val workingState = buildString {\n                    if (state.isSafeMode) {\n                        append(\" [${stringResource(id = R.string.safe_mode)}]\")\n                    }\n                    if (state.isLateLoadMode) {\n                        append(\" [${stringResource(id = R.string.jailbreak_mode)}]\")\n                    }\n                }\n                val workingMode = when (state.lkmMode) {\n                    null -> \"\"\n                    true -> \" <LKM>\"\n                    else -> \" <GKI>\"\n                }\n                val workingText = \"${stringResource(id = R.string.home_working)}$workingMode$workingState\"\n\n                Row(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .height(IntrinsicSize.Min),\n                    horizontalArrangement = Arrangement.spacedBy(12.dp),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Card(\n                        modifier = Modifier\n                            .weight(1f)\n                            .fillMaxHeight(),\n                        colors = CardDefaults.defaultColors(\n                            color = when {\n                                isDynamicColor -> colorScheme.secondaryContainer\n                                isInDarkTheme() -> Color(0xFF1A3825)\n                                else -> Color(0xFFDFFAE4)\n                            }\n                        ),\n                        onClick = {\n                            if (!state.isLateLoadMode) {\n                                actions.onInstallClick()\n                            }\n                        },\n                        showIndication = !state.isLateLoadMode,\n                        pressFeedbackType = PressFeedbackType.Tilt\n                    ) {\n                        Box(modifier = Modifier.fillMaxSize()) {\n                            Box(\n                                modifier = Modifier\n                                    .fillMaxSize()\n                                    .offset(38.dp, 45.dp),\n                                contentAlignment = Alignment.BottomEnd\n                            ) {\n                                Icon(\n                                    modifier = Modifier.size(170.dp),\n                                    imageVector = Icons.Rounded.CheckCircleOutline,\n                                    tint = if (isDynamicColor) {\n                                        colorScheme.primary.copy(alpha = 0.8f)\n                                    } else {\n                                        Color(0xFF36D167)\n                                    },\n                                    contentDescription = null\n                                )\n                            }\n                            Column(\n                                modifier = Modifier\n                                    .fillMaxSize()\n                                    .padding(all = 16.dp)\n                            ) {\n                                Text(\n                                    modifier = Modifier.fillMaxWidth(),\n                                    text = workingText,\n                                    fontSize = 20.sp,\n                                    fontWeight = FontWeight.SemiBold,\n                                )\n                                Spacer(Modifier.height(2.dp))\n                                Text(\n                                    modifier = Modifier.fillMaxWidth(),\n                                    text = stringResource(R.string.home_working_version, state.ksuVersion),\n                                    fontSize = 14.sp,\n                                    fontWeight = FontWeight.Medium,\n                                )\n                            }\n                        }\n                    }\n                    Column(\n                        modifier = Modifier\n                            .weight(1f)\n                            .fillMaxHeight()\n                    ) {\n                        Card(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .weight(1f),\n                            insideMargin = PaddingValues(16.dp),\n                            onClick = { actions.onSuperuserClick() },\n                            showIndication = true,\n                            pressFeedbackType = PressFeedbackType.Tilt\n                        ) {\n                            Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) {\n                                Text(\n                                    modifier = Modifier.fillMaxWidth(),\n                                    text = stringResource(R.string.superuser),\n                                    fontWeight = FontWeight.Medium,\n                                    fontSize = 15.sp,\n                                    color = colorScheme.onSurfaceVariantSummary,\n                                )\n                                Text(\n                                    modifier = Modifier.fillMaxWidth(),\n                                    text = state.superuserCount.toString(),\n                                    fontSize = 26.sp,\n                                    fontWeight = FontWeight.SemiBold,\n                                    color = colorScheme.onSurface,\n                                )\n                            }\n                        }\n                        Spacer(Modifier.height(12.dp))\n                        Card(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .weight(1f),\n                            insideMargin = PaddingValues(16.dp),\n                            onClick = { actions.onModuleClick() },\n                            showIndication = true,\n                            pressFeedbackType = PressFeedbackType.Tilt\n                        ) {\n                            Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) {\n                                Text(\n                                    modifier = Modifier.fillMaxWidth(),\n                                    text = stringResource(R.string.module),\n                                    fontWeight = FontWeight.Medium,\n                                    fontSize = 15.sp,\n                                    color = colorScheme.onSurfaceVariantSummary,\n                                )\n                                Text(\n                                    modifier = Modifier.fillMaxWidth(),\n                                    text = state.moduleCount.toString(),\n                                    fontSize = 26.sp,\n                                    fontWeight = FontWeight.SemiBold,\n                                    color = colorScheme.onSurface,\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n\n            state.kernelVersion.isGKI() -> {\n                Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {\n                    Card(\n                        modifier = Modifier.weight(1f),\n                        onClick = {\n                            if (!state.isLateLoadMode) {\n                                actions.onInstallClick()\n                            }\n                        },\n                        showIndication = !state.isLateLoadMode,\n                        pressFeedbackType = PressFeedbackType.Sink\n                    ) {\n                        BasicComponent(\n                            title = stringResource(R.string.home_not_installed),\n                            summary = stringResource(R.string.home_click_to_install),\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.ErrorOutline,\n                                    stringResource(R.string.home_not_installed),\n                                    modifier = Modifier.padding(end = 16.dp),\n                                    tint = colorScheme.onBackground,\n                                )\n                            },\n                            endActions = {\n                                if (state.isSELinuxPermissive) {\n                                    TextButton(\n                                        text = stringResource(R.string.home_jailbreak),\n                                        insideMargin = PaddingValues(12.dp),\n                                        onClick = actions.onJailbreakClick,\n                                        colors = ButtonDefaults.textButtonColorsPrimary()\n                                    )\n                                }\n                            }\n                        )\n                    }\n                }\n            }\n\n            else -> {\n                Card(\n                    onClick = {\n                        if (!state.isLateLoadMode) {\n                            actions.onInstallClick()\n                        }\n                    },\n                    showIndication = !state.isLateLoadMode,\n                    pressFeedbackType = PressFeedbackType.Sink\n                ) {\n                    BasicComponent(\n                        title = stringResource(R.string.home_unsupported),\n                        summary = stringResource(R.string.home_unsupported_reason),\n                        startAction = {\n                            Icon(\n                                Icons.Rounded.ErrorOutline,\n                                stringResource(R.string.home_unsupported),\n                                modifier = Modifier.padding(end = 16.dp),\n                                tint = colorScheme.onBackground,\n                            )\n                        }\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun WarningCard(\n    message: String,\n    color: Color? = null,\n    onClick: (() -> Unit)? = null,\n) {\n    Card(\n        onClick = { onClick?.invoke() },\n        colors = CardDefaults.defaultColors(\n            color = color ?: when {\n                isDynamicColor -> colorScheme.errorContainer\n                isInDarkTheme() -> Color(0XFF310808)\n                else -> Color(0xFFF8E2E2)\n            }\n        ),\n        showIndication = onClick != null,\n        pressFeedbackType = PressFeedbackType.Tilt\n    ) {\n        Row(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(16.dp)\n        ) {\n            Text(\n                text = message,\n                color = if (isDynamicColor) colorScheme.onErrorContainer else Color(0xFFF72727),\n                fontSize = 14.sp\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun LearnMoreCard(\n    onOpenUrl: (String) -> Unit,\n) {\n    val url = stringResource(R.string.home_learn_kernelsu_url)\n    Card(modifier = Modifier.fillMaxWidth()) {\n        BasicComponent(\n            title = stringResource(R.string.home_learn_kernelsu),\n            summary = stringResource(R.string.home_click_to_learn_kernelsu),\n            endActions = {\n                Icon(\n                    imageVector = MiuixIcons.Link,\n                    tint = colorScheme.onSurface,\n                    contentDescription = null\n                )\n            },\n            onClick = { onOpenUrl(url) }\n        )\n    }\n}\n\n@Composable\nprivate fun DonateCard(onOpenUrl: (String) -> Unit) {\n    Card(modifier = Modifier.fillMaxWidth()) {\n        BasicComponent(\n            title = stringResource(R.string.home_support_title),\n            summary = stringResource(R.string.home_support_content),\n            endActions = {\n                Icon(\n                    imageVector = MiuixIcons.Link,\n                    tint = colorScheme.onSurface,\n                    contentDescription = null\n                )\n            },\n            onClick = { onOpenUrl(\"https://patreon.com/weishu\") },\n            insideMargin = PaddingValues(18.dp)\n        )\n    }\n}\n\n@Composable\nprivate fun InfoCard(systemInfo: SystemInfo) {\n    @Composable\n    fun InfoText(\n        title: String,\n        content: String,\n        bottomPadding: Dp = 24.dp\n    ) {\n        Text(\n            text = title,\n            fontSize = MiuixTheme.textStyles.headline1.fontSize,\n            fontWeight = FontWeight.Medium,\n            color = colorScheme.onSurface\n        )\n        Text(\n            text = content,\n            fontSize = MiuixTheme.textStyles.body2.fontSize,\n            color = colorScheme.onSurfaceVariantSummary,\n            modifier = Modifier.padding(top = 2.dp, bottom = bottomPadding)\n        )\n    }\n\n    Card {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(16.dp)\n        ) {\n            InfoText(title = stringResource(R.string.home_kernel), content = systemInfo.kernelVersion)\n            InfoText(title = stringResource(R.string.home_manager_version), content = systemInfo.managerVersion)\n            InfoText(title = stringResource(R.string.home_fingerprint), content = systemInfo.fingerprint)\n            InfoText(\n                title = stringResource(R.string.home_selinux_status),\n                content = systemInfo.selinuxStatus,\n                bottomPadding = 0.dp\n            )\n        }\n    }\n}\n\n@Preview(name = \"Activated\")\n@Composable\nprivate fun StatusCardActivatedPreview() {\n    StatusCard(\n        state = previewHomeScreenState(ksuVersion = 12345, lkmMode = true, superuserCount = 5, moduleCount = 10),\n        actions = HomeActions({}, {}, {}, {})\n    )\n}\n\n@Preview(name = \"Not Activated\")\n@Composable\nprivate fun StatusCardNotActivatedPreview() {\n    StatusCard(state = previewHomeScreenState(ksuVersion = null, lkmMode = null), actions = HomeActions({}, {}, {}, {}))\n}\n\n@Preview(name = \"Permissive\")\n@Composable\nprivate fun StatusCardPermissivePreview() {\n    StatusCard(\n        state = previewHomeScreenState(ksuVersion = null, lkmMode = null, isSELinuxPermissive = true),\n        actions = HomeActions({}, {}, {}, {})\n    )\n}\n\n@Preview(name = \"Jailbreak\")\n@Composable\nprivate fun StatusCardJailbreakPreview() {\n    StatusCard(\n        state = previewHomeScreenState(ksuVersion = 12345, lkmMode = true, isLateLoadMode = true, superuserCount = 5, moduleCount = 10),\n        actions = HomeActions({}, {}, {}, {})\n    )\n}\n\nprivate val previewSystemInfo = SystemInfo(\n    kernelVersion = \"6.1.0-android14-0-g1234567\",\n    managerVersion = \"1.0.0 (10000)\",\n    fingerprint = \"google/raven/raven:14/AP1A.240305.019:user/release-keys\",\n    selinuxStatus = \"Enforcing\"\n)\n\nprivate val previewUriHandler = object : UriHandler {\n    override fun openUri(uri: String) {}\n}\n\n@Composable\nprivate fun HomeScreenPreviewContent(\n    ksuVersion: Int?,\n    lkmMode: Boolean?,\n    isSafeMode: Boolean = false,\n    isLateLoadMode: Boolean = false,\n    isSELinuxPermissive: Boolean = false,\n    superuserCount: Int = 0,\n    moduleCount: Int = 0,\n    selinuxStatus: String = \"Enforcing\",\n) {\n    CompositionLocalProvider(LocalUriHandler provides previewUriHandler) {\n        Column(\n            modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            verticalArrangement = Arrangement.spacedBy(12.dp),\n        ) {\n            val actions = HomeActions({}, {}, {}, {})\n            StatusCard(\n                state = previewHomeScreenState(\n                    ksuVersion = ksuVersion,\n                    lkmMode = lkmMode,\n                    isSafeMode = isSafeMode,\n                    isLateLoadMode = isLateLoadMode,\n                    isSELinuxPermissive = isSELinuxPermissive,\n                    superuserCount = superuserCount,\n                    moduleCount = moduleCount,\n                    selinuxStatus = selinuxStatus,\n                ),\n                actions = actions\n            )\n            InfoCard(previewSystemInfo.copy(selinuxStatus = selinuxStatus))\n            DonateCard(onOpenUrl = {})\n            LearnMoreCard(onOpenUrl = {})\n        }\n    }\n}\n\n@Preview(name = \"Home Activated\", showBackground = true)\n@Composable\nprivate fun HomeScreenActivatedPreview() {\n    HomeScreenPreviewContent(ksuVersion = 12345, lkmMode = true, superuserCount = 5, moduleCount = 10)\n}\n\n@Preview(name = \"Home Not Activated\", showBackground = true)\n@Composable\nprivate fun HomeScreenNotActivatedPreview() {\n    HomeScreenPreviewContent(ksuVersion = null, lkmMode = null)\n}\n\n@Preview(name = \"Home Permissive\", showBackground = true)\n@Composable\nprivate fun HomeScreenPermissivePreview() {\n    HomeScreenPreviewContent(ksuVersion = null, lkmMode = null, isSELinuxPermissive = true, selinuxStatus = \"Permissive\")\n}\n\n@Preview(name = \"Home Jailbreak\", showBackground = true)\n@Composable\nprivate fun HomeScreenJailbreakPreview() {\n    HomeScreenPreviewContent(ksuVersion = 12345, lkmMode = true, isLateLoadMode = true, superuserCount = 5, moduleCount = 10)\n}\n\nprivate fun previewHomeScreenState(\n    ksuVersion: Int?,\n    lkmMode: Boolean?,\n    isSafeMode: Boolean = false,\n    isLateLoadMode: Boolean = false,\n    isSELinuxPermissive: Boolean = false,\n    superuserCount: Int = 0,\n    moduleCount: Int = 0,\n    selinuxStatus: String = \"Enforcing\",\n) = HomeUiState(\n    kernelVersion = KernelVersion(6, 1, 0),\n    ksuVersion = ksuVersion,\n    lkmMode = lkmMode,\n    isManager = true,\n    isManagerPrBuild = false,\n    isKernelPrBuild = false,\n    requiresNewKernel = false,\n    isRootAvailable = ksuVersion != null,\n    isSafeMode = isSafeMode,\n    isLateLoadMode = isLateLoadMode,\n    isSELinuxPermissive = isSELinuxPermissive,\n    checkUpdateEnabled = false,\n    latestVersionInfo = me.weishu.kernelsu.ui.util.module.LatestVersionInfo(),\n    currentManagerVersionCode = 10000,\n    superuserCount = superuserCount,\n    moduleCount = moduleCount,\n    systemInfo = previewSystemInfo.copy(selinuxStatus = selinuxStatus),\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/home/HomeScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.home\n\nimport android.content.Intent\nimport android.widget.Toast\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.compose.ui.unit.Dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.delay\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.magica.MagicaService\nimport me.weishu.kernelsu.ui.LocalMainPagerState\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.component.dialog.rememberLoadingDialog\nimport me.weishu.kernelsu.ui.navigation3.Navigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.viewmodel.HomeViewModel\n\r\n@Composable\r\nfun HomePager(\r\n    navigator: Navigator,\r\n    bottomInnerPadding: Dp\r\n) {\r\n    val viewModel = viewModel<HomeViewModel>()\r\n    val uiState by viewModel.uiState.collectAsState()\r\n    val mainState = LocalMainPagerState.current\n    val uriHandler = LocalUriHandler.current\n    val context = LocalContext.current\n    val loadingDialog = rememberLoadingDialog()\n    val scope = rememberCoroutineScope()\n    val systemInfo = getSystemInfo()\n\r\n    LaunchedEffect(Unit) {\r\n        viewModel.refresh()\r\n        viewModel.updateSystemInfo(systemInfo)\r\n    }\r\n\r\n    val actions = HomeActions(\r\n        onInstallClick = { navigator.push(Route.Install) },\n        onSuperuserClick = { mainState.animateToPage(1) },\n        onModuleClick = { mainState.animateToPage(2) },\n        onOpenUrl = uriHandler::openUri,\n        onJailbreakClick = {\n            loadingDialog.showLoading()\n            context.startService(Intent(context, MagicaService::class.java))\n            // Manager will be force-stopped and restarted by late-load on success.\n            // If that doesn't happen within timeout, jailbreak likely failed.\n            scope.launch(Dispatchers.IO) {\n                delay(30_000)\n                withContext(Dispatchers.Main) {\n                    loadingDialog.hide()\n                    Toast.makeText(context, R.string.jailbreak_timeout, Toast.LENGTH_LONG).show()\n                }\n            }\n        },\n    )\n\r\n    when (LocalUiMode.current) {\r\n        UiMode.Miuix -> HomePagerMiuix(\r\n            state = uiState,\r\n            actions = actions,\r\n            bottomInnerPadding = bottomInnerPadding,\r\n        )\r\n\r\n        UiMode.Material -> HomePagerMaterial(\r\n            state = uiState,\r\n            actions = actions,\r\n            bottomInnerPadding = bottomInnerPadding,\r\n        )\r\n    }\r\n}\r\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/home/HomeUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.home\n\nimport androidx.compose.runtime.Immutable\nimport me.weishu.kernelsu.KernelVersion\nimport me.weishu.kernelsu.ui.util.module.LatestVersionInfo\n\n@Immutable\ndata class HomeUiState(\n    val kernelVersion: KernelVersion,\n    val ksuVersion: Int?,\n    val lkmMode: Boolean?,\n    val isManager: Boolean,\n    val isManagerPrBuild: Boolean,\n    val isKernelPrBuild: Boolean,\n    val requiresNewKernel: Boolean,\n    val isRootAvailable: Boolean,\n    val isSafeMode: Boolean,\n    val isLateLoadMode: Boolean,\n    val isSELinuxPermissive: Boolean,\n    val checkUpdateEnabled: Boolean,\n    val latestVersionInfo: LatestVersionInfo,\n    val currentManagerVersionCode: Long,\n    val superuserCount: Int,\n    val moduleCount: Int,\n    val systemInfo: SystemInfo,\n) {\n    val isFullFeatured: Boolean\n        get() = isManager && !requiresNewKernel && isRootAvailable\n\n    val showGkiWarning: Boolean\n        get() = ksuVersion != null && lkmMode == false\n\n    val showRequireKernelWarning: Boolean\n        get() = isManager && requiresNewKernel\n\n    val showRootWarning: Boolean\n        get() = ksuVersion != null && !isRootAvailable\n\n    val showManagerPrBuildWarning: Boolean\n        get() = isManager && isManagerPrBuild\n\n    val showKernelPrBuildWarning: Boolean\n        get() = isManager && !isManagerPrBuild && isKernelPrBuild\n\n    val showVersionMismatchWarning: Boolean\n        get() = ksuVersion != null && ksuVersion.toLong() != currentManagerVersionCode\n\n    val hasUpdate: Boolean\n        get() = latestVersionInfo.versionCode > currentManagerVersionCode\n}\n\n@Immutable\ndata class HomeActions(\n    val onInstallClick: () -> Unit,\n    val onSuperuserClick: () -> Unit,\n    val onModuleClick: () -> Unit,\n    val onOpenUrl: (String) -> Unit,\n    val onJailbreakClick: () -> Unit = {},\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/home/HomeUtils.kt",
    "content": "package me.weishu.kernelsu.ui.screen.home\n\nimport android.content.Context\nimport android.os.Build\nimport android.system.Os\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.core.content.pm.PackageInfoCompat\nimport me.weishu.kernelsu.ui.util.getSELinuxStatus\n\ndata class ManagerVersion(\n    val versionName: String,\n    val versionCode: Long\n)\n\ndata class SystemInfo(\n    val kernelVersion: String,\n    val managerVersion: String,\n    val fingerprint: String,\n    val selinuxStatus: String\n)\n\n@Composable\nfun getSystemInfo(): SystemInfo {\n    val context = LocalContext.current\n    val uname = Os.uname()\n    val managerVersion = getManagerVersion(context)\n\n    return SystemInfo(\n        kernelVersion = uname.release,\n        managerVersion = \"${managerVersion.versionName} (${managerVersion.versionCode})\",\n        fingerprint = Build.FINGERPRINT,\n        selinuxStatus = getSELinuxStatus()\n    )\n}\n\nfun getManagerVersion(context: Context): ManagerVersion {\n    val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!!\n    val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo)\n    return ManagerVersion(\n        versionName = packageInfo.versionName!!,\n        versionCode = versionCode\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/install/InstallMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.install\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.animateFloatAsState\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.automirrored.filled.DriveFileMove\nimport androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight\nimport androidx.compose.material.icons.filled.Close\nimport androidx.compose.material.icons.filled.Edit\nimport androidx.compose.material.icons.filled.ExpandMore\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeFlexibleTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.key\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.component.material.SegmentedCheckboxItem\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedDropdownItem\nimport me.weishu.kernelsu.ui.component.material.SegmentedListItem\nimport me.weishu.kernelsu.ui.component.material.SegmentedRadioItem\nimport me.weishu.kernelsu.ui.util.LkmSelection\n\n/**\n * @author weishu\n * @date 2024/3/12.\n */\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\ninternal fun InstallScreenMaterial(\n    uiState: InstallUiState,\n    actions: InstallScreenActions,\n) {\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    LaunchedEffect(Unit) {\n        scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit\n    }\n\n    Scaffold(\n        topBar = {\n            TopBar(\n                onBack = actions.onBack,\n                scrollBehavior = scrollBehavior,\n            )\n        },\n        contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .padding(innerPadding)\n                .fillMaxHeight()\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .verticalScroll(rememberScrollState())\n        ) {\n            SelectInstallMethod(\n                state = uiState,\n                onSelected = actions.onSelectMethod,\n                onSelectBootImage = actions.onSelectBootImage,\n            )\n\n            SegmentedColumn(\n                modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                content = buildList {\n                    if (uiState.displayPartitions.isNotEmpty()) add {\n                        SegmentedDropdownItem(\n                            enabled = uiState.canSelectPartition,\n                            items = uiState.displayPartitions,\n                            selectedIndex = uiState.partitionSelectionIndex,\n                            title = \"${stringResource(R.string.install_select_partition)} (${uiState.slotSuffix})\",\n                            onItemSelected = actions.onSelectPartition,\n                            icon = Icons.Filled.Edit\n                        )\n                    }\n                    add {\n                        SegmentedListItem(\n                            leadingContent = {\n                                Icon(\n                                    Icons.AutoMirrored.Filled.DriveFileMove,\n                                    null\n                                )\n                            },\n                            headlineContent = { Text(stringResource(R.string.install_upload_lkm_file)) },\n                            supportingContent = {\n                                (uiState.lkmSelection as? LkmSelection.LkmUri)?.let {\n                                    Text(\n                                        stringResource(\n                                            R.string.selected_lkm,\n                                            it.uri.lastPathSegment ?: \"(file)\"\n                                        )\n                                    )\n                                }\n                            },\n                            trailingContent = {\n                                if (uiState.lkmSelection is LkmSelection.LkmUri) {\n                                    IconButton(onClick = actions.onClearLkm) {\n                                        Icon(\n                                            Icons.Filled.Close,\n                                            contentDescription = stringResource(android.R.string.cancel)\n                                        )\n                                    }\n                                } else {\n                                    Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null)\n                                }\n                            },\n                            onClick = actions.onUploadLkm\n                        )\n                    }\n                }\n            )\n\n            SegmentedColumn(\n                modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                visibleLen = if (uiState.advancedOptionsShown) 0 else 1,\n                content = buildList {\n                    val rotationState by animateFloatAsState(\n                        targetValue = if (uiState.advancedOptionsShown) 180f else 0f,\n                        label = \"RotationAnimation\"\n                    )\n                    add {\n                        SegmentedListItem(\n                            headlineContent = { Text(stringResource(R.string.advanced_options)) },\n                            trailingContent = {\n                                Icon(\n                                    imageVector = Icons.Filled.ExpandMore,\n                                    contentDescription = stringResource(R.string.expand),\n                                    modifier = Modifier.graphicsLayer { rotationZ = rotationState }\n                                )\n                            },\n                            onClick = actions.onAdvancedOptionsClicked\n                        )\n                    }\n                    add {\n                        AnimatedVisibility(\n                            uiState.advancedOptionsShown,\n                            enter = expandVertically() + fadeIn(),\n                            exit = shrinkVertically() + fadeOut()\n                        ) {\n                            SegmentedCheckboxItem(\n                                title = stringResource(id = R.string.allow_shell),\n                                summary = stringResource(id = R.string.allow_shell_summary),\n                                checked = uiState.allowShell,\n                                onCheckedChange = actions.onSelectAllowShell,\n                            )\n                        }\n                    }\n                    add {\n                        AnimatedVisibility(\n                            uiState.advancedOptionsShown,\n                            enter = expandVertically() + fadeIn(),\n                            exit = shrinkVertically() + fadeOut()\n                        ) {\n                            SegmentedCheckboxItem(\n                                title = stringResource(id = R.string.enable_adb),\n                                summary = stringResource(id = R.string.enable_adb_summary),\n                                checked = uiState.enableAdb,\n                                onCheckedChange = actions.onSelectEnableAdb,\n                            )\n                        }\n                    }\n                }\n            )\n            Button(\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .padding(horizontal = 16.dp, vertical = 4.dp),\n                enabled = uiState.installMethod != null,\n                onClick = actions.onNext\n            ) { Text(stringResource(R.string.install_next)) }\n        }\n    }\n}\n\n@Composable\nprivate fun SelectInstallMethod(\n    state: InstallUiState,\n    onSelected: (InstallMethod) -> Unit,\n    onSelectBootImage: () -> Unit,\n) {\n    val confirmDialog = rememberConfirmDialog(\n        onConfirm = {\n            onSelected(InstallMethod.DirectInstallToInactiveSlot)\n        },\n        onDismiss = null\n    )\n    val dialogTitle = stringResource(android.R.string.dialog_alert_title)\n    val dialogContent = stringResource(R.string.install_inactive_slot_warning)\n\n    val onClick = { option: InstallMethod ->\n        when (option) {\n            is InstallMethod.SelectFile -> onSelectBootImage()\n            is InstallMethod.DirectInstall -> onSelected(option)\n            is InstallMethod.DirectInstallToInactiveSlot -> confirmDialog.showConfirm(dialogTitle, dialogContent)\n        }\n    }\n\n    key(state.installMethodOptions.size) {\n        SegmentedColumn(\n            modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n            content = state.installMethodOptions.map { option ->\n                {\n                    SegmentedRadioItem(\n                        title = stringResource(option.label),\n                        summary = option.summary,\n                        selected = option.javaClass == state.installMethod?.javaClass,\n                        onClick = { onClick(option) }\n                    )\n                }\n            }\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun TopBar(\n    onBack: () -> Unit = {},\n    scrollBehavior: TopAppBarScrollBehavior? = null\n) {\n    LargeFlexibleTopAppBar(\n        title = { Text(stringResource(R.string.install)) },\n        navigationIcon = {\n            IconButton(onClick = onBack) {\n                Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)\n            }\n        },\n        colors = TopAppBarDefaults.topAppBarColors(\n            containerColor = MaterialTheme.colorScheme.surface,\n            scrolledContainerColor = MaterialTheme.colorScheme.surface\n        ),\n        windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),\n        scrollBehavior = scrollBehavior\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/install/InstallMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.install\n\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.LocalIndication\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.selection.toggleable\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.util.LkmSelection\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport top.yukonga.miuix.kmp.basic.BasicComponent\nimport top.yukonga.miuix.kmp.basic.ButtonDefaults\nimport top.yukonga.miuix.kmp.basic.Card\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.IconButton\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.ScrollBehavior\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.basic.TopAppBar\nimport top.yukonga.miuix.kmp.extra.SuperCheckbox\nimport top.yukonga.miuix.kmp.extra.SuperDropdown\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.basic.ArrowRight\nimport top.yukonga.miuix.kmp.icon.extended.Back\nimport top.yukonga.miuix.kmp.icon.extended.Close\nimport top.yukonga.miuix.kmp.icon.extended.ConvertFile\nimport top.yukonga.miuix.kmp.icon.extended.ExpandLess\nimport top.yukonga.miuix.kmp.icon.extended.ExpandMore\nimport top.yukonga.miuix.kmp.icon.extended.MoveFile\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\n\n/**\n * @author weishu\n * @date 2024/3/12.\n */\n@Composable\ninternal fun InstallScreenMiuix(\n    uiState: InstallUiState,\n    actions: InstallScreenActions,\n) {\n    val enableBlur = LocalEnableBlur.current\n    val scrollBehavior = MiuixScrollBehavior()\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n\n    Scaffold(\n        topBar = {\n            TopBar(\n                onBack = actions.onBack,\n                scrollBehavior = scrollBehavior,\n                hazeState = hazeState,\n                hazeStyle = hazeStyle,\n                enableBlur = enableBlur,\n            )\n        },\n        popupHost = { },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxHeight()\n                .scrollEndHaptic()\n                .overScrollVertical()\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .let { if (enableBlur) it.hazeSource(state = hazeState) else it }\n                .padding(top = 12.dp)\n                .padding(horizontal = 16.dp),\n            contentPadding = innerPadding,\n            overscrollEffect = null,\n        ) {\n            item {\n                Card(\n                    modifier = Modifier.fillMaxWidth(),\n                ) {\n                    SelectInstallMethod(\n                        state = uiState,\n                        onSelected = actions.onSelectMethod,\n                        onSelectBootImage = actions.onSelectBootImage,\n                    )\n                }\n                AnimatedVisibility(\n                    visible = uiState.canSelectPartition,\n                    enter = expandVertically(),\n                    exit = shrinkVertically()\n                ) {\n                    Card(\n                        modifier = Modifier\n                            .fillMaxWidth()\n                            .padding(top = 12.dp),\n                    ) {\n                        SuperDropdown(\n                            items = uiState.displayPartitions,\n                            selectedIndex = uiState.partitionSelectionIndex,\n                            title = \"${stringResource(R.string.install_select_partition)} (${uiState.slotSuffix})\",\n                            onSelectedIndexChange = actions.onSelectPartition,\n                            startAction = {\n                                Icon(\n                                    MiuixIcons.ConvertFile,\n                                    tint = colorScheme.onSurface,\n                                    modifier = Modifier.padding(end = 12.dp),\n                                    contentDescription = null\n                                )\n                            }\n                        )\n                    }\n                }\n                Card(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 12.dp),\n                ) {\n                    BasicComponent(\n                        title = stringResource(id = R.string.install_upload_lkm_file),\n                        summary = (uiState.lkmSelection as? LkmSelection.LkmUri)?.let {\n                            stringResource(id = R.string.selected_lkm, it.uri.lastPathSegment ?: \"(file)\")\n                        },\n                        onClick = actions.onUploadLkm,\n                        startAction = {\n                            Icon(\n                                MiuixIcons.MoveFile,\n                                tint = colorScheme.onSurface,\n                                modifier = Modifier.padding(end = 12.dp),\n                                contentDescription = null\n                            )\n                        },\n                        endActions = {\n                            if (uiState.lkmSelection is LkmSelection.LkmUri) {\n                                IconButton(onClick = actions.onClearLkm) {\n                                    Icon(\n                                        MiuixIcons.Close,\n                                        modifier = Modifier.size(16.dp),\n                                        contentDescription = stringResource(android.R.string.cancel),\n                                        tint = colorScheme.onSurfaceVariantActions\n                                    )\n                                }\n                            } else {\n                                val layoutDirection = LocalLayoutDirection.current\n                                Icon(\n                                    modifier = Modifier\n                                        .size(width = 10.dp, height = 16.dp)\n                                        .graphicsLayer {\n                                            scaleX = if (layoutDirection == LayoutDirection.Rtl) -1f else 1f\n                                        }\n                                        .align(Alignment.CenterVertically),\n                                    imageVector = MiuixIcons.Basic.ArrowRight,\n                                    contentDescription = null,\n                                    tint = colorScheme.onSurfaceVariantActions,\n                                )\n                            }\n                        }\n                    )\n                }\n                Card(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 12.dp),\n                ) {\n                    BasicComponent(\n                        title = stringResource(id = R.string.advanced_options),\n                        onClick = actions.onAdvancedOptionsClicked,\n                        endActions = {\n                            Icon(\n                                if (uiState.advancedOptionsShown) MiuixIcons.ExpandLess else MiuixIcons.ExpandMore,\n                                modifier = Modifier.size(16.dp),\n                                tint = colorScheme.onSurfaceVariantActions,\n                                contentDescription = stringResource(R.string.expand),\n                            )\n                        }\n                    )\n                    AnimatedVisibility(\n                        visible = uiState.advancedOptionsShown,\n                        enter = expandVertically() + fadeIn(),\n                        exit = shrinkVertically() + fadeOut()\n                    ) {\n                        Column {\n                            SuperCheckbox(\n                                title = stringResource(id = R.string.allow_shell),\n                                checked = uiState.allowShell,\n                                summary = stringResource(id = R.string.allow_shell_summary),\n                                onCheckedChange = actions.onSelectAllowShell\n                            )\n                            SuperCheckbox(\n                                title = stringResource(id = R.string.enable_adb),\n                                checked = uiState.enableAdb,\n                                summary = stringResource(id = R.string.enable_adb_summary),\n                                onCheckedChange = actions.onSelectEnableAdb\n                            )\n                        }\n                    }\n                }\n                TextButton(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(top = 12.dp),\n                    text = stringResource(id = R.string.install_next),\n                    enabled = uiState.installMethod != null,\n                    colors = ButtonDefaults.textButtonColorsPrimary(),\n                    onClick = actions.onNext\n                )\n                Spacer(\n                    Modifier.height(\n                        WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                                WindowInsets.captionBar.asPaddingValues().calculateBottomPadding()\n                    )\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun SelectInstallMethod(\n    state: InstallUiState,\n    onSelected: (InstallMethod) -> Unit,\n    onSelectBootImage: () -> Unit,\n) {\n    val confirmDialog = rememberConfirmDialog(\n        onConfirm = {\n            onSelected(InstallMethod.DirectInstallToInactiveSlot)\n        }\n    )\n    val dialogTitle = stringResource(id = android.R.string.dialog_alert_title)\n    val dialogContent = stringResource(id = R.string.install_inactive_slot_warning)\n\n    val onClick = { option: InstallMethod ->\n        when (option) {\n            is InstallMethod.SelectFile -> onSelectBootImage()\n            is InstallMethod.DirectInstall -> onSelected(option)\n            is InstallMethod.DirectInstallToInactiveSlot -> confirmDialog.showConfirm(dialogTitle, dialogContent)\n        }\n    }\n\n    Column {\n        state.installMethodOptions.forEach { option ->\n            val interactionSource = remember { MutableInteractionSource() }\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier = Modifier\n                    .fillMaxWidth()\n                    .toggleable(\n                        value = option.javaClass == state.installMethod?.javaClass,\n                        onValueChange = { onClick(option) },\n                        role = Role.RadioButton,\n                        indication = LocalIndication.current,\n                        interactionSource = interactionSource\n                    )\n            ) {\n                SuperCheckbox(\n                    title = stringResource(id = option.label),\n                    summary = option.summary,\n                    checked = option.javaClass == state.installMethod?.javaClass,\n                    onCheckedChange = { onClick(option) },\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun TopBar(\n    onBack: () -> Unit = {},\n    scrollBehavior: ScrollBehavior,\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n    enableBlur: Boolean\n) {\n    TopAppBar(\n        modifier = if (enableBlur) {\n            Modifier.defaultHazeEffect(hazeState, hazeStyle)\n        } else {\n            Modifier\n        },\n        color = if (enableBlur) Color.Transparent else colorScheme.surface,\n        title = stringResource(R.string.install),\n        navigationIcon = {\n            IconButton(\n                modifier = Modifier.padding(start = 16.dp),\n                onClick = onBack\n            ) {\n                val layoutDirection = LocalLayoutDirection.current\n                Icon(\n                    modifier = Modifier.graphicsLayer {\n                        if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\n                    },\n                    imageVector = MiuixIcons.Back,\n                    tint = colorScheme.onSurface,\n                    contentDescription = null,\n                )\n            }\n        },\n        scrollBehavior = scrollBehavior\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/install/InstallScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.install\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.widget.Toast\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.produceState\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.compose.dropUnlessResumed\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.getKernelVersion\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.component.choosekmidialog.ChooseKmiDialog\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.screen.flash.FlashIt\nimport me.weishu.kernelsu.ui.util.LkmSelection\nimport me.weishu.kernelsu.ui.util.getAvailablePartitions\nimport me.weishu.kernelsu.ui.util.getCurrentKmi\nimport me.weishu.kernelsu.ui.util.getDefaultPartition\nimport me.weishu.kernelsu.ui.util.getSlotSuffix\nimport me.weishu.kernelsu.ui.util.isAbDevice\nimport me.weishu.kernelsu.ui.util.rootAvailable\n\n@Composable\nfun InstallScreen() {\n    val navigator = LocalNavigator.current\n    val context = LocalContext.current\n\n    var installMethod by rememberSaveable { mutableStateOf<InstallMethod?>(null) }\n    var lkmSelection by rememberSaveable { mutableStateOf<LkmSelection>(LkmSelection.KmiNone) }\n    var partitionSelectionIndex by rememberSaveable { mutableIntStateOf(0) }\n    var hasCustomSelected by rememberSaveable { mutableStateOf(false) }\n    val showChooseKmiDialog = rememberSaveable { mutableStateOf(false) }\n    var advancedOptionsShown by rememberSaveable { mutableStateOf(false) }\n    var allowShell by rememberSaveable { mutableStateOf(false) }\n    var enableAdb by rememberSaveable { mutableStateOf(false) }\n\n    val currentKmi by produceState(initialValue = \"\") { value = getCurrentKmi() }\n    val partitions by produceState(initialValue = emptyList()) { value = getAvailablePartitions() }\n    val defaultPartition by produceState(initialValue = \"\") { value = getDefaultPartition() }\n    val rootAvailable by produceState(initialValue = false) { value = rootAvailable() }\n    val isAbDevice by produceState(initialValue = false) { value = isAbDevice() }\n    val isGkiDevice by produceState(initialValue = false) { value = getKernelVersion().isGKI() }\n\n    val selectFileTip = stringResource(id = R.string.select_file_tip, defaultPartition)\n    val selectFileTipNoGki = stringResource(id = R.string.select_file_tip_nogki)\n    val installMethodOptions = remember(rootAvailable, isAbDevice, isGkiDevice, selectFileTip, selectFileTipNoGki) {\n        buildList {\n            add(InstallMethod.SelectFile(summary = if (isGkiDevice) selectFileTip else selectFileTipNoGki))\n            if (rootAvailable && isGkiDevice) {\n                add(InstallMethod.DirectInstall)\n                if (isAbDevice) add(InstallMethod.DirectInstallToInactiveSlot)\n            }\n        }\n    }\n\n    val isOta = installMethod is InstallMethod.DirectInstallToInactiveSlot\n    val slotSuffix by produceState(initialValue = \"\", isOta) { value = getSlotSuffix(isOta) }\n    val defaultIndex = remember(partitions, defaultPartition) {\n        partitions.indexOf(defaultPartition).coerceAtLeast(0)\n    }\n\n    LaunchedEffect(partitions, defaultIndex, hasCustomSelected) {\n        if (partitions.isEmpty()) return@LaunchedEffect\n        if (!hasCustomSelected) {\n            partitionSelectionIndex = defaultIndex.coerceIn(0, partitions.lastIndex)\n        } else if (partitionSelectionIndex > partitions.lastIndex) {\n            partitionSelectionIndex = partitions.lastIndex\n        }\n    }\n\n    val displayPartitions = remember(partitions, defaultPartition) {\n        partitions.map { name -> if (defaultPartition == name) \"$name (default)\" else name }\n    }\n\n    val onInstall = {\n        installMethod?.let { method ->\n            navigator.push(\n                Route.Flash(\n                    FlashIt.FlashBoot(\n                        boot = if (method is InstallMethod.SelectFile) method.uri else null,\n                        lkm = lkmSelection,\n                        ota = method is InstallMethod.DirectInstallToInactiveSlot,\n                        partition = partitions.getOrNull(partitionSelectionIndex),\n                        allowShell = allowShell,\n                        enableAdb = enableAdb,\n                    )\n                )\n            )\n        }\n    }\n\n    ChooseKmiDialog(\n        show = showChooseKmiDialog.value,\n        onDismissRequest = { showChooseKmiDialog.value = false },\n        onSelected = { kmi ->\n            kmi?.let {\n                lkmSelection = LkmSelection.KmiString(it)\n                onInstall()\n            }\n        }\n    )\n\n    val selectLkmLauncher = rememberLauncherForActivityResult(\n        contract = ActivityResultContracts.StartActivityForResult()\n    ) {\n        if (it.resultCode == Activity.RESULT_OK) {\n            it.data?.data?.let { uri ->\n                if (isKoFile(context, uri)) {\n                    lkmSelection = LkmSelection.LkmUri(uri)\n                } else {\n                    lkmSelection = LkmSelection.KmiNone\n                    Toast.makeText(context, R.string.install_only_support_ko_file, Toast.LENGTH_SHORT).show()\n                }\n            }\n        }\n    }\n    val selectImageLauncher = rememberLauncherForActivityResult(\n        contract = ActivityResultContracts.StartActivityForResult()\n    ) {\n        if (it.resultCode == Activity.RESULT_OK) {\n            it.data?.data?.let { uri ->\n                installMethod = InstallMethod.SelectFile(uri, summary = if (isGkiDevice) selectFileTip else selectFileTipNoGki)\n            }\n        }\n    }\n\n    val state = InstallUiState(\n        installMethod = installMethod,\n        lkmSelection = lkmSelection,\n        partitionSelectionIndex = partitionSelectionIndex,\n        displayPartitions = displayPartitions,\n        currentKmi = currentKmi,\n        slotSuffix = slotSuffix,\n        installMethodOptions = installMethodOptions,\n        canSelectPartition = installMethod is InstallMethod.DirectInstall || installMethod is InstallMethod.DirectInstallToInactiveSlot,\n        advancedOptionsShown = advancedOptionsShown,\n        allowShell = allowShell,\n        enableAdb = enableAdb,\n    )\n    val actions = InstallScreenActions(\n        onBack = dropUnlessResumed { navigator.pop() },\n        onSelectMethod = { method -> installMethod = method },\n        onSelectBootImage = {\n            selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { type = \"application/octet-stream\" })\n        },\n        onUploadLkm = {\n            selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { type = \"application/octet-stream\" })\n        },\n        onClearLkm = { lkmSelection = LkmSelection.KmiNone },\n        onSelectPartition = { index ->\n            hasCustomSelected = true\n            partitionSelectionIndex = index\n        },\n        onNext = {\n            val isLkmSelected = lkmSelection != LkmSelection.KmiNone\n            val isKmiUnknown = currentKmi.isBlank()\n            val isSelectFileMode = installMethod is InstallMethod.SelectFile\n            if (!isLkmSelected && (isKmiUnknown || isSelectFileMode)) {\n                showChooseKmiDialog.value = true\n            } else {\n                onInstall()\n            }\n        },\n        onAdvancedOptionsClicked = {\n            advancedOptionsShown = !advancedOptionsShown\n        },\n        onSelectAllowShell = {\n            allowShell = it\n        },\n        onSelectEnableAdb = {\n            enableAdb = it\n        },\n    )\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> InstallScreenMiuix(state, actions)\n        UiMode.Material -> InstallScreenMaterial(state, actions)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/install/InstallUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.install\n\nimport androidx.compose.runtime.Immutable\nimport me.weishu.kernelsu.ui.util.LkmSelection\n\n@Immutable\ninternal data class InstallUiState(\n    val installMethod: InstallMethod?,\n    val lkmSelection: LkmSelection,\n    val partitionSelectionIndex: Int,\n    val displayPartitions: List<String>,\n    val currentKmi: String,\n    val slotSuffix: String,\n    val installMethodOptions: List<InstallMethod>,\n    val canSelectPartition: Boolean,\n    val advancedOptionsShown: Boolean,\n    val allowShell: Boolean,\n    val enableAdb: Boolean,\n)\n\n@Immutable\ninternal data class InstallScreenActions(\n    val onBack: () -> Unit,\n    val onSelectMethod: (InstallMethod) -> Unit,\n    val onSelectBootImage: () -> Unit,\n    val onUploadLkm: () -> Unit,\n    val onClearLkm: () -> Unit,\n    val onSelectPartition: (Int) -> Unit,\n    val onNext: () -> Unit,\n    val onAdvancedOptionsClicked: () -> Unit,\n    val onSelectAllowShell: (Boolean) -> Unit,\n    val onSelectEnableAdb: (Boolean) -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/install/InstallUtils.kt",
    "content": "package me.weishu.kernelsu.ui.screen.install\n\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Parcelable\nimport android.provider.OpenableColumns\nimport androidx.annotation.StringRes\nimport kotlinx.parcelize.IgnoredOnParcel\nimport kotlinx.parcelize.Parcelize\nimport me.weishu.kernelsu.R\n\n@Parcelize\ninternal sealed class InstallMethod : Parcelable {\n    data class SelectFile(\n        val uri: Uri? = null,\n        @get:StringRes override val label: Int = R.string.select_file,\n        override val summary: String?\n    ) : InstallMethod()\n\n    data object DirectInstall : InstallMethod() {\n        override val label: Int\n            get() = R.string.direct_install\n    }\n\n    data object DirectInstallToInactiveSlot : InstallMethod() {\n        override val label: Int\n            get() = R.string.install_inactive_slot\n    }\n\n    abstract val label: Int\n\n    @IgnoredOnParcel\n    open val summary: String? = null\n}\n\nfun isKoFile(context: Context, uri: Uri): Boolean {\n    val seg = uri.lastPathSegment ?: \"\"\n    if (seg.endsWith(\".ko\", ignoreCase = true)) return true\n\n    return try {\n        context.contentResolver.query(\n            uri,\n            arrayOf(OpenableColumns.DISPLAY_NAME),\n            null,\n            null,\n            null\n        )?.use { cursor ->\n            val idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)\n            if (idx != -1 && cursor.moveToFirst()) {\n                val name = cursor.getString(idx)\n                name?.endsWith(\".ko\", ignoreCase = true) == true\n            } else {\n                false\n            }\n        } ?: false\n    } catch (_: Throwable) {\n        false\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.module\n\nimport android.annotation.SuppressLint\nimport android.app.Activity.RESULT_OK\nimport android.content.Intent\nimport android.net.Uri\nimport android.widget.Toast\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.LinearOutSlowInEasing\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.foundation.BorderStroke\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.LocalIndication\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.interaction.MutableInteractionSource\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.RowScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.defaultMinSize\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.selection.toggleable\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material.icons.outlined.Cloud\nimport androidx.compose.material.icons.outlined.Code\nimport androidx.compose.material.icons.outlined.Delete\nimport androidx.compose.material.icons.outlined.Download\nimport androidx.compose.material.icons.outlined.PlayArrow\nimport androidx.compose.material.icons.outlined.Refresh\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonColors\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.ExtendedFloatingActionButton\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.OutlinedButton\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.ProvideTextStyle\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarDuration\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.SnackbarResult\nimport androidx.compose.material3.Surface\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.pulltorefresh.PullToRefreshDefaults\nimport androidx.compose.material3.pulltorefresh.pullToRefresh\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.draw.rotate\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.Shape\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.layout.FixedScale\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalResources\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.semantics.Role\nimport androidx.compose.ui.semantics.role\nimport androidx.compose.ui.semantics.semantics\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.data.model.Module\nimport me.weishu.kernelsu.data.model.ModuleUpdateInfo\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.component.dialog.rememberLoadingDialog\nimport me.weishu.kernelsu.ui.component.material.ExpressiveSwitch\nimport me.weishu.kernelsu.ui.component.material.SearchAppBar\nimport me.weishu.kernelsu.ui.component.rebootlistpopup.RebootListPopup\nimport me.weishu.kernelsu.ui.component.statustag.StatusTag\nimport me.weishu.kernelsu.ui.screen.home.TonalCard\nimport me.weishu.kernelsu.ui.util.LocalSnackbarHost\nimport me.weishu.kernelsu.ui.util.reboot\n\n@SuppressLint(\"StringFormatInvalid\")\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun ModulePagerMaterial(\n    uiState: ModuleUiState,\n    confirmDialogState: ModuleConfirmDialogState?,\n    effect: ModuleEffect?,\n    actions: ModuleActions,\n    bottomInnerPadding: Dp,\n) {\n    val snackBarHost = LocalSnackbarHost.current\n\n    val context = LocalContext.current\n    val resource = LocalResources.current\n\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    val pullToRefreshState = rememberPullToRefreshState()\n\n    val scaleFraction = {\n        if (uiState.isRefreshing) 1f\n        else LinearOutSlowInEasing.transform(pullToRefreshState.distanceFraction).coerceIn(0f, 1f)\n    }\n\n    val listState = rememberLazyListState()\n    val searchListState = rememberLazyListState()\n    val threshold = with(LocalDensity.current) { 100.dp.toPx() }\n    val fabExpanded by remember {\n        var lastIndex = 0\n        var lastOffset = 0\n        var scrollDelta = 0f\n        var expanded = true\n        derivedStateOf {\n            val currentIndex = listState.firstVisibleItemIndex\n            val currentOffset = listState.firstVisibleItemScrollOffset\n            val delta = if (currentIndex == lastIndex) {\n                (currentOffset - lastOffset).toFloat()\n            } else if (currentIndex > lastIndex) {\n                100f\n            } else {\n                -100f\n            }\n            scrollDelta = (scrollDelta + delta).coerceIn(-threshold, threshold)\n            lastIndex = currentIndex\n            lastOffset = currentOffset\n            if (currentIndex == 0) {\n                expanded = true\n                scrollDelta = 0f\n            } else if (expanded && scrollDelta >= threshold) {\n                expanded = false\n                scrollDelta = 0f\n            } else if (!expanded && scrollDelta <= -threshold) {\n                expanded = true\n                scrollDelta = 0f\n            }\n            expanded\n        }\n    }\n\n    val shortcutState = rememberModuleShortcutState(context)\n    val showShortcutDialog = remember { mutableStateOf(false) }\n    val confirmDialog = rememberConfirmDialog(\n        onConfirm = {\n            when (val request = confirmDialogState?.request) {\n                is ModuleConfirmRequest.Uninstall -> actions.onUninstallModule(request.module)\n                is ModuleConfirmRequest.Update -> actions.onConfirmUpdate(request)\n                null -> Unit\n            }\n        },\n        onDismiss = actions.onDismissConfirmRequest,\n    )\n\n    fun openShortcutDialogForType(type: ShortcutType) {\n        shortcutState.selectType(type)\n        showShortcutDialog.value = true\n    }\n\n    val pickShortcutIconLauncher = rememberLauncherForActivityResult(\n        contract = ActivityResultContracts.GetContent()\n    ) { uri ->\n        shortcutState.updateIconUri(uri?.toString())\n    }\n\n    fun onModuleAddShortcut(module: Module, type: ShortcutType) {\n        shortcutState.bindModule(module)\n        openShortcutDialogForType(type)\n    }\n\n    LaunchedEffect(confirmDialogState) {\n        confirmDialogState?.let {\n            confirmDialog.showConfirm(\n                title = it.title,\n                content = it.content,\n                markdown = it.markdown,\n                html = it.html,\n                confirm = it.confirm,\n                dismiss = it.dismiss,\n            )\n        }\n    }\n\n    LaunchedEffect(effect) {\n        when (effect) {\n            is ModuleEffect.Toast -> {\n                Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()\n                actions.onConsumeEffect()\n            }\n\n            is ModuleEffect.SnackBar -> {\n                snackBarHost.currentSnackbarData?.dismiss()\n                val result = snackBarHost.showSnackbar(\n                    message = effect.message,\n                    actionLabel = resource.getString(R.string.reboot),\n                    duration = SnackbarDuration.Long\n                )\n                if (result == SnackbarResult.ActionPerformed) {\n                    reboot()\n                }\n                actions.onConsumeEffect()\n            }\n\n            null -> Unit\n        }\n    }\n\n    Scaffold(\n        modifier = Modifier\n            .nestedScroll(scrollBehavior.nestedScrollConnection)\n            .pullToRefresh(\n                state = pullToRefreshState,\n                isRefreshing = uiState.isRefreshing,\n                onRefresh = { actions.onRefresh() },\n            ),\n        topBar = {\n            SearchAppBar(\n                title = { Text(stringResource(R.string.module)) },\n                searchText = uiState.searchStatus.searchText,\n                onSearchTextChange = actions.onSearchTextChange,\n                onClearClick = actions.onClearSearch,\n                navigationIcon = {\n                    IconButton(\n                        onClick = actions.onOpenRepo\n                    ) {\n                        Icon(\n                            imageVector = Icons.Outlined.Cloud,\n                            contentDescription = stringResource(id = R.string.module_repos)\n                        )\n                    }\n                },\n                actions = {\n                    RebootListPopup()\n\n                    var showDropdown by remember { mutableStateOf(false) }\n                    IconButton(\n                        onClick = { showDropdown = true }\n                    ) {\n                        Icon(\n                            imageVector = Icons.Filled.MoreVert,\n                            contentDescription = stringResource(id = R.string.settings)\n                        )\n                        DropdownMenu(\n                            expanded = showDropdown,\n                            onDismissRequest = { showDropdown = false }\n                        ) {\n                            DropdownMenuItem(\n                                text = { Text(stringResource(R.string.module_sort_action_first)) },\n                                trailingIcon = { Checkbox(uiState.sortActionFirst, null) },\n                                onClick = {\n                                    actions.onToggleSortActionFirst()\n                                }\n                            )\n                            DropdownMenuItem(\n                                text = { Text(stringResource(R.string.module_sort_enabled_first)) },\n                                trailingIcon = { Checkbox(uiState.sortEnabledFirst, null) },\n                                onClick = {\n                                    actions.onToggleSortEnabledFirst()\n                                }\n                            )\n                        }\n                    }\n                },\n                scrollBehavior = scrollBehavior,\n                searchContent = { bottomPadding, closeSearch ->\n                    LaunchedEffect(uiState.searchStatus.searchText) {\n                        searchListState.scrollToItem(0)\n                    }\n                    ModuleList(\n                        bottomInnerPadding = bottomPadding,\n                        modifier = Modifier.fillMaxSize(),\n                        listState = searchListState,\n                        displayModules = uiState.searchResults,\n                        updateInfoMap = uiState.updateInfo,\n                        actions = actions,\n                        onClickModule = { module ->\n                            if (module.hasWebUi) {\n                                actions.onOpenWebUi(module)\n                                closeSearch()\n                            }\n                        },\n                        onModuleAddShortcut = { module, type -> onModuleAddShortcut(module, type) },\n                        closeSearch = closeSearch,\n                    )\n                }\n            )\n        },\n        floatingActionButton = {\n            if (uiState.installButtonVisible) {\n                val moduleInstall = stringResource(id = R.string.module_install)\n                val selectZipLauncher = rememberLauncherForActivityResult(\n                    contract = ActivityResultContracts.StartActivityForResult()\n                ) { activityResult ->\n                    if (activityResult.resultCode != RESULT_OK) {\n                        return@rememberLauncherForActivityResult\n                    }\n                    val data = activityResult.data ?: return@rememberLauncherForActivityResult\n                    val clipData = data.clipData\n\n                    val uris = mutableListOf<Uri>()\n                    if (clipData != null) {\n                        for (i in 0 until clipData.itemCount) {\n                            clipData.getItemAt(i)?.uri?.let { uris.add(it) }\n                        }\n                    } else {\n                        data.data?.let { uris.add(it) }\n                    }\n\n                    actions.onOpenFlash(uris)\n                }\n\n                ExtendedFloatingActionButton(\n                    modifier = Modifier.padding(bottom = bottomInnerPadding),\n                    expanded = fabExpanded,\n                    onClick = {\n                        // Select the zip files to install\n                        val intent = Intent(Intent.ACTION_GET_CONTENT).apply {\n                            type = \"application/zip\"\n                            putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)\n                        }\n                        selectZipLauncher.launch(intent)\n                    },\n                    icon = { Icon(Icons.Filled.Add, moduleInstall) },\n                    text = { Text(text = moduleInstall) },\n                )\n            }\n        },\n        contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),\n        snackbarHost = { SnackbarHost(hostState = snackBarHost) }\n    ) { innerPadding ->\n        if (uiState.magiskInstalled) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(24.dp),\n                contentAlignment = Alignment.Center\n            ) {\n                Text(\n                    stringResource(R.string.module_magisk_conflict),\n                    textAlign = TextAlign.Center,\n                )\n            }\n            return@Scaffold\n        }\n        Box(modifier = Modifier.padding(innerPadding)) {\n            if (uiState.moduleList.isEmpty()) {\n                Box(\n                    modifier = Modifier.fillMaxSize(),\n                    contentAlignment = Alignment.Center,\n                ) {\n                    Text(\n                        stringResource(R.string.module_empty),\n                        textAlign = TextAlign.Center,\n                    )\n                }\n            } else {\n                ModuleList(\n                    bottomInnerPadding = bottomInnerPadding,\n                    modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),\n                    listState = listState,\n                    displayModules = uiState.moduleList,\n                    updateInfoMap = uiState.updateInfo,\n                    actions = actions,\n                    onClickModule = { module ->\n                        if (module.hasWebUi) {\n                            actions.onOpenWebUi(module)\n                        }\n                    },\n                    onModuleAddShortcut = { module, type -> onModuleAddShortcut(module, type) },\n                )\n            }\n            Box(\n                modifier = Modifier\n                    .align(Alignment.TopCenter)\n                    .graphicsLayer {\n                        scaleX = scaleFraction()\n                        scaleY = scaleFraction()\n                    }\n            ) {\n                PullToRefreshDefaults.LoadingIndicator(\n                    state = pullToRefreshState,\n                    isRefreshing = uiState.isRefreshing,\n                )\n            }\n        }\n    }\n\n    ModuleShortcutSheet(\n        show = showShortcutDialog.value,\n        shortcutState = shortcutState,\n        onDismiss = { showShortcutDialog.value = false },\n        onPickShortcutIcon = { pickShortcutIconLauncher.launch(\"image/*\") },\n        onDeleteShortcut = {\n            shortcutState.deleteShortcut(context)\n            showShortcutDialog.value = false\n        },\n        onConfirmShortcut = {\n            shortcutState.createShortcut(context)\n            showShortcutDialog.value = false\n        },\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ModuleList(\n    bottomInnerPadding: Dp,\n    modifier: Modifier = Modifier,\n    listState: LazyListState = rememberLazyListState(),\n    displayModules: List<Module>,\n    updateInfoMap: Map<String, ModuleUpdateInfo>,\n    actions: ModuleActions,\n    onClickModule: (Module) -> Unit,\n    onModuleAddShortcut: (Module, ShortcutType) -> Unit,\n    closeSearch: () -> Unit? = {},\n) {\n    val loadingDialog = rememberLoadingDialog()\n    LazyColumn(\n        state = listState,\n        modifier = modifier,\n        verticalArrangement = Arrangement.spacedBy(16.dp),\n        contentPadding = PaddingValues(\n            start = 16.dp,\n            top = 8.dp,\n            end = 16.dp,\n            bottom = 16.dp + bottomInnerPadding + 56.dp + 16.dp\n        ),\n    ) {\n        items(displayModules, key = { it.id }) { module ->\n            val scope = rememberCoroutineScope()\n            val moduleUpdateInfo = updateInfoMap[module.id] ?: ModuleUpdateInfo.Empty\n\n            ModuleItem(\n                module = module,\n                updateUrl = moduleUpdateInfo.downloadUrl,\n                onUninstallClicked = {\n                    if (module.remove) {\n                        actions.onUndoUninstallModule(module)\n                    } else {\n                        actions.onRequestUninstallConfirmation(module)\n                    }\n                },\n                onCheckChanged = {\n                    actions.onToggleModule(module)\n                },\n                onUpdate = {\n                    scope.launch {\n                        loadingDialog.withLoading {\n                            actions.onRequestUpdateConfirmation(module, moduleUpdateInfo)\n                        }\n                    }\n                },\n                onAddShortcut = { type -> onModuleAddShortcut(module, type) },\n                onClick = { onClickModule(module) },\n                onExecuteAction = { actions.onExecuteModuleAction(module) },\n                closeSearch = { closeSearch() }\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun ModuleShortcutSheet(\n    show: Boolean,\n    shortcutState: ModuleShortcutState,\n    onDismiss: () -> Unit,\n    onPickShortcutIcon: () -> Unit,\n    onDeleteShortcut: () -> Unit,\n    onConfirmShortcut: () -> Unit,\n) {\n    if (!show) return\n\n    ModalBottomSheet(\n        onDismissRequest = onDismiss,\n        sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)\n    ) {\n        Column(\n            verticalArrangement = Arrangement.spacedBy(12.dp),\n            horizontalAlignment = Alignment.CenterHorizontally,\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(24.dp)\n        ) {\n            Text(\n                text = stringResource(R.string.module_shortcut_title),\n                style = MaterialTheme.typography.titleLarge\n            )\n            Box(\n                contentAlignment = Alignment.Center,\n                modifier = Modifier\n                    .padding(vertical = 16.dp)\n                    .size(100.dp)\n                    .clip(RoundedCornerShape(25.dp))\n            ) {\n                val preview = shortcutState.previewIcon\n                if (preview != null) {\n                    Image(\n                        bitmap = preview,\n                        modifier = Modifier.size(100.dp),\n                        contentDescription = null,\n                    )\n                } else {\n                    Box(\n                        modifier = Modifier\n                            .size(100.dp)\n                            .background(Color.White)\n                    )\n                    Image(\n                        painter = painterResource(id = R.drawable.ic_launcher_foreground),\n                        contentDescription = null,\n                        contentScale = FixedScale(1.5f)\n                    )\n                }\n            }\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.Center,\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                TextButton(onClick = onPickShortcutIcon) {\n                    Text(stringResource(id = R.string.module_shortcut_icon_pick))\n                }\n                AnimatedVisibility(\n                    visible = shortcutState.iconUri != shortcutState.defaultShortcutIconUri,\n                    enter = expandHorizontally() + slideInHorizontally(initialOffsetX = { it }),\n                    exit = shrinkHorizontally() + slideOutHorizontally(targetOffsetX = { it }),\n                ) {\n                    IconButton(\n                        onClick = shortcutState::resetIconToDefault,\n                        modifier = Modifier.padding(start = 12.dp)\n                    ) {\n                        Icon(\n                            imageVector = Icons.Outlined.Refresh,\n                            contentDescription = null,\n                            modifier = Modifier.size(24.dp),\n                        )\n                    }\n                }\n            }\n            OutlinedTextField(\n                value = shortcutState.name,\n                onValueChange = shortcutState::updateName,\n                label = { Text(stringResource(id = R.string.module_shortcut_name_label)) },\n                modifier = Modifier.fillMaxWidth()\n            )\n            if (shortcutState.hasExistingShortcut) {\n                TextButton(\n                    onClick = onDeleteShortcut,\n                    modifier = Modifier.fillMaxWidth(),\n                    colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error)\n                ) {\n                    Text(stringResource(id = R.string.module_shortcut_delete))\n                }\n            }\n            Row(\n                horizontalArrangement = Arrangement.spacedBy(12.dp),\n                modifier = Modifier.fillMaxWidth()\n            ) {\n                OutlinedButton(\n                    onClick = onDismiss,\n                    modifier = Modifier.weight(1f),\n                ) {\n                    Text(stringResource(id = android.R.string.cancel))\n                }\n                Button(\n                    onClick = onConfirmShortcut,\n                    modifier = Modifier.weight(1f),\n                ) {\n                    Text(\n                        if (shortcutState.hasExistingShortcut) {\n                            stringResource(id = R.string.module_update)\n                        } else {\n                            stringResource(id = android.R.string.ok)\n                        }\n                    )\n                }\n            }\n            Spacer(modifier = Modifier.height(32.dp))\n        }\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ModuleItem(\n    module: Module,\n    updateUrl: String,\n    onUninstallClicked: () -> Unit,\n    onCheckChanged: (Boolean) -> Unit,\n    onUpdate: () -> Unit,\n    onAddShortcut: (ShortcutType) -> Unit,\n    onClick: () -> Unit,\n    onExecuteAction: () -> Unit,\n    closeSearch: () -> Unit\n) {\n    TonalCard(\n        modifier = Modifier.fillMaxWidth()\n    ) {\n        val textDecoration = if (!module.remove) null else TextDecoration.LineThrough\n        val interactionSource = remember { MutableInteractionSource() }\n        val indication = LocalIndication.current\n        var expanded by rememberSaveable(module.id) { mutableStateOf(false) }\n        var isOverflowing by remember { mutableStateOf(false) }\n\n        Column(\n            modifier = Modifier\n                .run {\n                    if (module.hasWebUi) {\n                        toggleable(\n                            value = module.enabled,\n                            enabled = !module.remove && module.enabled,\n                            interactionSource = interactionSource,\n                            role = Role.Button,\n                            indication = indication,\n                            onValueChange = { onClick() }\n                        )\n                    } else {\n                        this\n                    }\n                }\n                .padding(22.dp, 18.dp, 22.dp, 12.dp)\n        ) {\n            Row(\n                modifier = Modifier.fillMaxWidth(),\n                horizontalArrangement = Arrangement.SpaceBetween,\n            ) {\n                val moduleVersion = stringResource(id = R.string.module_version)\n                val moduleAuthor = stringResource(id = R.string.module_author)\n\n                Column(\n                    modifier = Modifier.fillMaxWidth(0.8f)\n                ) {\n                    Text(\n                        text = module.name,\n                        fontWeight = FontWeight.SemiBold,\n                        style = MaterialTheme.typography.titleMedium,\n                        lineHeight = MaterialTheme.typography.bodySmall.lineHeight,\n                        textDecoration = textDecoration,\n                    )\n\n                    Text(\n                        text = \"$moduleVersion: ${module.version}\",\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        style = MaterialTheme.typography.bodySmall,\n                        textDecoration = textDecoration\n                    )\n\n                    Text(\n                        text = \"$moduleAuthor: ${module.author}\",\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        style = MaterialTheme.typography.bodySmall,\n                        textDecoration = textDecoration\n                    )\n                }\n\n                Spacer(modifier = Modifier.weight(1f))\n\n                Row(\n                    modifier = Modifier.fillMaxWidth(),\n                    horizontalArrangement = Arrangement.End,\n                ) {\n                    ExpressiveSwitch(\n                        enabled = !module.update,\n                        checked = module.enabled,\n                        onCheckedChange = onCheckChanged,\n                        interactionSource = if (!module.hasWebUi) interactionSource else remember { MutableInteractionSource() }\n                    )\n                }\n            }\n\n            Spacer(modifier = Modifier.height(8.dp))\n\n            Text(\n                modifier = Modifier\n                    .animateContentSize(\n                        animationSpec = tween(\n                            durationMillis = 250,\n                            easing = FastOutSlowInEasing\n                        )\n                    )\n                    .then(\n                        if (isOverflowing || expanded) {\n                            Modifier.clickable(\n                                interactionSource = remember { MutableInteractionSource() },\n                                indication = null\n                            ) { expanded = !expanded }\n                        } else {\n                            Modifier\n                        }\n                    ),\n                text = module.description,\n                color = MaterialTheme.colorScheme.outline,\n                style = MaterialTheme.typography.bodyMedium,\n                overflow = if (expanded) TextOverflow.Clip else TextOverflow.Ellipsis,\n                maxLines = if (expanded) Int.MAX_VALUE else 4,\n                textDecoration = textDecoration,\n                onTextLayout = { textLayoutResult ->\n                    isOverflowing = if (expanded) {\n                        textLayoutResult.lineCount > 4\n                    } else {\n                        textLayoutResult.hasVisualOverflow\n                    }\n                }\n            )\n\n            Row(modifier = Modifier.padding(vertical = 4.dp)) {\n                if (module.metamodule) {\n                    StatusTag(\n                        \"META\",\n                        modifier = Modifier.padding(bottom = 4.dp),\n                        contentColor = MaterialTheme.colorScheme.onPrimary,\n                        backgroundColor = MaterialTheme.colorScheme.primary\n                    )\n                }\n            }\n\n            HorizontalDivider(thickness = Dp.Hairline)\n\n            Spacer(modifier = Modifier.height(4.dp))\n\n            Row(\n                horizontalArrangement = Arrangement.SpaceBetween,\n                verticalAlignment = Alignment.CenterVertically\n            ) {\n                val hasUpdate by remember(updateUrl) { derivedStateOf { updateUrl.isNotEmpty() } }\n                val actionButtonsEnabled = !module.remove && module.enabled\n\n                AnimatedVisibility(\n                    visible = actionButtonsEnabled,\n                    enter = fadeIn(),\n                    exit = fadeOut()\n                ) {\n                    Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {\n                        if (module.hasActionScript) {\n                            CombinedClickableButton(\n                                onClick = {\n                                    onExecuteAction()\n                                    closeSearch()\n                                },\n                                onLongClick = { onAddShortcut(ShortcutType.Action) },\n                                modifier = Modifier.defaultMinSize(52.dp, 32.dp),\n                                shape = ButtonDefaults.filledTonalShape,\n                                colors = ButtonDefaults.buttonColors(\n                                    containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                                    contentColor = MaterialTheme.colorScheme.onSecondaryContainer\n                                ),\n                                contentPadding = ButtonDefaults.TextButtonContentPadding\n                            ) {\n                                Icon(\n                                    modifier = Modifier.size(20.dp),\n                                    imageVector = Icons.Outlined.PlayArrow,\n                                    contentDescription = null\n                                )\n                                if (!module.hasWebUi && !hasUpdate) {\n                                    Text(\n                                        modifier = Modifier.padding(start = 7.dp),\n                                        text = stringResource(R.string.action),\n                                        fontFamily = MaterialTheme.typography.labelMedium.fontFamily,\n                                        fontSize = MaterialTheme.typography.labelMedium.fontSize\n                                    )\n                                }\n                            }\n                        }\n\n                        if (module.hasWebUi) {\n                            CombinedClickableButton(\n                                onClick = {\n                                    onClick()\n                                    closeSearch()\n                                },\n                                onLongClick = { onAddShortcut(ShortcutType.WebUI) },\n                                modifier = Modifier.defaultMinSize(52.dp, 32.dp),\n                                shape = ButtonDefaults.filledTonalShape,\n                                colors = ButtonDefaults.buttonColors(\n                                    containerColor = MaterialTheme.colorScheme.secondaryContainer,\n                                    contentColor = MaterialTheme.colorScheme.onSecondaryContainer\n                                ),\n                                contentPadding = ButtonDefaults.TextButtonContentPadding\n                            ) {\n                                Icon(\n                                    modifier = Modifier.size(20.dp),\n                                    imageVector = Icons.Outlined.Code,\n                                    contentDescription = null\n                                )\n                                if (!module.hasActionScript && !hasUpdate) {\n                                    Text(\n                                        modifier = Modifier.padding(start = 7.dp),\n                                        fontFamily = MaterialTheme.typography.labelMedium.fontFamily,\n                                        fontSize = MaterialTheme.typography.labelMedium.fontSize,\n                                        text = stringResource(R.string.open)\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n\n                Spacer(modifier = Modifier.weight(1f, true))\n\n                AnimatedVisibility(\n                    visible = hasUpdate,\n                    enter = fadeIn(),\n                    exit = fadeOut()\n                ) {\n                    Row {\n                        Button(\n                            modifier = Modifier.defaultMinSize(52.dp, 32.dp),\n                            enabled = !module.remove,\n                            onClick = onUpdate,\n                            shape = ButtonDefaults.textShape,\n                            contentPadding = ButtonDefaults.TextButtonContentPadding\n                        ) {\n                            Icon(\n                                modifier = Modifier.size(20.dp),\n                                imageVector = Icons.Outlined.Download,\n                                contentDescription = null\n                            )\n                            if (!module.hasActionScript || !module.hasWebUi) {\n                                Text(\n                                    modifier = Modifier.padding(start = 7.dp),\n                                    fontFamily = MaterialTheme.typography.labelMedium.fontFamily,\n                                    fontSize = MaterialTheme.typography.labelMedium.fontSize,\n                                    text = stringResource(R.string.module_update)\n                                )\n                            }\n                        }\n\n                        Spacer(Modifier.width(12.dp))\n                    }\n                }\n\n                FilledTonalButton(\n                    modifier = Modifier.defaultMinSize(52.dp, 32.dp),\n                    onClick = onUninstallClicked,\n                    contentPadding = ButtonDefaults.TextButtonContentPadding\n                ) {\n                    if (!module.remove) {\n                        Icon(\n                            modifier = Modifier.size(20.dp),\n                            imageVector = Icons.Outlined.Delete,\n                            contentDescription = null,\n                        )\n                    } else {\n                        Icon(\n                            modifier = Modifier\n                                .size(20.dp)\n                                .rotate(180f),\n                            imageVector = Icons.Outlined.Refresh,\n                            contentDescription = null,\n                        )\n                    }\n                    if (!module.hasActionScript && !module.hasWebUi || !hasUpdate) {\n                        Text(\n                            modifier = Modifier.padding(start = 7.dp),\n                            fontFamily = MaterialTheme.typography.labelMedium.fontFamily,\n                            fontSize = MaterialTheme.typography.labelMedium.fontSize,\n                            text = stringResource(if (module.remove) R.string.undo else R.string.uninstall)\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun CombinedClickableButton(\n    onClick: () -> Unit,\n    onLongClick: () -> Unit,\n    modifier: Modifier = Modifier,\n    enabled: Boolean = true,\n    shape: Shape = ButtonDefaults.shape,\n    colors: ButtonColors = ButtonDefaults.buttonColors(),\n    border: BorderStroke? = null,\n    contentPadding: PaddingValues = ButtonDefaults.ContentPadding,\n    interactionSource: MutableInteractionSource? = null,\n    content: @Composable RowScope.() -> Unit,\n) {\n    val interactionSource = interactionSource ?: remember { MutableInteractionSource() }\n\n    Surface(\n        modifier = modifier\n            .semantics { role = Role.Button }\n            .clip(shape)\n            .combinedClickable(\n                interactionSource = interactionSource,\n                indication = LocalIndication.current,\n                enabled = enabled,\n                onClick = onClick,\n                onLongClick = onLongClick\n            ),\n        shape = shape,\n        color = if (enabled) colors.containerColor else colors.disabledContainerColor,\n        contentColor = if (enabled) colors.contentColor else colors.disabledContentColor,\n        border = border,\n    ) {\n        ProvideTextStyle(MaterialTheme.typography.labelLarge) {\n            Row(\n                Modifier\n                    .defaultMinSize(\n                        minWidth = ButtonDefaults.MinWidth,\n                        minHeight = ButtonDefaults.MinHeight,\n                    )\n                    .padding(contentPadding),\n                horizontalArrangement = Arrangement.Center,\n                verticalAlignment = Alignment.CenterVertically,\n                content = content,\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.module\n\nimport android.annotation.SuppressLint\nimport android.app.Activity.RESULT_OK\nimport android.content.Intent\nimport android.net.Uri\nimport android.widget.Toast\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.animateContentSize\nimport androidx.compose.animation.core.FastOutSlowInEasing\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.expandHorizontally\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkHorizontally\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.animation.slideInHorizontally\nimport androidx.compose.animation.slideOutHorizontally\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.combinedClickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.heightIn\nimport androidx.compose.foundation.layout.ime\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.layout.widthIn\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.Add\nimport androidx.compose.material.icons.rounded.Code\nimport androidx.compose.material.icons.rounded.PlayArrow\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.rememberUpdatedState\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.NestedScrollSource\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.layout.FixedScale\nimport androidx.compose.ui.layout.SubcomposeLayout\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.TextLayoutResult\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextDecoration\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Constraints\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport com.kyant.capsule.ContinuousRoundedRectangle\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.data.model.Module\nimport me.weishu.kernelsu.data.model.ModuleUpdateInfo\nimport me.weishu.kernelsu.ui.component.ListPopupDefaults\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.component.dialog.rememberLoadingDialog\nimport me.weishu.kernelsu.ui.component.miuix.SearchBox\nimport me.weishu.kernelsu.ui.component.miuix.SearchPager\nimport me.weishu.kernelsu.ui.component.rebootlistpopup.RebootListPopupMiuix\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.theme.isInDarkTheme\nimport me.weishu.kernelsu.ui.util.getFileName\nimport top.yukonga.miuix.kmp.basic.ButtonDefaults\nimport top.yukonga.miuix.kmp.basic.Card\nimport top.yukonga.miuix.kmp.basic.DropdownImpl\nimport top.yukonga.miuix.kmp.basic.FloatingActionButton\nimport top.yukonga.miuix.kmp.basic.HorizontalDivider\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.IconButton\nimport top.yukonga.miuix.kmp.basic.ListPopupColumn\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\nimport top.yukonga.miuix.kmp.basic.PopupPositionProvider\nimport top.yukonga.miuix.kmp.basic.PullToRefresh\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.Switch\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.basic.TextField\nimport top.yukonga.miuix.kmp.basic.TopAppBar\nimport top.yukonga.miuix.kmp.basic.rememberPullToRefreshState\nimport top.yukonga.miuix.kmp.extra.SuperDialog\nimport top.yukonga.miuix.kmp.extra.SuperListPopup\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.extended.Delete\nimport top.yukonga.miuix.kmp.icon.extended.Download\nimport top.yukonga.miuix.kmp.icon.extended.MoreCircle\nimport top.yukonga.miuix.kmp.icon.extended.Undo\nimport top.yukonga.miuix.kmp.icon.extended.UploadCloud\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\n\n@SuppressLint(\"StringFormatInvalid\", \"LocalContextGetResourceValueCall\")\n@Composable\nfun ModulePagerMiuix(\n    uiState: ModuleUiState,\n    confirmDialogState: ModuleConfirmDialogState?,\n    effect: ModuleEffect?,\n    actions: ModuleActions,\n    bottomInnerPadding: Dp,\n) {\n    val modules = uiState.moduleList\n    val searchStatus = uiState.searchStatus\n\n    val context = LocalContext.current\n    val enableBlur = LocalEnableBlur.current\n\n    val installPromptWithName = stringResource(R.string.module_install_prompt_with_name, \"%s\")\n    val confirmDialog = rememberConfirmDialog(\n        onConfirm = {\n            when (val request = confirmDialogState?.request) {\n                is ModuleConfirmRequest.Uninstall -> {\n                    actions.onUninstallModule(request.module)\n                }\n\n                is ModuleConfirmRequest.Update -> {\n                    actions.onConfirmUpdate(request)\n                }\n\n                null -> Unit\n            }\n        },\n        onDismiss = actions.onDismissConfirmRequest,\n    )\n\n    val scrollBehavior = MiuixScrollBehavior()\n    var fabVisible by remember { mutableStateOf(true) }\n    var scrollDistance by remember { mutableFloatStateOf(0f) }\n    val dynamicTopPadding by remember {\n        derivedStateOf { 12.dp * (1f - scrollBehavior.state.collapsedFraction) }\n    }\n\n    val shortcutState = rememberModuleShortcutState(context)\n    val showShortcutDialog = remember { mutableStateOf(false) }\n\n    val pickShortcutIconLauncher = rememberLauncherForActivityResult(\n        contract = ActivityResultContracts.GetContent()\n    ) { uri ->\n        shortcutState.updateIconUri(uri?.toString())\n    }\n\n    LaunchedEffect(confirmDialogState) {\n        confirmDialogState?.let {\n            confirmDialog.showConfirm(\n                title = it.title,\n                content = it.content,\n                markdown = it.markdown,\n                html = it.html,\n                confirm = it.confirm,\n                dismiss = it.dismiss,\n            )\n        }\n    }\n\n    LaunchedEffect(effect) {\n        when (effect) {\n            is ModuleEffect.Toast -> {\n                Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()\n                actions.onConsumeEffect()\n            }\n\n            is ModuleEffect.SnackBar -> {\n                Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()\n                actions.onConsumeEffect()\n            }\n\n            null -> Unit\n        }\n    }\n\n    fun onModuleAddShortcut(module: Module, type: ShortcutType) {\n        shortcutState.bindModule(module)\n        shortcutState.selectType(type)\n        showShortcutDialog.value = true\n    }\n\n    val listState = rememberLazyListState()\n    val nestedScrollConnection = remember(uiState.installButtonVisible) {\n        object : NestedScrollConnection {\n            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {\n                val isScrolledToEnd =\n                    (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1\n                            && (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.size\n                        ?: 0) < listState.layoutInfo.viewportEndOffset)\n                val delta = available.y\n                if (!isScrolledToEnd) {\n                    scrollDistance += delta\n                    if (scrollDistance < -50f) {\n                        if (fabVisible) fabVisible = false\n                        scrollDistance = 0f\n                    } else if (scrollDistance > 50f) {\n                        if (!fabVisible) fabVisible = true\n                        scrollDistance = 0f\n                    }\n                }\n                return Offset.Zero\n            }\n        }\n    }\n    val offsetHeight by animateDpAsState(\n        targetValue = if (fabVisible) 0.dp else 180.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n        animationSpec = tween(durationMillis = 350)\n    )\n\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n\n    Scaffold(\n        topBar = {\n            searchStatus.TopAppBarAnim(hazeState = hazeState, hazeStyle = hazeStyle) {\n                TopAppBar(\n                    color = if (enableBlur) Color.Transparent else colorScheme.surface,\n                    title = stringResource(R.string.module),\n                    actions = {\n                        Box {\n                            val showTopPopup = remember { mutableStateOf(false) }\n                            IconButton(\n                                modifier = Modifier.padding(end = 8.dp),\n                                onClick = { showTopPopup.value = true },\n                                holdDownState = showTopPopup.value\n                            ) {\n                                Icon(\n                                    imageVector = MiuixIcons.MoreCircle,\n                                    tint = colorScheme.onSurface,\n                                    contentDescription = null\n                                )\n                            }\n                            SuperListPopup(\n                                show = showTopPopup.value,\n                                popupPositionProvider = ListPopupDefaults.MenuPositionProvider,\n                                alignment = PopupPositionProvider.Align.TopEnd,\n                                onDismissRequest = {\n                                    showTopPopup.value = false\n                                },\n                                content = {\n                                    ListPopupColumn {\n                                        DropdownImpl(\n                                            text = stringResource(R.string.module_sort_action_first),\n                                            optionSize = 2,\n                                            isSelected = uiState.sortActionFirst,\n                                            onSelectedIndexChange = {\n                                                actions.onToggleSortActionFirst()\n                                                showTopPopup.value = false\n                                            },\n                                            index = 0\n                                        )\n                                        DropdownImpl(\n                                            text = stringResource(R.string.module_sort_enabled_first),\n                                            optionSize = 2,\n                                            isSelected = uiState.sortEnabledFirst,\n                                            onSelectedIndexChange = {\n                                                actions.onToggleSortEnabledFirst()\n                                                showTopPopup.value = false\n                                            },\n                                            index = 1\n                                        )\n                                    }\n                                }\n                            )\n                        }\n                        RebootListPopupMiuix(\n                            modifier = Modifier.padding(end = 16.dp),\n                            alignment = PopupPositionProvider.Align.TopEnd,\n                        )\n                    },\n                    navigationIcon = {\n                        IconButton(\n                            modifier = Modifier.padding(start = 16.dp),\n                            onClick = actions.onOpenRepo,\n                        ) {\n                            Icon(\n                                imageVector = MiuixIcons.Download,\n                                tint = colorScheme.onSurface,\n                                contentDescription = null\n                            )\n                        }\n                    },\n                    scrollBehavior = scrollBehavior\n                )\n            }\n        },\n        floatingActionButton = {\n            if (uiState.installButtonVisible) {\n                val moduleInstall = stringResource(id = R.string.module_install)\n                val confirmTitle = stringResource(R.string.module)\n                var zipUris by remember { mutableStateOf<List<Uri>>(emptyList()) }\n                val confirmDialog = rememberConfirmDialog(\n                    onConfirm = {\n                        actions.onOpenFlash(zipUris)\n                    }\n                )\n                val selectZipLauncher = rememberLauncherForActivityResult(\n                    contract = ActivityResultContracts.StartActivityForResult()\n                ) { activityResult ->\n                    val uris = mutableListOf<Uri>()\n                    if (activityResult.resultCode != RESULT_OK) {\n                        return@rememberLauncherForActivityResult\n                    }\n                    val data = activityResult.data ?: return@rememberLauncherForActivityResult\n                    val clipData = data.clipData\n\n                    if (clipData != null) {\n                        for (i in 0 until clipData.itemCount) {\n                            clipData.getItemAt(i)?.uri?.let { uris.add(it) }\n                        }\n                    } else {\n                        data.data?.let { uris.add(it) }\n                    }\n\n                    if (uris.size == 1) {\n                        actions.onOpenFlash(listOf(uris.first()))\n                    } else if (uris.size > 1) {\n                        // multiple files selected\n                        zipUris = uris\n                        val moduleNames = uris.mapIndexed { index, uri -> \"\\n${index + 1}. ${uri.getFileName(context)}\" }.joinToString(\"\")\n                        val confirmContent = installPromptWithName.format(moduleNames)\n                        confirmDialog.showConfirm(\n                            title = confirmTitle,\n                            content = confirmContent\n                        )\n                    }\n                }\n                FloatingActionButton(\n                    modifier = Modifier\n                        .offset {\n                            IntOffset(x = 0, y = offsetHeight.roundToPx())\n                        }\n                        .padding(bottom = bottomInnerPadding + 20.dp, end = 20.dp)\n                        .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape),\n                    shadowElevation = 0.dp,\n                    onClick = {\n                        // Select the zip files to install\n                        val intent = Intent(Intent.ACTION_GET_CONTENT).apply {\n                            type = \"application/zip\"\n                            putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)\n                        }\n                        selectZipLauncher.launch(intent)\n                    },\n                    content = {\n                        Icon(\n                            Icons.Rounded.Add,\n                            moduleInstall,\n                            modifier = Modifier.size(40.dp),\n                            tint = colorScheme.onPrimary\n                        )\n                    },\n                )\n            }\n        },\n        popupHost = {\n            searchStatus.SearchPager(\n                onSearchStatusChange = actions.onSearchStatusChange,\n                defaultResult = {},\n                searchBarTopPadding = dynamicTopPadding,\n            ) {\n                val imeBottomPadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding()\n                ModuleList(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .overScrollVertical(),\n                    modules = uiState.searchResults,\n                    updateInfoMap = uiState.updateInfo,\n                    actions = actions,\n                    onModuleAddShortcut = ::onModuleAddShortcut,\n                    contentPadding = PaddingValues(\n                        top = 6.dp,\n                        start = 0.dp,\n                        end = 0.dp,\n                        bottom = maxOf(bottomInnerPadding, imeBottomPadding),\n                    ),\n                    animateItems = true,\n                )\n            }\n        },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        if (uiState.magiskInstalled) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(12.dp),\n                contentAlignment = Alignment.Center\n            ) {\n                Text(\n                    stringResource(R.string.module_magisk_conflict),\n                    textAlign = TextAlign.Center,\n                )\n            }\n            return@Scaffold\n        }\n        val layoutDirection = LocalLayoutDirection.current\n        val pullToRefreshState = rememberPullToRefreshState()\n        val refreshTexts = listOf(\n            stringResource(R.string.refresh_pulling),\n            stringResource(R.string.refresh_release),\n            stringResource(R.string.refresh_refresh),\n            stringResource(R.string.refresh_complete),\n        )\n        searchStatus.SearchBox(\n            onSearchStatusChange = actions.onSearchStatusChange,\n            searchBarTopPadding = dynamicTopPadding,\n            contentPadding = PaddingValues(\n                top = innerPadding.calculateTopPadding(),\n                start = innerPadding.calculateStartPadding(layoutDirection),\n                end = innerPadding.calculateEndPadding(layoutDirection)\n            ),\n            hazeState = hazeState,\n            hazeStyle = hazeStyle\n        ) { boxHeight ->\n            if (modules.isEmpty()) {\n                Box(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .padding(\n                            top = innerPadding.calculateTopPadding(),\n                            start = innerPadding.calculateStartPadding(layoutDirection),\n                            end = innerPadding.calculateEndPadding(layoutDirection),\n                            bottom = bottomInnerPadding\n                        ),\n                    contentAlignment = Alignment.Center\n                ) {\n                    Text(\n                        stringResource(R.string.module_empty),\n                        textAlign = TextAlign.Center,\n                        color = Color.Gray,\n                    )\n                }\n            } else {\n                val contentPadding = PaddingValues(\n                    top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp,\n                    start = innerPadding.calculateStartPadding(layoutDirection),\n                    end = innerPadding.calculateEndPadding(layoutDirection),\n                    bottom = bottomInnerPadding,\n                )\n                PullToRefresh(\n                    isRefreshing = uiState.isRefreshing,\n                    pullToRefreshState = pullToRefreshState,\n                    onRefresh = actions.onRefresh,\n                    refreshTexts = refreshTexts,\n                    contentPadding = contentPadding,\n                ) {\n                    ModuleList(\n                        modifier = Modifier\n                            .fillMaxHeight()\n                            .scrollEndHaptic()\n                            .overScrollVertical()\n                            .nestedScroll(scrollBehavior.nestedScrollConnection)\n                            .nestedScroll(nestedScrollConnection)\n                            .let { if (enableBlur) it.hazeSource(state = hazeState) else it },\n                        modules = modules,\n                        updateInfoMap = uiState.updateInfo,\n                        actions = actions,\n                        onModuleAddShortcut = { module, type ->\n                            onModuleAddShortcut(module, type)\n                        },\n                        contentPadding = contentPadding,\n                    )\n                }\n            }\n        }\n    }\n    ModuleShortcutDialog(\n        show = showShortcutDialog.value,\n        onDismissRequest = { showShortcutDialog.value = false },\n        shortcutState = shortcutState,\n        onPickShortcutIcon = { pickShortcutIconLauncher.launch(\"image/*\") },\n        onDeleteShortcut = {\n            shortcutState.deleteShortcut(context)\n            showShortcutDialog.value = false\n        },\n        onConfirmShortcut = {\n            shortcutState.createShortcut(context)\n            showShortcutDialog.value = false\n        },\n    )\n}\n\n@Composable\nprivate fun ModuleShortcutDialog(\n    show: Boolean,\n    onDismissRequest: () -> Unit,\n    shortcutState: ModuleShortcutState,\n    onPickShortcutIcon: () -> Unit,\n    onDeleteShortcut: () -> Unit,\n    onConfirmShortcut: () -> Unit,\n) {\n    SuperDialog(\n        show = show,\n        title = stringResource(R.string.module_shortcut_title),\n        onDismissRequest = onDismissRequest,\n        content = {\n            Column(\n                verticalArrangement = Arrangement.spacedBy(12.dp),\n                horizontalAlignment = Alignment.CenterHorizontally,\n            ) {\n                Box(\n                    contentAlignment = Alignment.Center,\n                    modifier = Modifier\n                        .padding(vertical = 16.dp)\n                        .size(100.dp)\n                        .clip(ContinuousRoundedRectangle(25.dp))\n                ) {\n                    val preview = shortcutState.previewIcon\n                    if (preview != null) {\n                        Image(\n                            bitmap = preview,\n                            modifier = Modifier.size(100.dp),\n                            contentDescription = null,\n                        )\n                    } else {\n                        Box(\n                            modifier = Modifier\n                                .size(100.dp)\n                                .background(Color.White)\n                        )\n                        Image(\n                            painter = painterResource(id = R.drawable.ic_launcher_foreground),\n                            contentDescription = null,\n                            contentScale = FixedScale(1.5f)\n                        )\n                    }\n                }\n                Row {\n                    TextButton(\n                        modifier = Modifier.weight(1f),\n                        text = stringResource(id = R.string.module_shortcut_icon_pick),\n                        onClick = onPickShortcutIcon,\n                    )\n                    AnimatedVisibility(\n                        visible = shortcutState.iconUri != shortcutState.defaultShortcutIconUri,\n                        enter = expandHorizontally() + slideInHorizontally(initialOffsetX = { it }),\n                        exit = shrinkHorizontally() + slideOutHorizontally(targetOffsetX = { it }),\n                        modifier = Modifier.align(Alignment.CenterVertically),\n                    ) {\n                        IconButton(\n                            onClick = shortcutState::resetIconToDefault,\n                            modifier = Modifier.padding(start = 12.dp)\n                        ) {\n                            Icon(\n                                imageVector = MiuixIcons.Undo,\n                                contentDescription = null,\n                                tint = colorScheme.onSurface,\n                                modifier = Modifier.size(28.dp),\n                            )\n                        }\n                    }\n                }\n                TextField(\n                    value = shortcutState.name,\n                    onValueChange = shortcutState::updateName,\n                    label = stringResource(id = R.string.module_shortcut_name_label)\n                )\n                if (shortcutState.hasExistingShortcut) {\n                    TextButton(\n                        text = stringResource(id = R.string.module_shortcut_delete),\n                        onClick = onDeleteShortcut,\n                        modifier = Modifier.fillMaxWidth(),\n                    )\n                }\n                Row(\n                    horizontalArrangement = Arrangement.spacedBy(12.dp)\n                ) {\n                    TextButton(\n                        text = stringResource(id = android.R.string.cancel),\n                        onClick = onDismissRequest,\n                        modifier = Modifier.weight(1f),\n                    )\n                    TextButton(\n                        text = if (shortcutState.hasExistingShortcut) {\n                            stringResource(id = R.string.module_update)\n                        } else {\n                            stringResource(id = android.R.string.ok)\n                        },\n                        onClick = onConfirmShortcut,\n                        colors = ButtonDefaults.textButtonColorsPrimary(),\n                        modifier = Modifier.weight(1f),\n                    )\n                }\n            }\n        }\n    )\n}\n\n@Composable\nprivate fun ModuleList(\n    modifier: Modifier = Modifier,\n    modules: List<Module>,\n    updateInfoMap: Map<String, ModuleUpdateInfo>,\n    actions: ModuleActions,\n    onModuleAddShortcut: (Module, ShortcutType) -> Unit,\n    contentPadding: PaddingValues,\n    animateItems: Boolean = false,\n) {\n    val loadingDialog = rememberLoadingDialog()\n    val scope = rememberCoroutineScope()\n    LazyColumn(\n        modifier = modifier.fillMaxHeight(),\n        contentPadding = contentPadding,\n        overscrollEffect = null,\n    ) {\n        items(\n            items = modules,\n            key = { it.id },\n            contentType = { \"module\" }\n        ) { module ->\n            val currentModuleState = rememberUpdatedState(module)\n            val moduleUpdateInfo = updateInfoMap[module.id] ?: ModuleUpdateInfo.Empty\n            val content: @Composable () -> Unit = {\n                ModuleItem(\n                    module = module,\n                    updateUrl = moduleUpdateInfo.downloadUrl,\n                    onUninstall = {\n                        actions.onRequestUninstallConfirmation(currentModuleState.value)\n                    },\n                    onUndoUninstall = {\n                        scope.launch {\n                            loadingDialog.withLoading { actions.onUndoUninstallModule(module) }\n                        }\n                    },\n                    onCheckChanged = { _: Boolean ->\n                        scope.launch {\n                            loadingDialog.withLoading {\n                                actions.onToggleModule(module)\n                            }\n                        }\n                    },\n                    onUpdate = {\n                        scope.launch {\n                            loadingDialog.withLoading {\n                                actions.onRequestUpdateConfirmation(currentModuleState.value, moduleUpdateInfo)\n                            }\n                        }\n                    },\n                    onExecuteAction = {\n                        actions.onExecuteModuleAction(currentModuleState.value)\n                    },\n                    onAddActionShortcut = { type: ShortcutType ->\n                        onModuleAddShortcut(currentModuleState.value, type)\n                    },\n                    onOpenWebUi = {\n                        if (module.hasWebUi) {\n                            actions.onOpenWebUi(module)\n                        }\n                    }\n                )\n            }\n\n            if (animateItems) {\n                AnimatedVisibility(\n                    visible = true,\n                    enter = fadeIn() + expandVertically(),\n                    exit = fadeOut() + shrinkVertically()\n                ) {\n                    content()\n                }\n            } else {\n                content()\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nfun ModuleItem(\n    module: Module,\n    updateUrl: String,\n    onUndoUninstall: () -> Unit,\n    onUninstall: () -> Unit,\n    onCheckChanged: (Boolean) -> Unit,\n    onUpdate: () -> Unit,\n    onExecuteAction: () -> Unit,\n    onAddActionShortcut: (ShortcutType) -> Unit,\n    onOpenWebUi: () -> Unit\n) {\n    val secondaryContainer = colorScheme.secondaryContainer.copy(alpha = 0.8f)\n    val actionIconTint = colorScheme.onSurface.copy(alpha = if (isInDarkTheme()) 0.7f else 0.9f)\n    val updateBg = colorScheme.tertiaryContainer.copy(alpha = 0.6f)\n    val updateTint = colorScheme.onTertiaryContainer.copy(alpha = 0.8f)\n    val hasUpdate by remember(updateUrl) { derivedStateOf { updateUrl.isNotEmpty() } }\n    val textDecoration by remember(module.remove) {\n        mutableStateOf(if (module.remove) TextDecoration.LineThrough else null)\n    }\n    val hasDescription by remember(module.description) {\n        derivedStateOf { module.description.isNotBlank() }\n    }\n    var expanded by rememberSaveable(module.id) { mutableStateOf(false) }\n\n    Card(\n        modifier = Modifier\n            .padding(horizontal = 12.dp)\n            .padding(bottom = 12.dp),\n        insideMargin = PaddingValues(16.dp),\n        onClick = {\n            if (hasDescription) expanded = !expanded\n        }\n    ) {\n        Row(\n            horizontalArrangement = Arrangement.spacedBy(8.dp),\n            verticalAlignment = Alignment.CenterVertically,\n        ) {\n            Column(\n                modifier = Modifier\n                    .weight(1f)\n                    .padding(end = 4.dp)\n            ) {\n                val moduleVersion = stringResource(id = R.string.module_version)\n                val moduleAuthor = stringResource(id = R.string.module_author)\n\n                SubcomposeLayout { constraints ->\n                    val spacingPx = 6.dp.roundToPx()\n                    var nameTextLayout: TextLayoutResult? = null\n                    val metaPlaceable = if (module.metamodule) {\n                        subcompose(\"meta\") {\n                            Text(\n                                text = \"META\",\n                                fontSize = 12.sp,\n                                color = updateTint,\n                                modifier = Modifier\n                                    .clip(ContinuousRoundedRectangle(6.dp))\n                                    .background(updateBg)\n                                    .padding(horizontal = 6.dp, vertical = 2.dp),\n                                fontWeight = FontWeight(750),\n                                maxLines = 1,\n                                softWrap = false\n                            )\n                        }.first().measure(Constraints(0, constraints.maxWidth, 0, constraints.maxHeight))\n                    } else null\n\n                    val reserved = (metaPlaceable?.width ?: 0) + if (metaPlaceable != null) spacingPx else 0\n                    val nameMax = (constraints.maxWidth - reserved).coerceAtLeast(0)\n                    val namePlaceable = subcompose(\"name\") {\n                        Text(\n                            text = module.name,\n                            fontSize = 17.sp,\n                            fontWeight = FontWeight(550),\n                            color = colorScheme.onSurface,\n                            textDecoration = textDecoration,\n                            onTextLayout = { nameTextLayout = it }\n                        )\n                    }.first().measure(Constraints(constraints.minWidth, nameMax, constraints.minHeight, constraints.maxHeight))\n\n                    val width = (namePlaceable.width + reserved).coerceIn(constraints.minWidth, constraints.maxWidth)\n                    val height = maxOf(namePlaceable.height, metaPlaceable?.height ?: 0)\n\n                    layout(width, height) {\n                        namePlaceable.placeRelative(0, 0)\n                        val endX = nameTextLayout?.let { layoutRes ->\n                            val last = (layoutRes.lineCount - 1).coerceAtLeast(0)\n                            layoutRes.getLineRight(last).toInt()\n                        } ?: namePlaceable.width\n                        metaPlaceable?.placeRelative(endX + spacingPx, (height - (metaPlaceable.height)) / 2)\n                    }\n                }\n                Text(\n                    text = \"$moduleVersion: ${module.version}\",\n                    fontSize = 12.sp,\n                    modifier = Modifier.padding(top = 2.dp),\n                    fontWeight = FontWeight(550),\n                    color = colorScheme.onSurfaceVariantSummary,\n                    textDecoration = textDecoration\n                )\n                Text(\n                    text = \"$moduleAuthor: ${module.author}\",\n                    fontSize = 12.sp,\n                    modifier = Modifier.padding(bottom = 1.dp),\n                    fontWeight = FontWeight(550),\n                    color = colorScheme.onSurfaceVariantSummary,\n                    textDecoration = textDecoration\n                )\n            }\n            Switch(\n                enabled = !module.update,\n                checked = module.enabled,\n                onCheckedChange = {\n                    if (it != module.enabled) onCheckChanged(it)\n                }\n            )\n        }\n\n        if (hasDescription) {\n            Box(\n                modifier = Modifier\n                    .padding(top = 2.dp)\n                    .animateContentSize(\n                        animationSpec = tween(\n                            durationMillis = 250,\n                            easing = FastOutSlowInEasing\n                        )\n                    )\n            ) {\n                Text(\n                    text = module.description,\n                    fontSize = 14.sp,\n                    color = colorScheme.onSurfaceVariantSummary,\n                    overflow = if (expanded) TextOverflow.Clip else TextOverflow.Ellipsis,\n                    maxLines = if (expanded) Int.MAX_VALUE else 4,\n                    textDecoration = textDecoration\n                )\n            }\n        }\n\n        HorizontalDivider(\n            modifier = Modifier.padding(vertical = 8.dp),\n            thickness = 0.5.dp,\n            color = colorScheme.outline.copy(alpha = 0.5f)\n        )\n\n        Row {\n            AnimatedVisibility(\n                visible = module.enabled && !module.remove && !module.update,\n                enter = fadeIn(),\n                exit = fadeOut()\n            ) {\n                Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {\n                    if (module.hasActionScript) {\n                        Row(\n                            modifier = Modifier\n                                .heightIn(min = 35.dp)\n                                .widthIn(min = 35.dp)\n                                .clip(CircleShape)\n                                .background(secondaryContainer)\n                                .combinedClickable(\n                                    onClick = onExecuteAction,\n                                    onLongClick = { onAddActionShortcut(ShortcutType.Action) }\n                                )\n                                .padding(\n                                    start = if (!module.hasWebUi && !hasUpdate) 6.dp else 0.dp,\n                                    end = if (!module.hasWebUi && !hasUpdate) 8.dp else 0.dp,\n                                ),\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.Center\n                        ) {\n                            Icon(\n                                modifier = Modifier.size(24.dp),\n                                imageVector = Icons.Rounded.PlayArrow,\n                                tint = actionIconTint,\n                                contentDescription = stringResource(R.string.action)\n                            )\n                            if (!module.hasWebUi && !hasUpdate) {\n                                Text(\n                                    modifier = Modifier.padding(start = 3.dp, end = 4.dp),\n                                    text = stringResource(R.string.action),\n                                    color = actionIconTint,\n                                    fontWeight = FontWeight.Medium,\n                                    fontSize = 15.sp,\n                                )\n                            }\n                        }\n                    }\n                    if (module.hasWebUi) {\n                        Row(\n                            modifier = Modifier\n                                .heightIn(min = 35.dp)\n                                .widthIn(min = 35.dp)\n                                .clip(CircleShape)\n                                .background(secondaryContainer)\n                                .combinedClickable(\n                                    onClick = onOpenWebUi,\n                                    onLongClick = { onAddActionShortcut(ShortcutType.WebUI) }\n                                )\n                                .padding(horizontal = if (!module.hasActionScript && !hasUpdate) 10.dp else 0.dp),\n                            verticalAlignment = Alignment.CenterVertically,\n                            horizontalArrangement = Arrangement.Center\n                        ) {\n                            Icon(\n                                modifier = Modifier.size(22.dp),\n                                imageVector = Icons.Rounded.Code,\n                                tint = actionIconTint,\n                                contentDescription = stringResource(R.string.open)\n                            )\n                            if (!module.hasActionScript && !hasUpdate) {\n                                Text(\n                                    modifier = Modifier.padding(start = 4.dp, end = 2.dp),\n                                    text = stringResource(R.string.open),\n                                    color = actionIconTint,\n                                    fontWeight = FontWeight.Medium,\n                                    fontSize = 15.sp,\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n\n            Spacer(Modifier.weight(1f))\n\n            AnimatedVisibility(\n                visible = hasUpdate,\n                enter = fadeIn(),\n                exit = fadeOut()\n            ) {\n                IconButton(\n                    modifier = Modifier.padding(end = 16.dp),\n                    backgroundColor = updateBg,\n                    enabled = !module.remove,\n                    minHeight = 35.dp,\n                    minWidth = 35.dp,\n                    onClick = onUpdate,\n                ) {\n                    Row(\n                        modifier = Modifier.padding(horizontal = 10.dp),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(2.dp),\n                    ) {\n                        Icon(\n                            modifier = Modifier.size(20.dp),\n                            imageVector = MiuixIcons.UploadCloud,\n                            tint = updateTint,\n                            contentDescription = stringResource(R.string.module_update),\n                        )\n                        Text(\n                            modifier = Modifier.padding(start = 4.dp, end = 3.dp),\n                            text = stringResource(R.string.module_update),\n                            color = updateTint,\n                            fontWeight = FontWeight.Medium,\n                            fontSize = 15.sp\n                        )\n                    }\n                }\n            }\n            IconButton(\n                minHeight = 35.dp,\n                minWidth = 35.dp,\n                onClick = if (module.remove) onUndoUninstall else onUninstall,\n                backgroundColor = if (module.remove) {\n                    secondaryContainer.copy(alpha = 0.8f)\n                } else {\n                    secondaryContainer\n                },\n            ) {\n                val animatedPadding by animateDpAsState(\n                    targetValue = if (!hasUpdate) 10.dp else 0.dp,\n                    animationSpec = tween(durationMillis = 300)\n                )\n                Row(\n                    modifier = Modifier.padding(horizontal = animatedPadding),\n                    verticalAlignment = Alignment.CenterVertically,\n                ) {\n                    Icon(\n                        modifier = Modifier.size(20.dp),\n                        imageVector = if (module.remove) {\n                            MiuixIcons.Undo\n                        } else {\n                            MiuixIcons.Delete\n                        },\n                        tint = actionIconTint,\n                        contentDescription = null\n                    )\n                    AnimatedVisibility(\n                        visible = !hasUpdate,\n                        enter = expandHorizontally(),\n                        exit = shrinkHorizontally()\n                    ) {\n                        Text(\n                            modifier = Modifier.padding(start = 4.dp, end = 3.dp),\n                            text = stringResource(\n                                if (module.remove) R.string.undo else R.string.uninstall\n                            ),\n                            color = actionIconTint,\n                            fontWeight = FontWeight.Medium,\n                            fontSize = 15.sp\n                        )\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.module\n\nimport android.content.Intent\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalResources\nimport androidx.compose.ui.unit.Dp\nimport androidx.core.net.toUri\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.data.model.Module\nimport me.weishu.kernelsu.data.model.ModuleUpdateInfo\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.screen.flash.FlashIt\nimport me.weishu.kernelsu.ui.util.download\nimport me.weishu.kernelsu.ui.util.module.fetchModuleDetail\nimport me.weishu.kernelsu.ui.util.module.fetchReleaseDescriptionHtml\nimport me.weishu.kernelsu.ui.util.toggleModule\nimport me.weishu.kernelsu.ui.util.undoUninstallModule\nimport me.weishu.kernelsu.ui.util.uninstallModule\nimport me.weishu.kernelsu.ui.viewmodel.ModuleViewModel\nimport me.weishu.kernelsu.ui.webui.WebUIActivity\nimport okhttp3.Request\n\n@Composable\nfun ModulePager(\n    bottomInnerPadding: Dp\n) {\n    val uiMode = LocalUiMode.current\n    val navigator = LocalNavigator.current\n    val context = LocalContext.current\n    val resource = LocalResources.current\n    val viewModel = viewModel<ModuleViewModel>()\n    val rawUiState by viewModel.uiState.collectAsState()\n    val scope = rememberCoroutineScope()\n    var confirmDialogState by remember { mutableStateOf<ModuleConfirmDialogState?>(null) }\n    var effect by remember { mutableStateOf<ModuleEffect?>(null) }\n\n    val webUILauncher = rememberLauncherForActivityResult(\n        contract = ActivityResultContracts.StartActivityForResult()\n    ) { viewModel.fetchModuleList() }\n\n    LaunchedEffect(Unit) {\n        viewModel.refreshEnvironmentState()\n        viewModel.initializePreferences()\n\n        if (rawUiState.moduleList.isEmpty() || viewModel.isNeedRefresh) {\n            viewModel.fetchModuleList(checkUpdate = true)\n        }\n    }\n\n    suspend fun buildUpdateConfirmDialogState(\n        module: Module,\n        updateInfo: ModuleUpdateInfo,\n    ): ModuleConfirmDialogState {\n        val changelogUrl = updateInfo.changelog\n\n        var changelog = \"\"\n        var html = false\n\n        if (changelogUrl.isNotBlank()) {\n            withContext(Dispatchers.IO) {\n                if (changelogUrl.startsWith(\"#\") && changelogUrl.contains('@')) {\n                    val parts = changelogUrl.substring(1).split('@', limit = 2)\n                    if (parts.size == 2) {\n                        fetchReleaseDescriptionHtml(parts[0], parts[1])?.let {\n                            changelog = it\n                            html = true\n                        }\n                    }\n                }\n\n                if (changelog.isBlank()) {\n                    changelog = runCatching {\n                        ksuApp.okhttpClient.newCall(\n                            Request.Builder().url(changelogUrl).build()\n                        ).execute().body.string()\n                    }.getOrDefault(\"\")\n                }\n            }\n        }\n\n        if (changelog.isBlank()) {\n            withContext(Dispatchers.IO) {\n                runCatching {\n                    val latestTag = fetchModuleDetail(module.id)?.latestTag.orEmpty()\n                    if (latestTag.isNotBlank()) {\n                        fetchReleaseDescriptionHtml(module.id, latestTag)?.let {\n                            changelog = it\n                            html = true\n                        }\n                    }\n                }\n            }\n        }\n\n        return ModuleConfirmDialogState(\n            request = ModuleConfirmRequest.Update(\n                module = module,\n                downloadUrl = updateInfo.downloadUrl,\n                fileName = \"${module.name}-${updateInfo.version}.zip\",\n            ),\n            title = if (changelog.isNotBlank()) resource.getString(R.string.module_changelog) else resource.getString(R.string.module_update),\n            content = changelog.ifBlank { resource.getString(R.string.module_start_downloading).format(module.name) },\n            markdown = changelog.isNotBlank() && !html,\n            html = html,\n            confirm = resource.getString(R.string.module_update),\n        )\n    }\n\n    val actions = ModuleActions(\n        onRefresh = {\n            viewModel.fetchModuleList(checkUpdate = true)\n        },\n        onSearchStatusChange = {\n            viewModel.updateSearchStatus(it)\n        },\n        onSearchTextChange = { text ->\n            viewModel.updateSearchText(text)\n        },\n        onClearSearch = {\n            viewModel.updateSearchText(\"\")\n        },\n        onRequestUpdateConfirmation = { module, updateInfo ->\n            scope.launch {\n                confirmDialogState = buildUpdateConfirmDialogState(module, updateInfo)\n            }\n        },\n        onRequestUninstallConfirmation = { module ->\n            confirmDialogState = ModuleConfirmDialogState(\n                request = ModuleConfirmRequest.Uninstall(module),\n                title = resource.getString(R.string.module),\n                content = (if (module.metamodule) resource.getString(R.string.metamodule_uninstall_confirm) else resource.getString(R.string.module_uninstall_confirm)).format(\n                    module.name\n                ),\n                confirm = resource.getString(R.string.uninstall),\n                dismiss = resource.getString(android.R.string.cancel),\n            )\n        },\n        onDismissConfirmRequest = {\n            confirmDialogState = null\n        },\n        onConsumeEffect = {\n            effect = null\n        },\n        onConfirmUpdate = { request ->\n            scope.launch {\n                withContext(Dispatchers.IO) {\n                    download(\n                        url = request.downloadUrl,\n                        fileName = request.fileName,\n                        onDownloaded = { uri ->\n                            navigator.push(Route.Flash(FlashIt.FlashModules(listOf(uri))))\n                            viewModel.markNeedRefresh()\n                        },\n                        onDownloading = {\n                            effect = ModuleEffect.Toast(resource.getString(R.string.module_downloading).format(request.module.name))\n                        },\n                    )\n                }\n                confirmDialogState = null\n            }\n        },\n        onOpenRepo = { navigator.push(Route.ModuleRepo) },\n        onToggleSortActionFirst = {\n            viewModel.toggleSortActionFirst()\n        },\n        onToggleSortEnabledFirst = {\n            viewModel.toggleSortEnabledFirst()\n        },\n        onOpenWebUi = { module ->\n            webUILauncher.launch(\n                Intent(context, WebUIActivity::class.java)\n                    .setData(\"kernelsu://webui/${module.id}\".toUri())\n                    .putExtra(\"id\", module.id)\n            )\n        },\n        onToggleModule = { module ->\n            scope.launch {\n                val success = withContext(Dispatchers.IO) {\n                    toggleModule(module.id, !module.enabled)\n                }\n                if (success) {\n                    viewModel.fetchModuleList(checkUpdate = true)\n                    effect = ModuleEffect.SnackBar(resource.getString(R.string.reboot_to_apply))\n                } else {\n                    val message = if (module.enabled) R.string.module_failed_to_disable else R.string.module_failed_to_enable\n                    effect = ModuleEffect.SnackBar(resource.getString(message).format(module.name))\n                }\n            }\n        },\n        onUninstallModule = { module ->\n            scope.launch {\n                val success = withContext(Dispatchers.IO) {\n                    uninstallModule(module.id)\n                }\n                if (success) {\n                    viewModel.fetchModuleList(checkUpdate = true)\n                }\n                confirmDialogState = null\n                effect = ModuleEffect.SnackBar(\n                    resource.getString(\n                        if (success) R.string.module_uninstall_success else R.string.module_uninstall_failed\n                    ).format(module.name)\n                )\n            }\n        },\n        onUndoUninstallModule = { module ->\n            scope.launch {\n                val success = withContext(Dispatchers.IO) {\n                    undoUninstallModule(module.id)\n                }\n                if (success) {\n                    viewModel.fetchModuleList(checkUpdate = true)\n                }\n                effect = ModuleEffect.SnackBar(\n                    resource.getString(\n                        if (success) R.string.module_undo_uninstall_success else R.string.module_undo_uninstall_failed\n                    ).format(module.name)\n                )\n            }\n        },\n        onOpenFlash = { uris ->\n            if (uris.isNotEmpty()) {\n                navigator.push(Route.Flash(FlashIt.FlashModules(uris)))\n                viewModel.markNeedRefresh()\n            }\n        },\n        onExecuteModuleAction = { module ->\n            navigator.push(Route.ExecuteModuleAction(module.id))\n            viewModel.markNeedRefresh()\n        },\n    )\n\n    when (uiMode) {\n        UiMode.Miuix -> ModulePagerMiuix(\n            uiState = rawUiState,\n            confirmDialogState = confirmDialogState,\n            effect = effect,\n            actions = actions,\n            bottomInnerPadding = bottomInnerPadding,\n        )\n\n        UiMode.Material -> ModulePagerMaterial(\n            uiState = rawUiState,\n            confirmDialogState = confirmDialogState,\n            effect = effect,\n            actions = actions,\n            bottomInnerPadding = bottomInnerPadding,\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleShortcutState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.module\n\nimport android.content.Context\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.Stable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.listSaver\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.graphics.ImageBitmap\nimport androidx.compose.ui.graphics.asImageBitmap\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.data.model.Module\nimport me.weishu.kernelsu.ui.util.module.Shortcut\n\n@Stable\nclass ModuleShortcutState internal constructor(\n    moduleId: String?,\n    name: String,\n    iconUri: String?,\n    selectedTypeName: String?,\n    supportsActionShortcut: Boolean,\n    supportsWebUiShortcut: Boolean,\n    defaultActionIconUri: String?,\n    defaultWebUiIconUri: String?,\n) {\n    var moduleId by mutableStateOf(moduleId)\n        private set\n\n    var name by mutableStateOf(name)\n\n    var iconUri by mutableStateOf(iconUri)\n        private set\n\n    var selectedType by mutableStateOf(selectedTypeName?.let(ShortcutType::valueOf))\n        private set\n\n    var supportsActionShortcut by mutableStateOf(supportsActionShortcut)\n        private set\n\n    var supportsWebUiShortcut by mutableStateOf(supportsWebUiShortcut)\n        private set\n\n    var defaultActionIconUri by mutableStateOf(defaultActionIconUri)\n        private set\n\n    var defaultWebUiIconUri by mutableStateOf(defaultWebUiIconUri)\n        private set\n\n    var previewIcon by mutableStateOf<ImageBitmap?>(null)\n        internal set\n\n    var hasExistingShortcut by mutableStateOf(false)\n        internal set\n\n    val availableTypes: List<ShortcutType>\n        get() = buildList {\n            if (supportsActionShortcut) add(ShortcutType.Action)\n            if (supportsWebUiShortcut) add(ShortcutType.WebUI)\n        }\n\n    val defaultShortcutIconUri: String?\n        get() = selectedType?.let(::defaultIconFor)\n\n    fun bindModule(module: Module) {\n        moduleId = module.id\n        name = module.name\n        iconUri = null\n        selectedType = null\n        supportsActionShortcut = module.hasActionScript\n        supportsWebUiShortcut = module.hasWebUi\n        defaultActionIconUri = module.actionIconPath\n            ?.takeIf { it.isNotBlank() }\n            ?.let { \"su:$it\" }\n        defaultWebUiIconUri = module.webUiIconPath\n            ?.takeIf { it.isNotBlank() }\n            ?.let { \"su:$it\" }\n    }\n\n    fun selectType(type: ShortcutType) {\n        selectedType = type\n        iconUri = defaultIconFor(type)\n    }\n\n    fun updateName(value: String) {\n        name = value\n    }\n\n    fun updateIconUri(value: String?) {\n        iconUri = value\n    }\n\n    fun resetIconToDefault() {\n        iconUri = defaultShortcutIconUri\n    }\n\n    fun createShortcut(context: Context) {\n        val currentModuleId = moduleId\n        val type = selectedType\n        if (currentModuleId.isNullOrBlank() || name.isBlank() || type == null) {\n            return\n        }\n        createModuleShortcut(\n            context = context,\n            moduleId = currentModuleId,\n            name = name,\n            iconUri = iconUri,\n            type = type,\n        )\n        hasExistingShortcut = true\n    }\n\n    fun deleteShortcut(context: Context) {\n        val currentModuleId = moduleId\n        val type = selectedType\n        if (currentModuleId.isNullOrBlank() || type == null) {\n            return\n        }\n        deleteModuleShortcut(context, currentModuleId, type)\n        hasExistingShortcut = false\n    }\n\n    private fun defaultIconFor(type: ShortcutType): String? {\n        return when (type) {\n            ShortcutType.Action -> defaultActionIconUri ?: defaultWebUiIconUri\n            ShortcutType.WebUI -> defaultWebUiIconUri ?: defaultActionIconUri\n        }\n    }\n\n    companion object {\n        val Saver = listSaver<ModuleShortcutState, Any?>(\n            save = {\n                listOf(\n                    it.moduleId,\n                    it.name,\n                    it.iconUri,\n                    it.selectedType?.name,\n                    it.supportsActionShortcut,\n                    it.supportsWebUiShortcut,\n                    it.defaultActionIconUri,\n                    it.defaultWebUiIconUri,\n                )\n            },\n            restore = {\n                ModuleShortcutState(\n                    moduleId = it[0] as String?,\n                    name = it[1] as String,\n                    iconUri = it[2] as String?,\n                    selectedTypeName = it[3] as String?,\n                    supportsActionShortcut = it[4] as Boolean,\n                    supportsWebUiShortcut = it[5] as Boolean,\n                    defaultActionIconUri = it[6] as String?,\n                    defaultWebUiIconUri = it[7] as String?,\n                )\n            }\n        )\n    }\n}\n\n@Composable\nfun rememberModuleShortcutState(\n    context: Context,\n): ModuleShortcutState {\n    val state = rememberSaveable(saver = ModuleShortcutState.Saver) {\n        ModuleShortcutState(\n            moduleId = null,\n            name = \"\",\n            iconUri = null,\n            selectedTypeName = null,\n            supportsActionShortcut = false,\n            supportsWebUiShortcut = false,\n            defaultActionIconUri = null,\n            defaultWebUiIconUri = null,\n        )\n    }\n\n    LaunchedEffect(state.iconUri) {\n        val uri = state.iconUri\n        if (uri.isNullOrBlank()) {\n            state.previewIcon = null\n            return@LaunchedEffect\n        }\n        val bitmap = withContext(Dispatchers.IO) {\n            Shortcut.loadShortcutBitmap(context, uri)\n        }\n        state.previewIcon = bitmap?.asImageBitmap()\n    }\n\n    LaunchedEffect(state.moduleId, state.selectedType) {\n        val moduleId = state.moduleId\n        val type = state.selectedType\n        if (moduleId.isNullOrBlank() || type == null) {\n            state.hasExistingShortcut = false\n            return@LaunchedEffect\n        }\n        state.hasExistingShortcut = withContext(Dispatchers.IO) {\n            hasModuleShortcut(context, moduleId, type)\n        }\n    }\n\n    return remember(state) { state }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.module\n\nimport android.net.Uri\nimport me.weishu.kernelsu.data.model.Module\nimport me.weishu.kernelsu.data.model.ModuleUpdateInfo\nimport me.weishu.kernelsu.ui.component.SearchStatus\n\nsealed interface ModuleConfirmRequest {\n    data class Update(\n        val module: Module,\n        val downloadUrl: String,\n        val fileName: String,\n    ) : ModuleConfirmRequest\n\n    data class Uninstall(\n        val module: Module,\n    ) : ModuleConfirmRequest\n}\n\ndata class ModuleConfirmDialogState(\n    val request: ModuleConfirmRequest,\n    val title: String,\n    val content: String? = null,\n    val markdown: Boolean = false,\n    val html: Boolean = false,\n    val confirm: String? = null,\n    val dismiss: String? = null,\n)\n\nsealed interface ModuleEffect {\n    data class Toast(\n        val message: String,\n    ) : ModuleEffect\n\n    data class SnackBar(\n        val message: String,\n    ) : ModuleEffect\n}\n\ndata class ModuleUiState(\n    val isRefreshing: Boolean = false,\n    val modules: List<Module> = emptyList(),\n    val moduleList: List<Module> = emptyList(),\n    val updateInfo: Map<String, ModuleUpdateInfo> = emptyMap(),\n    val searchStatus: SearchStatus = SearchStatus(\"\"),\n    val searchResults: List<Module> = emptyList(),\n    val sortEnabledFirst: Boolean = false,\n    val sortActionFirst: Boolean = false,\n    val checkModuleUpdate: Boolean = true,\n    val isSafeMode: Boolean = false,\n    val magiskInstalled: Boolean = false,\n) {\n    val installButtonVisible: Boolean\n        get() = !(isSafeMode || magiskInstalled)\n}\n\ndata class ModuleActions(\n    val onRefresh: () -> Unit,\n    val onSearchStatusChange: (SearchStatus) -> Unit,\n    val onSearchTextChange: (String) -> Unit,\n    val onClearSearch: () -> Unit,\n    val onRequestUpdateConfirmation: (Module, ModuleUpdateInfo) -> Unit,\n    val onRequestUninstallConfirmation: (Module) -> Unit,\n    val onDismissConfirmRequest: () -> Unit,\n    val onConsumeEffect: () -> Unit,\n    val onConfirmUpdate: (ModuleConfirmRequest.Update) -> Unit,\n    val onOpenRepo: () -> Unit,\n    val onToggleSortActionFirst: () -> Unit,\n    val onToggleSortEnabledFirst: () -> Unit,\n    val onOpenWebUi: (Module) -> Unit,\n    val onToggleModule: (Module) -> Unit,\n    val onUninstallModule: (Module) -> Unit,\n    val onUndoUninstallModule: (Module) -> Unit,\n    val onOpenFlash: (List<Uri>) -> Unit,\n    val onExecuteModuleAction: (Module) -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleUtils.kt",
    "content": "package me.weishu.kernelsu.ui.screen.module\n\nimport android.content.Context\nimport me.weishu.kernelsu.ui.util.module.Shortcut\n\nenum class ShortcutType {\n    Action,\n    WebUI\n}\n\nfun hasModuleShortcut(context: Context, moduleId: String, type: ShortcutType): Boolean {\n    return when (type) {\n        ShortcutType.Action -> Shortcut.hasModuleActionShortcut(context, moduleId)\n        ShortcutType.WebUI -> Shortcut.hasModuleWebUiShortcut(context, moduleId)\n    }\n}\n\nfun deleteModuleShortcut(context: Context, moduleId: String, type: ShortcutType) {\n    when (type) {\n        ShortcutType.Action -> Shortcut.deleteModuleActionShortcut(context, moduleId)\n        ShortcutType.WebUI -> Shortcut.deleteModuleWebUiShortcut(context, moduleId)\n    }\n}\n\nfun createModuleShortcut(\n    context: Context,\n    moduleId: String,\n    name: String,\n    iconUri: String?,\n    type: ShortcutType\n) {\n    when (type) {\n        ShortcutType.Action -> {\n            Shortcut.createModuleActionShortcut(\n                context = context,\n                moduleId = moduleId,\n                name = name,\n                iconUri = iconUri\n            )\n        }\n\n        ShortcutType.WebUI -> {\n            Shortcut.createModuleWebUiShortcut(\n                context = context,\n                moduleId = moduleId,\n                name = name,\n                iconUri = iconUri\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/modulerepo/ModuleRepoMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.modulerepo\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.net.Uri\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.defaultMinSize\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyListState\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.automirrored.outlined.ArrowBack\nimport androidx.compose.material.icons.automirrored.outlined.ChromeReaderMode\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material.icons.outlined.Download\nimport androidx.compose.material.icons.outlined.Link\nimport androidx.compose.material.icons.rounded.Star\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.CircularWavyProgressIndicator\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.FilledTonalButton\nimport androidx.compose.material3.HorizontalDivider\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeFlexibleTopAppBar\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.PrimaryTabRow\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Tab\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.platform.LocalLocale\nimport androidx.compose.ui.platform.UriHandler\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.data.model.RepoModule\nimport me.weishu.kernelsu.ui.component.GithubMarkdown\nimport me.weishu.kernelsu.ui.component.dialog.ConfirmDialogHandle\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.component.material.SearchAppBar\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedListItem\nimport me.weishu.kernelsu.ui.component.statustag.StatusTag\nimport me.weishu.kernelsu.ui.screen.home.TonalCard\nimport me.weishu.kernelsu.ui.util.download\nimport java.text.Collator\n\n@SuppressLint(\"LocalContextGetResourceValueCall\")\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun ModuleRepoScreenMaterial(\n    state: ModuleRepoUiState,\n    actions: ModuleRepoActions,\n) {\n    val listState = rememberLazyListState()\n    val searchListState = rememberLazyListState()\n\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    Scaffold(\n        topBar = {\n            SearchAppBar(\n                title = { Text(text = stringResource(R.string.module_repos)) },\n                searchText = state.searchStatus.searchText,\n                onSearchTextChange = actions.onSearchTextChange,\n                onClearClick = actions.onClearSearch,\n                scrollBehavior = scrollBehavior,\n                navigationIcon = {\n                    IconButton(\n                        onClick = actions.onBack,\n                        content = { Icon(Icons.AutoMirrored.Outlined.ArrowBack, null) }\n                    )\n                },\n                actions = {\n                    var showDropdown by remember { mutableStateOf(false) }\n\n                    IconButton(\n                        onClick = { showDropdown = true }\n                    ) {\n                        Icon(\n                            imageVector = Icons.Filled.MoreVert,\n                            contentDescription = stringResource(id = R.string.settings)\n                        )\n\n                        DropdownMenu(expanded = showDropdown, onDismissRequest = {\n                            showDropdown = false\n                        }) {\n                            DropdownMenuItem(\n                                text = { Text(stringResource(R.string.module_repos_sort_name)) },\n                                trailingIcon = { Checkbox(state.sortByName, null) },\n                                onClick = {\n                                    actions.onToggleSortByName()\n                                }\n                            )\n                        }\n                    }\n                },\n                searchContent = { _, closeSearch ->\n                    LaunchedEffect(state.searchStatus.searchText) {\n                        searchListState.scrollToItem(0)\n                    }\n                    val sortByName = state.sortByName\n                    val collator = Collator.getInstance(LocalLocale.current.platformLocale)\n                    val searchModules = if (!sortByName) {\n                        state.searchResults\n                    } else {\n                        state.searchResults.sortedWith(compareBy(collator) { it.moduleName })\n                    }\n                    RepoModuleList(\n                        modules = searchModules,\n                        listState = searchListState,\n                        modifier = Modifier.fillMaxSize(),\n                        onModuleClick = {\n                            closeSearch()\n                            actions.onOpenRepoDetail(it)\n                        }\n                    )\n                }\n            )\n        },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        val isLoading = state.modules.isEmpty()\n\n        if (isLoading) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding),\n                contentAlignment = Alignment.Center\n            ) {\n                if (state.offline) {\n                    Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                        Text(text = stringResource(R.string.network_offline), color = MaterialTheme.colorScheme.outline)\n                        Spacer(Modifier.height(12.dp))\n                        Button(\n                            onClick = actions.onRefresh,\n                        ) {\n                            Text(stringResource(R.string.network_retry))\n                        }\n                    }\n                } else {\n                    LoadingIndicator()\n                }\n            }\n        } else {\n            val displayModules = run {\n                val base = state.modules\n                val sortByName = state.sortByName\n                val collator = Collator.getInstance(LocalLocale.current.platformLocale)\n                if (!sortByName) base else base.sortedWith(compareBy(collator) { it.moduleName })\n            }\n            RepoModuleList(\n                modules = displayModules,\n                listState = listState,\n                modifier = Modifier\n                    .fillMaxSize()\n                    .nestedScroll(scrollBehavior.nestedScrollConnection)\n                    .padding(innerPadding),\n                onModuleClick = actions.onOpenRepoDetail\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun RepoModuleList(\n    modules: List<RepoModule>,\n    listState: LazyListState,\n    modifier: Modifier = Modifier,\n    bottomPadding: Dp = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding(),\n    onModuleClick: (RepoModule) -> Unit,\n) {\n    LazyColumn(\n        modifier = modifier,\n        state = listState,\n        verticalArrangement = Arrangement.spacedBy(16.dp),\n        contentPadding = PaddingValues(\n            start = 16.dp,\n            top = 8.dp,\n            end = 16.dp,\n            bottom = 16.dp + bottomPadding\n        ),\n    ) {\n        items(modules, key = { it.moduleId }, contentType = { \"module\" }) { module ->\n            val latestReleaseTime = remember(module.latestReleaseTime) { module.latestReleaseTime }\n            val moduleAuthor = stringResource(id = R.string.module_author)\n\n            TonalCard(modifier = Modifier.fillMaxWidth()) {\n                Column(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .clickable { onModuleClick(module) }\n                        .padding(22.dp, 18.dp, 22.dp, 12.dp)\n                ) {\n                    if (module.moduleName.isNotEmpty()) {\n                        Text(\n                            text = module.moduleName,\n                            style = MaterialTheme.typography.titleMedium,\n                            fontWeight = FontWeight.SemiBold,\n                            lineHeight = MaterialTheme.typography.bodySmall.lineHeight,\n                        )\n                    }\n                    if (module.moduleId.isNotEmpty()) {\n                        Text(\n                            text = \"ID: ${module.moduleId}\",\n                            style = MaterialTheme.typography.bodyMedium,\n                            color = MaterialTheme.colorScheme.onSurfaceVariant,\n                        )\n                    }\n                    Text(\n                        text = \"$moduleAuthor: ${module.authors}\",\n                        style = MaterialTheme.typography.bodyMedium,\n                        color = MaterialTheme.colorScheme.onSurfaceVariant,\n                    )\n                    if (module.summary.isNotEmpty()) {\n                        Spacer(modifier = Modifier.height(8.dp))\n                        Text(\n                            text = module.summary,\n                            color = MaterialTheme.colorScheme.outline,\n                            style = MaterialTheme.typography.bodyMedium,\n                            overflow = TextOverflow.Ellipsis,\n                            maxLines = 4,\n                        )\n                    }\n\n                    Row(modifier = Modifier.padding(vertical = 4.dp)) {\n                        if (module.metamodule) {\n                            StatusTag(\n                                \"META\",\n                                contentColor = MaterialTheme.colorScheme.onPrimary,\n                                backgroundColor = MaterialTheme.colorScheme.primary\n                            )\n                        }\n                    }\n                    HorizontalDivider(thickness = Dp.Hairline)\n                    Spacer(modifier = Modifier.height(4.dp))\n\n                    Row(\n                        horizontalArrangement = Arrangement.SpaceBetween,\n                        verticalAlignment = Alignment.CenterVertically\n                    ) {\n                        if (module.stargazerCount > 0) {\n                            Row(verticalAlignment = Alignment.CenterVertically) {\n                                Icon(\n                                    imageVector = Icons.Rounded.Star,\n                                    contentDescription = \"stars\",\n                                    tint = MaterialTheme.colorScheme.outline,\n                                    modifier = Modifier.size(16.dp)\n                                )\n                                Text(\n                                    text = module.stargazerCount.toString(),\n                                    style = MaterialTheme.typography.bodyMedium,\n                                    color = MaterialTheme.colorScheme.outline,\n                                    modifier = Modifier.padding(start = 4.dp)\n                                )\n                            }\n                        }\n                        Spacer(Modifier.weight(1f))\n                        if (latestReleaseTime.isNotEmpty()) {\n                            Text(\n                                text = latestReleaseTime,\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.outline,\n                            )\n                        }\n                    }\n                }\n            }\n        }\n        item { Spacer(Modifier.height(12.dp)) }\n    }\n}\n\n@SuppressLint(\"StringFormatInvalid\", \"DefaultLocale\")\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun ModuleRepoDetailScreenMaterial(\n    state: ModuleRepoDetailUiState,\n    actions: ModuleRepoDetailActions,\n) {\n    val module = state.module\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val confirmTitle = stringResource(R.string.module_install)\n    var pendingDownload by remember { mutableStateOf<(() -> Unit)?>(null) }\n    val confirmDialog = rememberConfirmDialog(onConfirm = { pendingDownload?.invoke() })\n\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    LaunchedEffect(Unit) {\n        scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit\n    }\n\n    Scaffold(\n        topBar = {\n            LargeFlexibleTopAppBar(\n                title = { Text(text = module.moduleName) },\n                navigationIcon = {\n                    IconButton(onClick = actions.onBack) {\n                        Icon(\n                            imageVector = Icons.AutoMirrored.Filled.ArrowBack,\n                            contentDescription = null,\n                        )\n                    }\n                },\n                actions = {\n                    if (state.webUrl.isNotEmpty()) {\n                        IconButton(onClick = actions.onOpenWebUrl) {\n                            Icon(\n                                imageVector = Icons.AutoMirrored.Outlined.ChromeReaderMode,\n                                contentDescription = null,\n                            )\n                        }\n                    }\n                },\n                colors = TopAppBarDefaults.topAppBarColors(\n                    containerColor = MaterialTheme.colorScheme.surface,\n                    scrolledContainerColor = MaterialTheme.colorScheme.surface\n                ),\n                scrollBehavior = scrollBehavior\n            )\n        },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal),\n    ) { innerPadding ->\n        val tabs = listOf(\n            stringResource(R.string.tab_readme),\n            stringResource(R.string.tab_releases),\n            stringResource(R.string.tab_info)\n        )\n        val pagerState = rememberPagerState(initialPage = 0, pageCount = { tabs.size })\n        val layoutDirection = LocalLayoutDirection.current\n        Box(modifier = Modifier.fillMaxSize()) {\n            HorizontalPager(\n                state = pagerState,\n                modifier = Modifier.fillMaxSize(),\n                beyondViewportPageCount = 2\n            ) { page ->\n                val paddedInnerPadding = PaddingValues(\n                    top = innerPadding.calculateTopPadding() + 56.dp + 8.dp,\n                    start = innerPadding.calculateStartPadding(layoutDirection),\n                    end = innerPadding.calculateEndPadding(layoutDirection),\n                    bottom = innerPadding.calculateBottomPadding()\n                            + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() + 16.dp\n                )\n                when (page) {\n                    0 -> ReadmePage(\n                        readmeHtml = state.readmeHtml,\n                        readmeLoaded = state.readmeLoaded,\n                        innerPadding = paddedInnerPadding,\n                        scrollBehavior = scrollBehavior\n                    )\n\n                    1 -> ReleasesPage(\n                        detailReleases = state.detailReleases,\n                        innerPadding = paddedInnerPadding,\n                        scrollBehavior = scrollBehavior,\n                        confirmTitle = confirmTitle,\n                        confirmDialog = confirmDialog,\n                        scope = scope,\n                        onInstallModule = actions.onInstallModule,\n                        context = context,\n                        setPendingDownload = { pendingDownload = it }\n                    )\n\n                    2 -> InfoPage(\n                        module = module,\n                        innerPadding = paddedInnerPadding,\n                        scrollBehavior = scrollBehavior,\n                        uriHandler = object : UriHandler {\n                            override fun openUri(uri: String) = actions.onOpenUrl(uri)\n                        },\n                        sourceUrl = state.sourceUrl\n                    )\n                }\n            }\n            PrimaryTabRow(\n                selectedTabIndex = pagerState.currentPage,\n                containerColor = MaterialTheme.colorScheme.surface,\n                modifier = Modifier.padding(top = innerPadding.calculateTopPadding()),\n            ) {\n                tabs.forEachIndexed { index, tab ->\n                    Tab(\n                        selected = pagerState.currentPage == index,\n                        onClick = { scope.launch { pagerState.animateScrollToPage(index) } },\n                        text = { Text(tab) },\n                        unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun ReadmePage(\n    readmeHtml: String?,\n    readmeLoaded: Boolean,\n    innerPadding: PaddingValues,\n    scrollBehavior: TopAppBarScrollBehavior\n) {\n    val layoutDirection = LocalLayoutDirection.current\n\n    LazyColumn(\n        modifier = Modifier\n            .fillMaxSize()\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        contentPadding = PaddingValues(\n            top = innerPadding.calculateTopPadding(),\n            start = innerPadding.calculateStartPadding(layoutDirection),\n            end = innerPadding.calculateEndPadding(layoutDirection),\n            bottom = innerPadding.calculateBottomPadding(),\n        ),\n    ) {\n        item {\n            var isLoading by remember { mutableStateOf(true) }\n            if (isLoading) {\n                Box(\n                    modifier = Modifier.fillParentMaxSize(),\n                    contentAlignment = Alignment.Center\n                ) {\n                    LoadingIndicator()\n                }\n            }\n            if (readmeLoaded && readmeHtml != null) {\n                Box(modifier = Modifier.fillMaxSize()) {\n                    GithubMarkdown(\n                        content = readmeHtml,\n                        onLoadingChange = { isLoading = it },\n                        containerColor = MaterialTheme.colorScheme.surface,\n                    )\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@SuppressLint(\"DefaultLocale\")\n@Composable\nfun ReleasesPage(\n    detailReleases: List<ReleaseArg>,\n    innerPadding: PaddingValues,\n    scrollBehavior: TopAppBarScrollBehavior,\n    confirmTitle: String,\n    confirmDialog: ConfirmDialogHandle,\n    scope: CoroutineScope,\n    onInstallModule: (Uri) -> Unit,\n    context: Context,\n    setPendingDownload: ((() -> Unit)) -> Unit,\n) {\n    val layoutDirection = LocalLayoutDirection.current\n    LazyColumn(\n        modifier = Modifier\n            .fillMaxSize()\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        contentPadding = PaddingValues(\n            top = innerPadding.calculateTopPadding(),\n            start = innerPadding.calculateStartPadding(layoutDirection) + 16.dp,\n            end = innerPadding.calculateEndPadding(layoutDirection) + 16.dp,\n            bottom = innerPadding.calculateBottomPadding(),\n        ),\n        verticalArrangement = Arrangement.spacedBy(16.dp),\n    ) {\n        if (detailReleases.isNotEmpty()) {\n            items(\n                items = detailReleases,\n                key = { it.tagName },\n                contentType = { \"release\" }\n            ) { rel ->\n                val title = remember(rel.name, rel.tagName) { rel.name.ifBlank { rel.tagName } }\n                TonalCard {\n                    Column(\n                        modifier = Modifier.padding(vertical = 18.dp, horizontal = 22.dp)\n                    ) {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier.fillMaxWidth()\n                        ) {\n                            Column(modifier = Modifier.weight(1f)) {\n                                Text(\n                                    text = title,\n                                    style = MaterialTheme.typography.titleMedium,\n                                    fontWeight = FontWeight.SemiBold,\n                                    color = MaterialTheme.colorScheme.onSurface\n                                )\n                                Text(\n                                    text = rel.tagName,\n                                    style = MaterialTheme.typography.bodyMedium,\n                                    color = MaterialTheme.colorScheme.outline,\n                                    modifier = Modifier.padding(top = 2.dp)\n                                )\n                            }\n                            Text(\n                                text = rel.publishedAt,\n                                style = MaterialTheme.typography.bodyMedium,\n                                color = MaterialTheme.colorScheme.outline,\n                                modifier = Modifier.align(Alignment.Top)\n                            )\n                        }\n\n                        AnimatedVisibility(\n                            visible = rel.descriptionHTML.isNotEmpty(),\n                            enter = fadeIn() + expandVertically(),\n                            exit = fadeOut() + shrinkVertically()\n                        ) {\n                            Column {\n                                HorizontalDivider(\n                                    modifier = Modifier.padding(vertical = 4.dp),\n                                    thickness = Dp.Hairline,\n                                    color = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)\n                                )\n                                GithubMarkdown(content = rel.descriptionHTML)\n                            }\n                        }\n\n                        AnimatedVisibility(\n                            visible = rel.assets.isNotEmpty(),\n                            enter = fadeIn() + expandVertically(),\n                            exit = fadeOut() + shrinkVertically()\n                        ) {\n                            Column {\n                                HorizontalDivider(\n                                    modifier = Modifier.padding(vertical = 8.dp),\n                                    thickness = Dp.Hairline\n                                )\n\n                                rel.assets.forEachIndexed { index, asset ->\n                                    val fileName = asset.name\n                                    val sizeText = remember(asset.size) {\n                                        val s = asset.size\n                                        when {\n                                            s >= 1024L * 1024L * 1024L -> String.format(\"%.1f GB\", s / (1024f * 1024f * 1024f))\n                                            s >= 1024L * 1024L -> String.format(\"%.1f MB\", s / (1024f * 1024f))\n                                            s >= 1024L -> String.format(\"%.0f KB\", s / 1024f)\n                                            else -> \"$s B\"\n                                        }\n                                    }\n                                    val sizeAndDownloads =\n                                        remember(sizeText, asset.downloadCount) { \"$sizeText · ${asset.downloadCount} downloads\" }\n                                    var isDownloading by remember(fileName, asset.downloadUrl) { mutableStateOf(false) }\n                                    var progress by remember(fileName, asset.downloadUrl) { mutableIntStateOf(0) }\n                                    val onClickDownload = remember(fileName, asset.downloadUrl) {\n                                        {\n                                            val startText = context.getString(R.string.module_start_downloading, fileName)\n                                            setPendingDownload {\n                                                isDownloading = true\n                                                scope.launch(Dispatchers.IO) {\n                                                    download(\n                                                        asset.downloadUrl,\n                                                        fileName,\n                                                        onDownloaded = onInstallModule,\n                                                        onDownloading = { isDownloading = true },\n                                                        onProgress = { p -> scope.launch(Dispatchers.Main) { progress = p } }\n                                                    )\n                                                }\n                                            }\n                                            confirmDialog.showConfirm(title = confirmTitle, content = startText)\n                                        }\n                                    }\n                                    Row(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        verticalAlignment = Alignment.CenterVertically,\n                                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                                    ) {\n                                        Column(modifier = Modifier.weight(1f)) {\n                                            Text(\n                                                text = fileName,\n                                                style = MaterialTheme.typography.bodyMedium,\n                                                color = MaterialTheme.colorScheme.onSurface\n                                            )\n                                            Text(\n                                                text = sizeAndDownloads,\n                                                style = MaterialTheme.typography.bodyMedium,\n                                                color = MaterialTheme.colorScheme.outline,\n                                                modifier = Modifier.padding(top = 2.dp)\n                                            )\n                                        }\n                                        FilledTonalButton(\n                                            onClick = onClickDownload,\n                                            contentPadding = ButtonDefaults.TextButtonContentPadding\n                                        ) {\n                                            if (isDownloading) {\n                                                CircularWavyProgressIndicator(\n                                                    progress = { progress / 100f },\n                                                    modifier = Modifier.size(20.dp),\n                                                )\n                                            } else {\n                                                Icon(\n                                                    modifier = Modifier.size(20.dp),\n                                                    imageVector = Icons.Outlined.Download,\n                                                    contentDescription = stringResource(R.string.install)\n                                                )\n                                                Text(\n                                                    modifier = Modifier.padding(start = 7.dp),\n                                                    text = stringResource(R.string.install),\n                                                    style = MaterialTheme.typography.labelMedium,\n                                                )\n                                            }\n                                        }\n                                    }\n                                    if (index != rel.assets.lastIndex) {\n                                        HorizontalDivider(\n                                            modifier = Modifier.padding(vertical = 8.dp),\n                                            thickness = Dp.Hairline\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun InfoPage(\n    module: RepoModuleArg,\n    innerPadding: PaddingValues,\n    scrollBehavior: TopAppBarScrollBehavior,\n    uriHandler: UriHandler,\n    sourceUrl: String,\n) {\n    val layoutDirection = LocalLayoutDirection.current\n    LazyColumn(\n        modifier = Modifier\n            .fillMaxSize()\n            .nestedScroll(scrollBehavior.nestedScrollConnection),\n        contentPadding = PaddingValues(\n            top = innerPadding.calculateTopPadding(),\n            start = innerPadding.calculateStartPadding(layoutDirection),\n            end = innerPadding.calculateEndPadding(layoutDirection),\n            bottom = innerPadding.calculateBottomPadding(),\n        ),\n    ) {\n        if (module.authorsList.isNotEmpty()) {\n            item {\n                SegmentedColumn(\n                    title = stringResource(R.string.module_author),\n                    modifier = Modifier\n                        .padding(horizontal = 16.dp)\n                        .padding(bottom = 8.dp),\n                    content = module.authorsList.map { author ->\n                        {\n                            SegmentedListItem(\n                                headlineContent = {\n                                    Text(\n                                        text = author.name,\n                                        fontSize = MaterialTheme.typography.bodyMedium.fontSize,\n                                        lineHeight = MaterialTheme.typography.bodyMedium.lineHeight,\n                                        fontFamily = MaterialTheme.typography.bodyMedium.fontFamily\n                                    )\n                                },\n                                trailingContent = {\n                                    FilledTonalButton(\n                                        modifier = Modifier.defaultMinSize(52.dp, 32.dp),\n                                        onClick = { uriHandler.openUri(author.link) },\n                                        contentPadding = ButtonDefaults.TextButtonContentPadding\n                                    ) {\n                                        Icon(\n                                            modifier = Modifier.size(20.dp),\n                                            imageVector = Icons.Outlined.Link,\n                                            contentDescription = null\n                                        )\n                                    }\n                                }\n                            )\n                        }\n                    }\n                )\n            }\n        }\n        if (sourceUrl.isNotEmpty()) {\n            item {\n                SegmentedColumn(\n                    title = stringResource(R.string.module_repos_source_code),\n                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                    content = listOf(\n                        {\n                            SegmentedListItem(\n                                headlineContent = {\n                                    Text(\n                                        text = sourceUrl,\n                                        fontSize = MaterialTheme.typography.bodyMedium.fontSize,\n                                        lineHeight = MaterialTheme.typography.bodyMedium.lineHeight,\n                                        fontFamily = MaterialTheme.typography.bodyMedium.fontFamily\n                                    )\n                                },\n                                trailingContent = {\n                                    FilledTonalButton(\n                                        modifier = Modifier.defaultMinSize(52.dp, 32.dp),\n                                        onClick = { uriHandler.openUri(sourceUrl) },\n                                        contentPadding = ButtonDefaults.TextButtonContentPadding\n                                    ) {\n                                        Icon(\n                                            modifier = Modifier.size(20.dp),\n                                            imageVector = Icons.Outlined.Link,\n                                            contentDescription = null\n                                        )\n                                    }\n                                }\n                            )\n                        }\n                    )\n                )\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/modulerepo/ModuleRepoMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.modulerepo\n\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.net.Uri\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.EaseInOut\nimport androidx.compose.animation.core.tween\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.pager.HorizontalPager\nimport androidx.compose.foundation.pager.rememberPagerState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableIntStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.runtime.withFrameNanos\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.draw.clip\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.platform.LocalLocale\nimport androidx.compose.ui.platform.UriHandler\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport androidx.compose.ui.zIndex\nimport com.kyant.capsule.ContinuousRoundedRectangle\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.GithubMarkdown\nimport me.weishu.kernelsu.ui.component.dialog.ConfirmDialogHandle\nimport me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog\nimport me.weishu.kernelsu.ui.component.miuix.SearchBox\nimport me.weishu.kernelsu.ui.component.miuix.SearchPager\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.theme.isInDarkTheme\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport me.weishu.kernelsu.ui.util.download\nimport top.yukonga.miuix.kmp.basic.Card\nimport top.yukonga.miuix.kmp.basic.CircularProgressIndicator\nimport top.yukonga.miuix.kmp.basic.DropdownImpl\nimport top.yukonga.miuix.kmp.basic.HorizontalDivider\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.IconButton\nimport top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator\nimport top.yukonga.miuix.kmp.basic.ListPopupColumn\nimport top.yukonga.miuix.kmp.basic.ListPopupDefaults\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\nimport top.yukonga.miuix.kmp.basic.PopupPositionProvider\nimport top.yukonga.miuix.kmp.basic.PullToRefresh\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.ScrollBehavior\nimport top.yukonga.miuix.kmp.basic.SmallTitle\nimport top.yukonga.miuix.kmp.basic.TabRow\nimport top.yukonga.miuix.kmp.basic.TabRowDefaults\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.basic.TopAppBar\nimport top.yukonga.miuix.kmp.basic.rememberPullToRefreshState\nimport top.yukonga.miuix.kmp.extra.SuperListPopup\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.extended.Back\nimport top.yukonga.miuix.kmp.icon.extended.FileDownloads\nimport top.yukonga.miuix.kmp.icon.extended.HorizontalSplit\nimport top.yukonga.miuix.kmp.icon.extended.Link\nimport top.yukonga.miuix.kmp.icon.extended.MoreCircle\nimport top.yukonga.miuix.kmp.icon.extended.TopDownloads\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.utils.PressFeedbackType\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\nimport java.text.Collator\n\n@SuppressLint(\"LocalContextGetResourceValueCall\")\n@Composable\nfun ModuleRepoScreenMiuix(\n    state: ModuleRepoUiState,\n    actions: ModuleRepoActions,\n) {\n    val searchStatus = state.searchStatus\n    val platformLocale = LocalLocale.current.platformLocale\n    val metaBg = colorScheme.tertiaryContainer.copy(alpha = 0.6f)\n    val metaTint = colorScheme.onTertiaryContainer.copy(alpha = 0.8f)\n\n    LaunchedEffect(searchStatus.searchText) {\n        actions.onSearchTextChange(searchStatus.searchText)\n    }\n\n    val scrollBehavior = MiuixScrollBehavior()\n    val dynamicTopPadding by remember {\n        derivedStateOf { 12.dp * (1f - scrollBehavior.state.collapsedFraction) }\n    }\n\n    val enableBlur = LocalEnableBlur.current\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n\n    Scaffold(\n        topBar = {\n            searchStatus.TopAppBarAnim(hazeState = hazeState, hazeStyle = hazeStyle) {\n                TopAppBar(\n                    color = if (enableBlur) Color.Transparent else colorScheme.surface,\n                    title = stringResource(R.string.module_repos),\n                    actions = {\n                        val showTopPopup = remember { mutableStateOf(false) }\n                        SuperListPopup(\n                            show = showTopPopup.value,\n                            popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,\n                            alignment = PopupPositionProvider.Align.TopEnd,\n                            onDismissRequest = { showTopPopup.value = false },\n                            content = {\n                                ListPopupColumn {\n                                    DropdownImpl(\n                                        text = stringResource(R.string.module_repos_sort_name),\n                                        optionSize = 1,\n                                        isSelected = state.sortByName,\n                                        onSelectedIndexChange = {\n                                            actions.onToggleSortByName()\n                                            showTopPopup.value = false\n                                        },\n                                        index = 0\n                                    )\n                                }\n                            })\n                        IconButton(\n                            modifier = Modifier.padding(end = 16.dp),\n                            onClick = { showTopPopup.value = true },\n                            holdDownState = showTopPopup.value\n                        ) {\n                            Icon(\n                                imageVector = MiuixIcons.MoreCircle,\n                                tint = colorScheme.onSurface,\n                                contentDescription = null,\n                            )\n                        }\n                    },\n                    navigationIcon = {\n                        IconButton(\n                            modifier = Modifier.padding(start = 16.dp),\n                            onClick = actions.onBack\n\n                        ) {\n                            val layoutDirection = LocalLayoutDirection.current\n                            Icon(\n                                modifier = Modifier.graphicsLayer {\n                                    if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\n                                },\n                                imageVector = MiuixIcons.Back,\n                                contentDescription = null,\n                                tint = colorScheme.onSurface\n                            )\n                        }\n                    },\n                    scrollBehavior = scrollBehavior,\n                )\n            }\n        },\n        popupHost = {\n            searchStatus.SearchPager(\n                onSearchStatusChange = actions.onSearchStatusChange,\n                defaultResult = {},\n                searchBarTopPadding = dynamicTopPadding,\n            ) {\n                LazyColumn(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .overScrollVertical(),\n                ) {\n                    item {\n                        Spacer(Modifier.height(6.dp))\n                    }\n                    val displaySearch = run {\n                        val base = state.searchResults\n                        val sortByName = state.sortByName\n                        val collator = Collator.getInstance(platformLocale)\n                        if (!sortByName) base else base.sortedWith(compareBy(collator) { it.moduleName })\n                    }\n                    items(displaySearch, key = { it.moduleId }) { module ->\n                        Card(\n                            modifier = Modifier\n                                .fillMaxWidth()\n                                .padding(horizontal = 12.dp)\n                                .padding(bottom = 12.dp),\n                            insideMargin = PaddingValues(16.dp),\n                            showIndication = true,\n                            pressFeedbackType = PressFeedbackType.Sink,\n                            onClick = {\n                                actions.onOpenRepoDetail(module)\n                            }\n                        ) {\n                            Column {\n                                if (module.moduleName.isNotEmpty()) {\n                                    Row(verticalAlignment = Alignment.CenterVertically) {\n                                        Text(\n                                            text = module.moduleName,\n                                            fontSize = 17.sp,\n                                            fontWeight = FontWeight(550),\n                                            color = colorScheme.onSurface\n                                        )\n                                        if (module.metamodule) {\n                                            Text(\n                                                text = \"META\",\n                                                fontSize = 12.sp,\n                                                color = metaTint,\n                                                modifier = Modifier\n                                                    .padding(start = 6.dp)\n                                                    .clip(ContinuousRoundedRectangle(6.dp))\n                                                    .background(metaBg)\n                                                    .padding(horizontal = 6.dp, vertical = 2.dp),\n                                                fontWeight = FontWeight(750),\n                                                maxLines = 1\n                                            )\n                                        }\n                                        Spacer(Modifier.weight(1f))\n                                        if (module.stargazerCount > 0) {\n                                            Row(verticalAlignment = Alignment.CenterVertically) {\n                                                Icon(\n                                                    imageVector = MiuixIcons.TopDownloads,\n                                                    contentDescription = \"stars\",\n                                                    tint = colorScheme.onSurfaceVariantSummary,\n                                                    modifier = Modifier.size(16.dp)\n                                                )\n                                                Text(\n                                                    text = module.stargazerCount.toString(),\n                                                    fontSize = 12.sp,\n                                                    color = colorScheme.onSurfaceVariantSummary,\n                                                    modifier = Modifier.padding(start = 4.dp)\n                                                )\n                                            }\n                                        }\n                                    }\n                                }\n                                if (module.moduleId.isNotEmpty()) {\n                                    Text(\n                                        text = \"ID: ${module.moduleId}\",\n                                        fontSize = 12.sp,\n                                        fontWeight = FontWeight(550),\n                                        color = colorScheme.onSurfaceVariantSummary,\n                                    )\n                                }\n                                Text(\n                                    text = \"${stringResource(id = R.string.module_author)}: ${module.authors}\",\n                                    fontSize = 12.sp,\n                                    modifier = Modifier.padding(bottom = 1.dp),\n                                    fontWeight = FontWeight(550),\n                                    color = colorScheme.onSurfaceVariantSummary,\n                                )\n                                if (module.summary.isNotEmpty()) {\n                                    Text(\n                                        text = module.summary,\n                                        fontSize = 14.sp,\n                                        color = colorScheme.onSurfaceVariantSummary,\n                                        modifier = Modifier.padding(top = 2.dp),\n                                        overflow = TextOverflow.Ellipsis,\n                                        maxLines = 4,\n                                    )\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        },\n    ) { innerPadding ->\n        val layoutDirection = LocalLayoutDirection.current\n        val isLoading = state.modules.isEmpty()\n        val offline = state.offline\n\n        if (isLoading) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize(),\n                contentAlignment = Alignment.Center\n            ) {\n                if (offline) {\n                    Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                        Text(text = stringResource(R.string.network_offline), color = colorScheme.onSurfaceVariantSummary, fontSize = 16.sp)\n                        Spacer(Modifier.height(12.dp))\n                        TextButton(\n                            modifier = Modifier\n                                .padding(horizontal = 24.dp)\n                                .fillMaxWidth(),\n                            text = stringResource(R.string.network_retry),\n                            onClick = actions.onRefresh,\n                        )\n                    }\n                } else {\n                    InfiniteProgressIndicator()\n                }\n            }\n        } else {\n            searchStatus.SearchBox(\n                onSearchStatusChange = actions.onSearchStatusChange,\n                searchBarTopPadding = dynamicTopPadding,\n                contentPadding = PaddingValues(\n                    top = innerPadding.calculateTopPadding(),\n                    start = innerPadding.calculateStartPadding(layoutDirection),\n                    end = innerPadding.calculateEndPadding(layoutDirection)\n                ),\n                hazeState = hazeState,\n                hazeStyle = hazeStyle\n            ) { boxHeight ->\n                val pullToRefreshState = rememberPullToRefreshState()\n                val refreshTexts = listOf(\n                    stringResource(R.string.refresh_pulling),\n                    stringResource(R.string.refresh_release),\n                    stringResource(R.string.refresh_refresh),\n                    stringResource(R.string.refresh_complete),\n                )\n                PullToRefresh(\n                    isRefreshing = state.isRefreshing,\n                    pullToRefreshState = pullToRefreshState,\n                    onRefresh = actions.onRefresh,\n                    refreshTexts = refreshTexts,\n                    contentPadding = PaddingValues(\n                        top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp,\n                        start = innerPadding.calculateStartPadding(layoutDirection),\n                        end = innerPadding.calculateEndPadding(layoutDirection)\n                    ),\n                ) {\n                    val displayModules = run {\n                        val base = state.modules\n                        val sortByName = state.sortByName\n                        val collator = Collator.getInstance(platformLocale)\n                        if (!sortByName) base else base.sortedWith(compareBy(collator) { it.moduleName })\n                    }\n                    LazyColumn(\n                        modifier = Modifier\n                            .fillMaxHeight()\n                            .scrollEndHaptic()\n                            .overScrollVertical()\n                            .nestedScroll(scrollBehavior.nestedScrollConnection)\n                            .let { if (enableBlur) it.hazeSource(state = hazeState) else it },\n                        contentPadding = PaddingValues(\n                            top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp,\n                            start = innerPadding.calculateStartPadding(layoutDirection),\n                            end = innerPadding.calculateEndPadding(layoutDirection)\n                        ),\n                        overscrollEffect = null,\n                    ) {\n                        items(\n                            items = displayModules,\n                            key = { it.moduleId },\n                            contentType = { \"module\" }\n                        ) { module ->\n                            val moduleAuthor = stringResource(id = R.string.module_author)\n\n                            Card(\n                                modifier = Modifier\n                                    .fillMaxWidth()\n                                    .padding(horizontal = 12.dp)\n                                    .padding(bottom = 12.dp),\n                                insideMargin = PaddingValues(16.dp),\n                                showIndication = true,\n                                onClick = { actions.onOpenRepoDetail(module) }\n                            ) {\n                                Column {\n                                    if (module.moduleName.isNotEmpty()) {\n                                        Row(verticalAlignment = Alignment.CenterVertically) {\n                                            Text(\n                                                text = module.moduleName,\n                                                fontSize = 17.sp,\n                                                fontWeight = FontWeight(550),\n                                                color = colorScheme.onSurface\n                                            )\n                                            if (module.metamodule) {\n                                                Text(\n                                                    text = \"META\",\n                                                    fontSize = 12.sp,\n                                                    color = metaTint,\n                                                    modifier = Modifier\n                                                        .padding(start = 6.dp)\n                                                        .clip(ContinuousRoundedRectangle(6.dp))\n                                                        .background(metaBg)\n                                                        .padding(horizontal = 6.dp, vertical = 2.dp),\n                                                    fontWeight = FontWeight(750),\n                                                    maxLines = 1\n                                                )\n                                            }\n                                        }\n                                    }\n                                    if (module.moduleId.isNotEmpty()) {\n                                        Text(\n                                            text = \"ID: ${module.moduleId}\",\n                                            fontSize = 12.sp,\n                                            fontWeight = FontWeight(550),\n                                            color = colorScheme.onSurfaceVariantSummary,\n                                        )\n                                    }\n                                    Text(\n                                        text = \"$moduleAuthor: ${module.authors}\",\n                                        fontSize = 12.sp,\n                                        modifier = Modifier.padding(bottom = 1.dp),\n                                        fontWeight = FontWeight(550),\n                                        color = colorScheme.onSurfaceVariantSummary,\n                                    )\n                                    if (module.summary.isNotEmpty()) {\n                                        Text(\n                                            text = module.summary,\n                                            fontSize = 14.sp,\n                                            color = colorScheme.onSurfaceVariantSummary,\n                                            modifier = Modifier.padding(top = 2.dp),\n                                            overflow = TextOverflow.Ellipsis,\n                                            maxLines = 4,\n                                        )\n                                    }\n                                    HorizontalDivider(\n                                        modifier = Modifier.padding(vertical = 8.dp),\n                                        thickness = 0.5.dp,\n                                        color = colorScheme.outline.copy(alpha = 0.5f)\n                                    )\n                                    Row(\n                                        verticalAlignment = Alignment.CenterVertically,\n                                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                                    ) {\n                                        Row {\n                                            if (module.stargazerCount > 0) {\n                                                Row(verticalAlignment = Alignment.CenterVertically) {\n                                                    Icon(\n                                                        imageVector = MiuixIcons.TopDownloads,\n                                                        contentDescription = \"stars\",\n                                                        tint = colorScheme.onSurfaceVariantSummary,\n                                                        modifier = Modifier.size(16.dp)\n                                                    )\n                                                    Text(\n                                                        text = module.stargazerCount.toString(),\n                                                        fontSize = 12.sp,\n                                                        color = colorScheme.onSurfaceVariantSummary,\n                                                        modifier = Modifier.padding(start = 4.dp)\n                                                    )\n                                                }\n                                            }\n                                            Spacer(Modifier.weight(1f))\n                                            if (module.latestReleaseTime.isNotEmpty()) {\n                                                Text(\n                                                    text = module.latestReleaseTime,\n                                                    fontSize = 12.sp,\n                                                    color = colorScheme.onSurfaceVariantSummary,\n                                                    textAlign = TextAlign.End\n                                                )\n                                            }\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                        item {\n                            Spacer(Modifier.height(WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()))\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun ReadmePage(\n    readmeHtml: String?,\n    readmeLoaded: Boolean,\n    innerPadding: PaddingValues,\n    scrollBehavior: ScrollBehavior,\n    hazeState: HazeState\n) {\n    val layoutDirection = LocalLayoutDirection.current\n    LazyColumn(\n        modifier = Modifier\n            .fillMaxHeight()\n            .scrollEndHaptic()\n            .overScrollVertical()\n            .nestedScroll(scrollBehavior.nestedScrollConnection)\n            .hazeSource(state = hazeState),\n        contentPadding = PaddingValues(\n            top = innerPadding.calculateTopPadding(),\n            start = innerPadding.calculateStartPadding(layoutDirection),\n            end = innerPadding.calculateEndPadding(layoutDirection),\n            bottom = innerPadding.calculateBottomPadding(),\n        ),\n        overscrollEffect = null,\n    ) {\n        item {\n            var isLoading by remember { mutableStateOf(true) }\n            if (isLoading) {\n                Box(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .padding(\n                            top = innerPadding.calculateTopPadding(),\n                            start = innerPadding.calculateStartPadding(layoutDirection),\n                            end = innerPadding.calculateEndPadding(layoutDirection),\n                            bottom = innerPadding.calculateBottomPadding(),\n                        ),\n                    contentAlignment = Alignment.Center\n                ) {\n                    InfiniteProgressIndicator()\n                }\n            }\n            var isReady by remember { mutableStateOf(false) }\n            LaunchedEffect(Unit) {\n                repeat(60) { withFrameNanos { } }\n                isReady = true\n            }\n            AnimatedVisibility(\n                visible = isReady && readmeLoaded && readmeHtml != null,\n                enter = expandVertically() + fadeIn(),\n                exit = shrinkVertically() + fadeOut()\n            ) {\n                Column {\n                    Spacer(Modifier.height(6.dp))\n                    Card(\n                        modifier = Modifier.padding(horizontal = 12.dp),\n                    ) {\n                        GithubMarkdown(content = readmeHtml!!, onLoadingChange = { isLoading = it })\n                    }\n                }\n            }\n        }\n        item { Spacer(Modifier.height(12.dp)) }\n    }\n}\n\n@SuppressLint(\"DefaultLocale\")\n@Composable\nfun ReleasesPage(\n    detailReleases: List<ReleaseArg>,\n    innerPadding: PaddingValues,\n    scrollBehavior: ScrollBehavior,\n    hazeState: HazeState,\n    actionIconTint: Color,\n    secondaryContainer: Color,\n    confirmTitle: String,\n    confirmDialog: ConfirmDialogHandle,\n    scope: CoroutineScope,\n    onInstallModule: (Uri) -> Unit,\n    context: Context,\n    setPendingDownload: ((() -> Unit)) -> Unit,\n) {\n    val layoutDirection = LocalLayoutDirection.current\n    LazyColumn(\n        modifier = Modifier\n            .fillMaxHeight()\n            .scrollEndHaptic()\n            .overScrollVertical()\n            .nestedScroll(scrollBehavior.nestedScrollConnection)\n            .hazeSource(state = hazeState),\n        contentPadding = PaddingValues(\n            top = innerPadding.calculateTopPadding(),\n            start = innerPadding.calculateStartPadding(layoutDirection),\n            end = innerPadding.calculateEndPadding(layoutDirection),\n            bottom = innerPadding.calculateBottomPadding(),\n        ),\n        overscrollEffect = null,\n    ) {\n        if (detailReleases.isNotEmpty()) {\n            item {\n                Spacer(Modifier.height(6.dp))\n            }\n            items(\n                items = detailReleases,\n                key = { it.tagName },\n                contentType = { \"release\" }\n            ) { rel ->\n                val title = remember(rel.name, rel.tagName) { rel.name.ifBlank { rel.tagName } }\n                Card(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 12.dp)\n                        .padding(bottom = 12.dp)\n                ) {\n                    Column {\n                        Row(\n                            verticalAlignment = Alignment.CenterVertically,\n                            modifier = Modifier.fillMaxWidth()\n                        ) {\n                            Column(\n                                modifier = Modifier\n                                    .padding(start = 16.dp, end = 16.dp, top = 16.dp)\n                                    .weight(1f)\n                            ) {\n                                Text(\n                                    text = title,\n                                    fontSize = 17.sp,\n                                    fontWeight = FontWeight(550),\n                                    color = colorScheme.onSurface\n                                )\n                                Text(\n                                    text = rel.tagName,\n                                    fontSize = 12.sp,\n                                    fontWeight = FontWeight(550),\n                                    color = colorScheme.onSurfaceVariantSummary,\n                                    modifier = Modifier.padding(top = 2.dp)\n                                )\n                            }\n                            Text(\n                                text = rel.publishedAt,\n                                fontSize = 12.sp,\n                                color = colorScheme.onSurfaceVariantSummary,\n                                modifier = Modifier\n                                    .padding(start = 16.dp, end = 16.dp, top = 16.dp)\n                                    .align(Alignment.Top)\n                            )\n                        }\n                        AnimatedVisibility(\n                            visible = rel.assets.isNotEmpty(),\n                            enter = fadeIn() + expandVertically(),\n                            exit = fadeOut() + shrinkVertically()\n                        ) {\n                            Column {\n                                AnimatedVisibility(\n                                    visible = rel.descriptionHTML.isNotEmpty(),\n                                    enter = fadeIn() + expandVertically(),\n                                    exit = fadeOut() + shrinkVertically()\n                                ) {\n                                    Column {\n                                        HorizontalDivider(\n                                            modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 4.dp),\n                                            thickness = 0.5.dp,\n                                            color = colorScheme.outline.copy(alpha = 0.5f)\n                                        )\n                                        GithubMarkdown(content = rel.descriptionHTML)\n                                    }\n                                }\n                                HorizontalDivider(\n                                    modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),\n                                    thickness = 0.5.dp,\n                                    color = colorScheme.outline.copy(alpha = 0.5f)\n                                )\n                                rel.assets.forEachIndexed { index, asset ->\n                                    val fileName = asset.name\n                                    stringResource(R.string.module_downloading)\n                                    val sizeText = remember(asset.size) {\n                                        val s = asset.size\n                                        when {\n                                            s >= 1024L * 1024L * 1024L -> String.format(\"%.1f GB\", s / (1024f * 1024f * 1024f))\n                                            s >= 1024L * 1024L -> String.format(\"%.1f MB\", s / (1024f * 1024f))\n                                            s >= 1024L -> String.format(\"%.0f KB\", s / 1024f)\n                                            else -> \"$s B\"\n                                        }\n                                    }\n                                    val sizeAndDownloads =\n                                        remember(sizeText, asset.downloadCount) { \"$sizeText · ${asset.downloadCount} downloads\" }\n                                    var isDownloading by remember(fileName, asset.downloadUrl) { mutableStateOf(false) }\n                                    var progress by remember(fileName, asset.downloadUrl) { mutableIntStateOf(0) }\n                                    val onClickDownload = remember(fileName, asset.downloadUrl) {\n                                        {\n                                            val startText = context.getString(R.string.module_start_downloading, fileName)\n                                            setPendingDownload {\n                                                isDownloading = true\n                                                scope.launch(Dispatchers.IO) {\n                                                    download(\n                                                        asset.downloadUrl,\n                                                        fileName,\n                                                        onDownloaded = onInstallModule,\n                                                        onDownloading = { isDownloading = true },\n                                                        onProgress = { p -> scope.launch(Dispatchers.Main) { progress = p } }\n                                                    )\n                                                }\n                                            }\n                                            confirmDialog.showConfirm(title = confirmTitle, content = startText)\n                                        }\n                                    }\n                                    val bottomPadding = if (index == rel.assets.lastIndex) 16.dp else 8.dp\n                                    Row(\n                                        modifier = Modifier.fillMaxWidth(),\n                                        verticalAlignment = Alignment.CenterVertically,\n                                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                                    ) {\n                                        Column(\n                                            modifier = Modifier\n                                                .padding(start = 16.dp, end = 16.dp, bottom = bottomPadding)\n                                                .weight(1f)\n                                        ) {\n                                            Text(\n                                                text = fileName,\n                                                fontSize = 14.sp,\n                                                color = colorScheme.onSurface\n                                            )\n                                            Text(\n                                                text = sizeAndDownloads,\n                                                fontSize = 12.sp,\n                                                color = colorScheme.onSurfaceVariantSummary,\n                                                modifier = Modifier.padding(top = 2.dp)\n                                            )\n                                        }\n                                        IconButton(\n                                            modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = bottomPadding),\n                                            backgroundColor = secondaryContainer,\n                                            minHeight = 35.dp,\n                                            minWidth = 35.dp,\n                                            enabled = !isDownloading,\n                                            onClick = onClickDownload,\n                                        ) {\n                                            if (isDownloading) {\n                                                CircularProgressIndicator(\n                                                    progress = progress / 100f,\n                                                    size = 20.dp,\n                                                    strokeWidth = 2.dp\n                                                )\n                                            } else {\n                                                Row(\n                                                    modifier = Modifier.padding(horizontal = 10.dp),\n                                                    verticalAlignment = Alignment.CenterVertically,\n                                                ) {\n                                                    Icon(\n                                                        modifier = Modifier.size(20.dp),\n                                                        imageVector = MiuixIcons.FileDownloads,\n                                                        tint = actionIconTint,\n                                                        contentDescription = stringResource(R.string.install)\n                                                    )\n                                                    Text(\n                                                        modifier = Modifier.padding(start = 4.dp, end = 2.dp),\n                                                        text = stringResource(R.string.install),\n                                                        color = actionIconTint,\n                                                        fontWeight = FontWeight.Medium,\n                                                        fontSize = 15.sp\n                                                    )\n                                                }\n                                            }\n                                        }\n                                    }\n                                    if (index != rel.assets.lastIndex) {\n                                        HorizontalDivider(\n                                            modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp),\n                                            thickness = 0.5.dp,\n                                            color = colorScheme.outline.copy(alpha = 0.5f)\n                                        )\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n        }\n    }\n}\n\n@Composable\nfun InfoPage(\n    module: RepoModuleArg,\n    innerPadding: PaddingValues,\n    scrollBehavior: ScrollBehavior,\n    hazeState: HazeState,\n    actionIconTint: Color,\n    secondaryContainer: Color,\n    uriHandler: UriHandler,\n    sourceUrl: String,\n) {\n    val layoutDirection = LocalLayoutDirection.current\n    LazyColumn(\n        modifier = Modifier\n            .fillMaxHeight()\n            .scrollEndHaptic()\n            .overScrollVertical()\n            .nestedScroll(scrollBehavior.nestedScrollConnection)\n            .hazeSource(state = hazeState),\n        contentPadding = PaddingValues(\n            top = innerPadding.calculateTopPadding(),\n            start = innerPadding.calculateStartPadding(layoutDirection),\n            end = innerPadding.calculateEndPadding(layoutDirection),\n            bottom = innerPadding.calculateBottomPadding(),\n        ),\n        overscrollEffect = null,\n    ) {\n        if (module.authorsList.isNotEmpty()) {\n            item {\n                SmallTitle(\n                    text = stringResource(R.string.module_author),\n                    modifier = Modifier.padding(top = 6.dp)\n                )\n                Card(\n                    modifier = Modifier\n                        .padding(horizontal = 12.dp),\n                    insideMargin = PaddingValues(16.dp)\n                ) {\n                    Column {\n                        module.authorsList.forEachIndexed { index, author ->\n                            Row(\n                                modifier = Modifier.fillMaxWidth(),\n                                verticalAlignment = Alignment.CenterVertically,\n                                horizontalArrangement = Arrangement.spacedBy(8.dp)\n                            ) {\n                                Text(\n                                    text = author.name,\n                                    fontSize = 14.sp,\n                                    color = colorScheme.onSurface,\n                                    modifier = Modifier.weight(1f)\n                                )\n                                val clickable = author.link.isNotEmpty()\n                                val tint = if (clickable) actionIconTint else actionIconTint.copy(alpha = 0.35f)\n                                IconButton(\n                                    backgroundColor = secondaryContainer,\n                                    minHeight = 35.dp,\n                                    minWidth = 35.dp,\n                                    enabled = clickable,\n                                    onClick = {\n                                        if (clickable) {\n                                            uriHandler.openUri(author.link)\n                                        }\n                                    },\n                                ) {\n                                    Icon(\n                                        modifier = Modifier.size(20.dp),\n                                        imageVector = MiuixIcons.Link,\n                                        tint = tint,\n                                        contentDescription = null\n                                    )\n                                }\n                            }\n                            if (index != module.authorsList.lastIndex) {\n                                HorizontalDivider(\n                                    modifier = Modifier.padding(vertical = 8.dp),\n                                    thickness = 0.5.dp,\n                                    color = colorScheme.outline.copy(alpha = 0.5f)\n                                )\n                            }\n                        }\n                    }\n                }\n            }\n        }\n        if (sourceUrl.isNotEmpty()) {\n            item {\n                SmallTitle(\n                    text = stringResource(R.string.module_repos_source_code),\n                    modifier = Modifier.padding(top = 6.dp)\n                )\n                Card(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(horizontal = 12.dp)\n                        .padding(bottom = 12.dp),\n                    insideMargin = PaddingValues(16.dp)\n                ) {\n                    Row(\n                        modifier = Modifier.fillMaxWidth(),\n                        verticalAlignment = Alignment.CenterVertically,\n                        horizontalArrangement = Arrangement.spacedBy(8.dp)\n                    ) {\n                        Text(\n                            text = sourceUrl,\n                            fontSize = 16.sp,\n                            color = colorScheme.onSurface,\n                            modifier = Modifier.weight(1f),\n                            maxLines = 1,\n                            overflow = TextOverflow.Ellipsis\n                        )\n                        IconButton(\n                            backgroundColor = secondaryContainer,\n                            minHeight = 35.dp,\n                            minWidth = 35.dp,\n                            onClick = {\n                                uriHandler.openUri(sourceUrl)\n                            },\n                        ) {\n                            Icon(\n                                modifier = Modifier.size(20.dp),\n                                imageVector = MiuixIcons.Link,\n                                tint = actionIconTint,\n                                contentDescription = null\n                            )\n                        }\n                    }\n                }\n            }\n        }\n        item { Spacer(Modifier.height(12.dp)) }\n    }\n}\n\n@SuppressLint(\"StringFormatInvalid\", \"DefaultLocale\")\n@Composable\nfun ModuleRepoDetailScreenMiuix(\n    state: ModuleRepoDetailUiState,\n    actions: ModuleRepoDetailActions,\n) {\n    val context = LocalContext.current\n    val enableBlur = LocalEnableBlur.current\n    val actionIconTint = colorScheme.onSurface.copy(alpha = if (isInDarkTheme()) 0.7f else 0.9f)\n    val secondaryContainer = colorScheme.secondaryContainer.copy(alpha = 0.8f)\n    val module = state.module\n    val scope = rememberCoroutineScope()\n    val confirmTitle = stringResource(R.string.module_install)\n    var pendingDownload by remember { mutableStateOf<(() -> Unit)?>(null) }\n    val confirmDialog = rememberConfirmDialog(onConfirm = { pendingDownload?.invoke() })\n\n    val scrollBehavior = MiuixScrollBehavior()\n\n    val hazeState = remember { HazeState() }\n    val hazeStyle = HazeStyle(\n        backgroundColor = colorScheme.surface,\n        tint = HazeTint(colorScheme.surface.copy(0.8f))\n    )\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                modifier = if (enableBlur) {\n                    Modifier.defaultHazeEffect(hazeState, hazeStyle)\n                } else {\n                    Modifier\n                },\n                color = if (enableBlur) Color.Transparent else colorScheme.surface,\n                title = module.moduleName,\n                scrollBehavior = scrollBehavior,\n                navigationIcon = {\n                    IconButton(\n                        modifier = Modifier.padding(start = 16.dp),\n                        onClick = actions.onBack\n                    ) {\n                        val layoutDirection = LocalLayoutDirection.current\n                        Icon(\n                            modifier = Modifier.graphicsLayer {\n                                if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\n                            },\n                            imageVector = MiuixIcons.Back,\n                            contentDescription = null,\n                            tint = colorScheme.onSurface\n                        )\n                    }\n                },\n                actions = {\n                    if (state.webUrl.isNotEmpty()) {\n                        IconButton(\n                            modifier = Modifier.padding(end = 16.dp),\n                            onClick = actions.onOpenWebUrl\n                        ) {\n                            Icon(\n                                imageVector = MiuixIcons.HorizontalSplit,\n                                contentDescription = null,\n                                tint = colorScheme.onBackground\n                            )\n                        }\n                    }\n                }\n            )\n        },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal),\n    ) { innerPadding ->\n        val tabs = listOf(\n            stringResource(R.string.tab_readme),\n            stringResource(R.string.tab_releases),\n            stringResource(R.string.tab_info)\n        )\n        val pagerState = rememberPagerState(initialPage = 0, pageCount = { tabs.size })\n        val tabRowHeight by remember { mutableStateOf(40.dp) }\n        var collapsedFraction by remember { mutableFloatStateOf(scrollBehavior.state.collapsedFraction) }\n        LaunchedEffect(scrollBehavior.state.collapsedFraction) {\n            snapshotFlow { scrollBehavior.state.collapsedFraction }.collectLatest { collapsedFraction = it }\n        }\n        val dynamicTopPadding by remember { derivedStateOf { 12.dp * (1f - collapsedFraction) } }\n        val layoutDirection = LocalLayoutDirection.current\n        val coroutineScope = rememberCoroutineScope()\n        Box(\n            modifier = Modifier.fillMaxSize()\n        ) {\n            Column(\n                modifier = Modifier\n                    .then(\n                        if (enableBlur) {\n                            Modifier.defaultHazeEffect(hazeState, hazeStyle)\n                        } else Modifier.background(colorScheme.surface),\n                    )\n                    .zIndex(1f)\n                    .padding(\n                        top = innerPadding.calculateTopPadding() + dynamicTopPadding,\n                        start = innerPadding.calculateStartPadding(layoutDirection),\n                        end = innerPadding.calculateEndPadding(layoutDirection),\n                        bottom = 6.dp\n                    )\n                    .padding(horizontal = 12.dp)\n            ) {\n                TabRow(\n                    tabs = tabs,\n                    selectedTabIndex = pagerState.currentPage,\n                    onTabSelected = { index ->\n                        coroutineScope.launch {\n                            pagerState.animateScrollToPage(page = index, animationSpec = tween(easing = EaseInOut))\n                        }\n                    },\n                    colors = TabRowDefaults.tabRowColors(\n                        backgroundColor = if (enableBlur) Color.Transparent else colorScheme.surface\n                    ),\n                    height = tabRowHeight,\n                )\n            }\n            HorizontalPager(\n                state = pagerState,\n                modifier = Modifier.fillMaxSize(),\n            ) { page ->\n                val innerPadding = PaddingValues(\n                    top = innerPadding.calculateTopPadding() + tabRowHeight + dynamicTopPadding + 6.dp,\n                    start = innerPadding.calculateStartPadding(layoutDirection),\n                    end = innerPadding.calculateEndPadding(layoutDirection),\n                    bottom = innerPadding.calculateBottomPadding() + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding()\n                )\n\n                when (page) {\n                    0 -> ReadmePage(\n                        readmeHtml = state.readmeHtml,\n                        readmeLoaded = state.readmeLoaded,\n                        innerPadding = innerPadding,\n                        scrollBehavior = scrollBehavior,\n                        hazeState = hazeState\n                    )\n\n                    1 -> ReleasesPage(\n                        detailReleases = state.detailReleases,\n                        innerPadding = innerPadding,\n                        scrollBehavior = scrollBehavior,\n                        hazeState = hazeState,\n                        actionIconTint = actionIconTint,\n                        secondaryContainer = secondaryContainer,\n                        confirmTitle = confirmTitle,\n                        confirmDialog = confirmDialog,\n                        scope = scope,\n                        onInstallModule = actions.onInstallModule,\n                        context = context,\n                        setPendingDownload = { pendingDownload = it }\n                    )\n\n                    2 -> InfoPage(\n                        module = module,\n                        innerPadding = innerPadding,\n                        scrollBehavior = scrollBehavior,\n                        hazeState = hazeState,\n                        actionIconTint = actionIconTint,\n                        secondaryContainer = secondaryContainer,\n                        uriHandler = object : UriHandler {\n                            override fun openUri(uri: String) = actions.onOpenUrl(uri)\n                        },\n                        sourceUrl = state.sourceUrl,\n                    )\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/modulerepo/ModuleRepoModels.kt",
    "content": "package me.weishu.kernelsu.ui.screen.modulerepo\n\nimport android.os.Parcelable\nimport kotlinx.parcelize.Parcelize\n\n@Parcelize\ndata class ReleaseAssetArg(\n    val name: String,\n    val downloadUrl: String,\n    val size: Long,\n    val downloadCount: Int\n) : Parcelable\n\n@Parcelize\ndata class ReleaseArg(\n    val tagName: String,\n    val name: String,\n    val publishedAt: String,\n    val assets: List<ReleaseAssetArg>,\n    val descriptionHTML: String\n) : Parcelable\n\n@Parcelize\ndata class AuthorArg(\n    val name: String,\n    val link: String,\n) : Parcelable\n\n@Parcelize\ndata class RepoModuleArg(\n    val moduleId: String,\n    val moduleName: String,\n    val authors: String,\n    val authorsList: List<AuthorArg>,\n    val latestRelease: String,\n    val latestReleaseTime: String,\n    val releases: List<ReleaseArg>\n) : Parcelable\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/modulerepo/ModuleRepoScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.modulerepo\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalUriHandler\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.screen.flash.FlashIt\nimport me.weishu.kernelsu.ui.util.module.fetchModuleDetail\nimport me.weishu.kernelsu.ui.viewmodel.ModuleRepoViewModel\nimport me.weishu.kernelsu.ui.viewmodel.ModuleViewModel\n\n@Composable\nfun ModuleRepoScreen() {\n    val navigator = LocalNavigator.current\n    val viewModel = viewModel<ModuleRepoViewModel>()\n    val uiState by viewModel.uiState.collectAsState()\n    val installedVm = viewModel<ModuleViewModel>()\n    val installedUiState by installedVm.uiState.collectAsState()\n\n    LaunchedEffect(Unit) {\n        if (uiState.modules.isEmpty()) {\n            viewModel.refresh()\n        }\n        if (installedUiState.moduleList.isEmpty()) {\n            installedVm.fetchModuleList()\n        }\n    }\n\n    val actions = ModuleRepoActions(\n        onBack = { navigator.pop() },\n        onRefresh = viewModel::refresh,\n        onSearchTextChange = viewModel::updateSearchText,\n        onClearSearch = { viewModel.updateSearchText(\"\") },\n        onSearchStatusChange = viewModel::updateSearchStatus,\n        onToggleSortByName = viewModel::toggleSortByName,\n        onOpenRepoDetail = { module ->\n            val args = RepoModuleArg(\n                moduleId = module.moduleId,\n                moduleName = module.moduleName,\n                authors = module.authors,\n                authorsList = module.authorList.map { AuthorArg(it.name, it.link) },\n                latestRelease = module.latestRelease,\n                latestReleaseTime = module.latestReleaseTime,\n                releases = emptyList()\n            )\n            navigator.push(Route.ModuleRepoDetail(args))\n        },\n    )\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> ModuleRepoScreenMiuix(uiState, actions)\n        UiMode.Material -> ModuleRepoScreenMaterial(uiState, actions)\n    }\n}\n\n@Composable\nfun ModuleRepoDetailScreen(module: RepoModuleArg) {\n    val navigator = LocalNavigator.current\n    val uriHandler = LocalUriHandler.current\n    var readmeHtml by remember(module.moduleId) { mutableStateOf<String?>(null) }\n    var readmeLoaded by remember(module.moduleId) { mutableStateOf(false) }\n    var detailReleases by remember(module.moduleId) { mutableStateOf<List<ReleaseArg>>(emptyList()) }\n    var webUrl by remember(module.moduleId) { mutableStateOf(\"https://modules.kernelsu.org/module/${module.moduleId}\") }\n    var sourceUrl by remember(module.moduleId) { mutableStateOf(\"https://github.com/KernelSU-Modules-Repo/${module.moduleId}\") }\n\n    LaunchedEffect(module.moduleId) {\n        if (module.moduleId.isNotEmpty()) {\n            withContext(Dispatchers.IO) {\n                runCatching {\n                    val detail = fetchModuleDetail(module.moduleId)\n                    if (detail != null) {\n                        readmeHtml = detail.readmeHtml\n                        if (detail.sourceUrl.isNotEmpty()) sourceUrl = detail.sourceUrl\n                        detailReleases = detail.releases.map { r ->\n                            ReleaseArg(\n                                tagName = r.tagName,\n                                name = r.name,\n                                publishedAt = r.publishedAt,\n                                assets = r.assets.map { a -> ReleaseAssetArg(a.name, a.downloadUrl, a.size, a.downloadCount) },\n                                descriptionHTML = r.descriptionHTML\n                            )\n                        }\n                    } else {\n                        detailReleases = emptyList()\n                    }\n                }.onSuccess {\n                    readmeLoaded = true\n                }.onFailure {\n                    readmeLoaded = true\n                    detailReleases = emptyList()\n                }\n            }\n        } else {\n            readmeLoaded = true\n        }\n    }\n\n    val state = ModuleRepoDetailUiState(\n        module = module,\n        readmeHtml = readmeHtml,\n        readmeLoaded = readmeLoaded,\n        detailReleases = detailReleases,\n        webUrl = webUrl,\n        sourceUrl = sourceUrl,\n    )\n    val actions = ModuleRepoDetailActions(\n        onBack = { navigator.pop() },\n        onOpenWebUrl = { if (webUrl.isNotEmpty()) uriHandler.openUri(webUrl) },\n        onOpenUrl = uriHandler::openUri,\n        onInstallModule = { uri -> navigator.push(Route.Flash(FlashIt.FlashModules(listOf(uri)))) },\n    )\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> ModuleRepoDetailScreenMiuix(state, actions)\n        UiMode.Material -> ModuleRepoDetailScreenMaterial(state, actions)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/modulerepo/ModuleRepoUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.modulerepo\n\nimport androidx.compose.runtime.Immutable\nimport me.weishu.kernelsu.data.model.RepoModule\nimport me.weishu.kernelsu.ui.component.SearchStatus\n\ndata class ModuleRepoUiState(\n    val isRefreshing: Boolean = false,\n    val sortByName: Boolean = false,\n    val offline: Boolean = false,\n    val modules: List<RepoModule> = emptyList(),\n    val searchStatus: SearchStatus = SearchStatus(\"\"),\n    val searchResults: List<RepoModule> = emptyList(),\n    val error: Throwable? = null\n)\n\n@Immutable\ndata class ModuleRepoActions(\n    val onBack: () -> Unit,\n    val onRefresh: () -> Unit,\n    val onSearchTextChange: (String) -> Unit,\n    val onClearSearch: () -> Unit,\n    val onSearchStatusChange: (SearchStatus) -> Unit,\n    val onToggleSortByName: () -> Unit,\n    val onOpenRepoDetail: (RepoModule) -> Unit,\n)\n\n@Immutable\ndata class ModuleRepoDetailUiState(\n    val module: RepoModuleArg,\n    val readmeHtml: String?,\n    val readmeLoaded: Boolean,\n    val detailReleases: List<ReleaseArg>,\n    val webUrl: String,\n    val sourceUrl: String,\n)\n\n@Immutable\ndata class ModuleRepoDetailActions(\n    val onBack: () -> Unit,\n    val onOpenWebUrl: () -> Unit,\n    val onOpenUrl: (String) -> Unit,\n    val onInstallModule: (android.net.Uri) -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/settings/SettingsMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.settings\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight\nimport androidx.compose.material.icons.filled.BugReport\nimport androidx.compose.material.icons.filled.ContactPage\nimport androidx.compose.material.icons.filled.Delete\nimport androidx.compose.material.icons.filled.DeveloperMode\nimport androidx.compose.material.icons.filled.ElectricalServices\nimport androidx.compose.material.icons.filled.Fence\nimport androidx.compose.material.icons.filled.FolderDelete\nimport androidx.compose.material.icons.filled.Palette\nimport androidx.compose.material.icons.filled.RemoveCircle\nimport androidx.compose.material.icons.filled.RemoveModerator\nimport androidx.compose.material.icons.filled.Update\nimport androidx.compose.material.icons.rounded.Dashboard\nimport androidx.compose.material.icons.rounded.UploadFile\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.LargeFlexibleTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.SnackbarHost\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.component.KsuIsValid\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedDropdownItem\nimport me.weishu.kernelsu.ui.component.material.SegmentedListItem\nimport me.weishu.kernelsu.ui.component.material.SegmentedSwitchItem\nimport me.weishu.kernelsu.ui.component.material.SendLogBottomSheet\nimport me.weishu.kernelsu.ui.component.uninstalldialog.UninstallDialog\nimport me.weishu.kernelsu.ui.util.LocalSnackbarHost\n\n/**\n * @author weishu\n * @date 2023/1/1.\n */\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SettingPagerMaterial(\n    uiState: SettingsUiState,\n    actions: SettingsScreenActions,\n    bottomInnerPadding: Dp,\n) {\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n    val snackBarHost = LocalSnackbarHost.current\n    val showUninstallDialog = rememberSaveable { mutableStateOf(false) }\n    var showBottomsheet by remember { mutableStateOf(false) }\n\n    UninstallDialog(\n        show = showUninstallDialog.value,\n        onDismissRequest = { showUninstallDialog.value = false }\n    )\n\n    Scaffold(\n        topBar = {\n            TopBar(\n                scrollBehavior = scrollBehavior\n            )\n        },\n        snackbarHost = { SnackbarHost(snackBarHost) },\n        contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)\n    ) { paddingValues ->\n        Column(\n            modifier = Modifier\n                .padding(paddingValues)\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .verticalScroll(rememberScrollState())\n        ) {\n            Spacer(modifier = Modifier.height(8.dp))\n            KsuIsValid {\n                SegmentedColumn(\n                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                    content = listOf(\n                        {\n                            SegmentedSwitchItem(\n                                icon = Icons.Filled.Update,\n                                title = stringResource(id = R.string.settings_check_update),\n                                summary = stringResource(id = R.string.settings_check_update_summary),\n                                checked = uiState.checkUpdate,\n                                onCheckedChange = actions.onSetCheckUpdate\n                            )\n                        },\n                        {\n                            SegmentedSwitchItem(\n                                icon = Icons.Rounded.UploadFile,\n                                title = stringResource(id = R.string.settings_module_check_update),\n                                summary = stringResource(id = R.string.settings_check_update_summary),\n                                checked = uiState.checkModuleUpdate,\n                                onCheckedChange = actions.onSetCheckModuleUpdate\n                            )\n                        }\n                    )\n                )\n            }\n\n            SegmentedColumn(\n                modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                content = buildList {\n                    add {\n                        SegmentedDropdownItem(\n                            icon = Icons.Rounded.Dashboard,\n                            title = stringResource(id = R.string.settings_ui_mode),\n                            summary = stringResource(id = R.string.settings_ui_mode_summary),\n                            items = UiMode.entries.map { it.name },\n                            selectedIndex = if (uiState.uiMode == UiMode.Material.value) 1 else 0,\n                            onItemSelected = actions.onSetUiModeIndex\n                        )\n                    }\n                    add {\n                        SegmentedListItem(\n                            onClick = actions.onOpenTheme,\n                            headlineContent = { Text(stringResource(id = R.string.settings_theme)) },\n                            supportingContent = { Text(stringResource(id = R.string.settings_theme_summary)) },\n                            leadingContent = { Icon(Icons.Filled.Palette, stringResource(id = R.string.settings_theme)) },\n                            trailingContent = {\n                                Icon(\n                                    Icons.AutoMirrored.Filled.KeyboardArrowRight,\n                                    null\n                                )\n                            }\n                        )\n                    }\n                }\n            )\n\n            val profileTemplate = stringResource(id = R.string.settings_profile_template)\n            KsuIsValid {\n                SegmentedColumn(\n                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                    content = listOf {\n                        SegmentedListItem(\n                            onClick = actions.onOpenProfileTemplate,\n                            headlineContent = { Text(profileTemplate) },\n                            supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) },\n                            leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) },\n                            trailingContent = {\n                                Icon(\n                                    Icons.AutoMirrored.Filled.KeyboardArrowRight,\n                                    null\n                                )\n                            }\n                        )\n                    }\n                )\n            }\n\n            KsuIsValid {\n                val suCompatModeItems = listOf(\n                    stringResource(id = R.string.settings_mode_enable_by_default),\n                    stringResource(id = R.string.settings_mode_disable_until_reboot),\n                    stringResource(id = R.string.settings_mode_disable_always),\n                )\n\n                SegmentedColumn(\n                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                    content = listOf(\n                        {\n                            val suSummary = when (uiState.suCompatStatus) {\n                                \"unsupported\" -> stringResource(id = R.string.feature_status_unsupported_summary)\n                                \"managed\" -> stringResource(id = R.string.feature_status_managed_summary)\n                                else -> stringResource(id = R.string.settings_sucompat_summary)\n                            }\n                            SegmentedDropdownItem(\n                                icon = Icons.Filled.RemoveModerator,\n                                title = stringResource(id = R.string.settings_sucompat),\n                                summary = suSummary,\n                                items = suCompatModeItems,\n                                enabled = uiState.suCompatStatus == \"supported\",\n                                selectedIndex = uiState.suCompatMode,\n                                onItemSelected = actions.onSetSuCompatMode\n                            )\n                        },\n                        {\n                            val umountSummary = when (uiState.kernelUmountStatus) {\n                                \"unsupported\" -> stringResource(id = R.string.feature_status_unsupported_summary)\n                                \"managed\" -> stringResource(id = R.string.feature_status_managed_summary)\n                                else -> stringResource(id = R.string.settings_kernel_umount_summary)\n                            }\n                            SegmentedSwitchItem(\n                                icon = Icons.Filled.RemoveCircle,\n                                title = stringResource(id = R.string.settings_kernel_umount),\n                                summary = umountSummary,\n                                enabled = uiState.kernelUmountStatus == \"supported\",\n                                checked = uiState.isKernelUmountEnabled,\n                                onCheckedChange = actions.onSetKernelUmountEnabled\n                            )\n                        },\n                        {\n                            SegmentedSwitchItem(\n                                icon = Icons.Filled.FolderDelete,\n                                title = stringResource(id = R.string.settings_umount_modules_default),\n                                summary = stringResource(id = R.string.settings_umount_modules_default_summary),\n                                checked = uiState.isDefaultUmountModules,\n                                onCheckedChange = actions.onSetDefaultUmountModules\n                            )\n                        },\n                        {\n                            SegmentedSwitchItem(\n                                icon = Icons.Filled.DeveloperMode,\n                                title = stringResource(id = R.string.enable_web_debugging),\n                                summary = stringResource(id = R.string.enable_web_debugging_summary),\n                                checked = uiState.enableWebDebugging,\n                                onCheckedChange = actions.onSetEnableWebDebugging\n                            )\n                        },\n                        {\n                            SegmentedSwitchItem(\n                                icon = Icons.Filled.ElectricalServices,\n                                title = stringResource(id = R.string.settings_auto_jailbreak),\n                                summary = stringResource(id = R.string.settings_auto_jailbreak_summary),\n                                enabled = uiState.isLateLoadMode,\n                                checked = uiState.autoJailbreak,\n                                onCheckedChange = actions.onSetAutoJailbreak\n                            )\n                        }\n                    )\n                )\n            }\n\n            if (uiState.isLkmMode) {\n                SegmentedColumn(\n                    modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                    content = listOf(\n                        {\n                            val uninstall = stringResource(id = R.string.settings_uninstall)\n                            SegmentedListItem(\n                                onClick = { showUninstallDialog.value = true },\n                                enabled = !uiState.isLateLoadMode,\n                                headlineContent = { Text(uninstall) },\n                                leadingContent = { Icon(Icons.Filled.Delete, uninstall) }\n                            )\n                        }\n                    )\n                )\n            }\n\n            SegmentedColumn(\n                modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                content = listOf(\n                    {\n                        SegmentedListItem(\n                            onClick = { showBottomsheet = true },\n                            headlineContent = { Text(stringResource(id = R.string.send_log)) },\n                            leadingContent = {\n                                Icon(\n                                    Icons.Filled.BugReport,\n                                    stringResource(id = R.string.send_log)\n                                )\n                            },\n                        )\n                    },\n                    {\n                        SegmentedListItem(\n                            onClick = actions.onOpenAbout,\n                            headlineContent = { Text(stringResource(id = R.string.about)) },\n                            leadingContent = {\n                                Icon(\n                                    Icons.Filled.ContactPage,\n                                    stringResource(id = R.string.about)\n                                )\n                            },\n                        )\n                    }\n                )\n            )\n            Spacer(modifier = Modifier.height(8.dp))\n\n            if (showBottomsheet) {\n                SendLogBottomSheet { showBottomsheet = false }\n            }\n            Spacer(modifier = Modifier.height(bottomInnerPadding))\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun TopBar(\n    scrollBehavior: TopAppBarScrollBehavior? = null\n) {\n    LargeFlexibleTopAppBar(\n        title = { Text(stringResource(R.string.settings)) },\n        colors = TopAppBarDefaults.topAppBarColors(\n            containerColor = MaterialTheme.colorScheme.surface,\n            scrolledContainerColor = MaterialTheme.colorScheme.surface\n        ),\n        windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),\n        scrollBehavior = scrollBehavior\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/settings/SettingsMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.settings\n\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.rounded.BugReport\nimport androidx.compose.material.icons.rounded.ContactPage\nimport androidx.compose.material.icons.rounded.Dashboard\nimport androidx.compose.material.icons.rounded.Delete\nimport androidx.compose.material.icons.rounded.DeveloperMode\nimport androidx.compose.material.icons.rounded.ElectricalServices\nimport androidx.compose.material.icons.rounded.Fence\nimport androidx.compose.material.icons.rounded.FolderDelete\nimport androidx.compose.material.icons.rounded.Palette\nimport androidx.compose.material.icons.rounded.RemoveCircle\nimport androidx.compose.material.icons.rounded.RemoveModerator\nimport androidx.compose.material.icons.rounded.Update\nimport androidx.compose.material.icons.rounded.UploadFile\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.component.KsuIsValid\nimport me.weishu.kernelsu.ui.component.dialog.rememberLoadingDialog\nimport me.weishu.kernelsu.ui.component.miuix.SendLogDialog\nimport me.weishu.kernelsu.ui.component.uninstalldialog.UninstallDialog\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport top.yukonga.miuix.kmp.basic.Card\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.TopAppBar\nimport top.yukonga.miuix.kmp.extra.SuperArrow\nimport top.yukonga.miuix.kmp.extra.SuperDropdown\nimport top.yukonga.miuix.kmp.extra.SuperSwitch\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\n\n/**\n * @author weishu\n * @date 2023/1/1.\n */\n@Composable\nfun SettingPagerMiuix(\n    uiState: SettingsUiState,\n    actions: SettingsScreenActions,\n    bottomInnerPadding: Dp\n) {\n    val scrollBehavior = MiuixScrollBehavior()\n    val enableBlur = LocalEnableBlur.current\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n    val loadingDialog = rememberLoadingDialog()\n    val showUninstallDialog = rememberSaveable { mutableStateOf(false) }\n    val showSendLogDialog = rememberSaveable { mutableStateOf(false) }\n\n    Scaffold(\n        topBar = {\n            TopAppBar(\n                modifier = if (enableBlur) {\n                    Modifier.defaultHazeEffect(hazeState, hazeStyle)\n                } else {\n                    Modifier\n                },\n                color = if (enableBlur) Color.Transparent else colorScheme.surface,\n                title = stringResource(R.string.settings),\n                scrollBehavior = scrollBehavior\n            )\n        },\n        popupHost = { },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxHeight()\n                .scrollEndHaptic()\n                .overScrollVertical()\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .let { if (enableBlur) it.hazeSource(state = hazeState) else it }\n                .padding(horizontal = 12.dp),\n            contentPadding = innerPadding,\n            overscrollEffect = null,\n        ) {\n            item {\n                Card(\n                    modifier = Modifier\n                        .padding(top = 12.dp)\n                        .fillMaxWidth(),\n                ) {\n                    SuperSwitch(\n                        title = stringResource(id = R.string.settings_check_update),\n                        summary = stringResource(id = R.string.settings_check_update_summary),\n                        startAction = {\n                            Icon(\n                                Icons.Rounded.Update,\n                                modifier = Modifier.padding(end = 6.dp),\n                                contentDescription = stringResource(id = R.string.settings_check_update),\n                                tint = colorScheme.onBackground\n                            )\n                        },\n                        checked = uiState.checkUpdate,\n                        onCheckedChange = actions.onSetCheckUpdate\n                    )\n                    KsuIsValid {\n                        SuperSwitch(\n                            title = stringResource(id = R.string.settings_module_check_update),\n                            summary = stringResource(id = R.string.settings_check_update_summary),\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.UploadFile,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = stringResource(id = R.string.settings_check_update),\n                                    tint = colorScheme.onBackground\n                                )\n                            },\n                            checked = uiState.checkModuleUpdate,\n                            onCheckedChange = actions.onSetCheckModuleUpdate\n                        )\n                    }\n                }\n\n                Card(\n                    modifier = Modifier\n                        .padding(top = 12.dp)\n                        .fillMaxWidth(),\n                ) {\n                    SuperDropdown(\n                        title = stringResource(id = R.string.settings_ui_mode),\n                        summary = stringResource(id = R.string.settings_ui_mode_summary),\n                        items = UiMode.entries.map { it.name },\n                        startAction = {\n                            Icon(\n                                Icons.Rounded.Dashboard,\n                                modifier = Modifier.padding(end = 6.dp),\n                                contentDescription = stringResource(id = R.string.settings_ui_mode),\n                                tint = colorScheme.onBackground\n                            )\n                        },\n                        selectedIndex = if (uiState.uiMode == UiMode.Material.value) 1 else 0,\n                        onSelectedIndexChange = actions.onSetUiModeIndex\n                    )\n                    SuperArrow(\n                        title = stringResource(id = R.string.settings_theme),\n                        summary = stringResource(id = R.string.settings_theme_summary),\n                        startAction = {\n                            Icon(\n                                Icons.Rounded.Palette,\n                                modifier = Modifier.padding(end = 6.dp),\n                                contentDescription = stringResource(id = R.string.settings_theme),\n                                tint = colorScheme.onBackground\n                            )\n                        },\n                        onClick = actions.onOpenTheme\n                    )\n                }\n\n                KsuIsValid {\n                    Card(\n                        modifier = Modifier\n                            .padding(top = 12.dp)\n                            .fillMaxWidth(),\n                    ) {\n                        val profileTemplate = stringResource(id = R.string.settings_profile_template)\n                        SuperArrow(\n                            title = profileTemplate,\n                            summary = stringResource(id = R.string.settings_profile_template_summary),\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.Fence,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = profileTemplate,\n                                    tint = colorScheme.onBackground\n                                )\n                            },\n                            onClick = actions.onOpenProfileTemplate\n                        )\n                    }\n                }\n\n                KsuIsValid {\n                    Card(\n                        modifier = Modifier\n                            .padding(top = 12.dp)\n                            .fillMaxWidth(),\n                    ) {\n                        val suCompatModeItems = listOf(\n                            stringResource(id = R.string.settings_mode_enable_by_default),\n                            stringResource(id = R.string.settings_mode_disable_until_reboot),\n                            stringResource(id = R.string.settings_mode_disable_always),\n                        )\n\n                        val suSummary = when (uiState.suCompatStatus) {\n                            \"unsupported\" -> stringResource(id = R.string.feature_status_unsupported_summary)\n                            \"managed\" -> stringResource(id = R.string.feature_status_managed_summary)\n                            else -> stringResource(id = R.string.settings_sucompat_summary)\n                        }\n                        SuperDropdown(\n                            title = stringResource(id = R.string.settings_sucompat),\n                            summary = suSummary,\n                            items = suCompatModeItems,\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.RemoveModerator,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = stringResource(id = R.string.settings_sucompat),\n                                    tint = colorScheme.onBackground\n                                )\n                            },\n                            enabled = uiState.suCompatStatus == \"supported\",\n                            selectedIndex = uiState.suCompatMode,\n                            onSelectedIndexChange = actions.onSetSuCompatMode\n                        )\n\n                        val umountSummary = when (uiState.kernelUmountStatus) {\n                            \"unsupported\" -> stringResource(id = R.string.feature_status_unsupported_summary)\n                            \"managed\" -> stringResource(id = R.string.feature_status_managed_summary)\n                            else -> stringResource(id = R.string.settings_kernel_umount_summary)\n                        }\n                        SuperSwitch(\n                            title = stringResource(id = R.string.settings_kernel_umount),\n                            summary = umountSummary,\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.RemoveCircle,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = stringResource(id = R.string.settings_kernel_umount),\n                                    tint = colorScheme.onBackground\n                                )\n                            },\n                            enabled = uiState.kernelUmountStatus == \"supported\",\n                            checked = uiState.isKernelUmountEnabled,\n                            onCheckedChange = actions.onSetKernelUmountEnabled\n                        )\n\n                        SuperSwitch(\n                            title = stringResource(id = R.string.settings_umount_modules_default),\n                            summary = stringResource(id = R.string.settings_umount_modules_default_summary),\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.FolderDelete,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = stringResource(id = R.string.settings_umount_modules_default),\n                                    tint = colorScheme.onBackground\n                                )\n                            },\n                            checked = uiState.isDefaultUmountModules,\n                            onCheckedChange = actions.onSetDefaultUmountModules\n                        )\n\n                        SuperSwitch(\n                            title = stringResource(id = R.string.enable_web_debugging),\n                            summary = stringResource(id = R.string.enable_web_debugging_summary),\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.DeveloperMode,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = stringResource(id = R.string.enable_web_debugging),\n                                    tint = colorScheme.onBackground\n                                )\n                            },\n                            checked = uiState.enableWebDebugging,\n                            onCheckedChange = actions.onSetEnableWebDebugging\n                        )\n                        SuperSwitch(\n                            title = stringResource(id = R.string.settings_auto_jailbreak),\n                            summary = stringResource(id = R.string.settings_auto_jailbreak_summary),\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.ElectricalServices,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = stringResource(id = R.string.settings_auto_jailbreak),\n                                    tint = colorScheme.onBackground\n                                )\n                            },\n                            enabled = uiState.isLateLoadMode,\n                            checked = uiState.autoJailbreak,\n                            onCheckedChange = actions.onSetAutoJailbreak\n                        )\n                    }\n                }\n\n                if (uiState.isLkmMode) {\n                    Card(\n                        modifier = Modifier\n                            .padding(top = 12.dp)\n                            .fillMaxWidth(),\n                    ) {\n                        val uninstall = stringResource(id = R.string.settings_uninstall)\n                        SuperArrow(\n                            title = uninstall,\n                            enabled = !uiState.isLateLoadMode,\n                            startAction = {\n                                Icon(\n                                    Icons.Rounded.Delete,\n                                    modifier = Modifier.padding(end = 6.dp),\n                                    contentDescription = uninstall,\n                                    tint = colorScheme.onBackground,\n                                )\n                            },\n                            onClick = { showUninstallDialog.value = true }\n                        )\n                        UninstallDialog(\n                            show = showUninstallDialog.value,\n                            onDismissRequest = { showUninstallDialog.value = false }\n                        )\n                    }\n                }\n\n                Card(\n                    modifier = Modifier\n                        .padding(vertical = 12.dp)\n                        .fillMaxWidth(),\n                ) {\n                    SuperArrow(\n                        title = stringResource(id = R.string.send_log),\n                        startAction = {\n                            Icon(\n                                Icons.Rounded.BugReport,\n                                modifier = Modifier.padding(end = 6.dp),\n                                contentDescription = stringResource(id = R.string.send_log),\n                                tint = colorScheme.onBackground\n                            )\n                        },\n                        onClick = { showSendLogDialog.value = true },\n                    )\n                    SendLogDialog(\n                        show = showSendLogDialog.value,\n                        onDismissRequest = { showSendLogDialog.value = false },\n                        loadingDialog = loadingDialog\n                    )\n                    val about = stringResource(id = R.string.about)\n                    SuperArrow(\n                        title = about,\n                        startAction = {\n                            Icon(\n                                Icons.Rounded.ContactPage,\n                                modifier = Modifier.padding(end = 6.dp),\n                                contentDescription = about,\n                                tint = colorScheme.onBackground\n                            )\n                        },\n                        onClick = actions.onOpenAbout\n                    )\n                }\n                Spacer(Modifier.height(bottomInnerPadding))\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/settings/SettingsScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.settings\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.unit.Dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.Navigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.viewmodel.SettingsViewModel\n\n@Composable\nfun SettingPager(\n    navigator: Navigator,\n    bottomInnerPadding: Dp\n) {\n    val viewModel = viewModel<SettingsViewModel>()\n    val uiState by viewModel.uiState.collectAsState()\n    val actions = SettingsScreenActions(\n        onSetCheckUpdate = viewModel::setCheckUpdate,\n        onSetCheckModuleUpdate = viewModel::setCheckModuleUpdate,\n        onOpenTheme = { navigator.push(Route.ColorPalette) },\n        onSetUiModeIndex = { index ->\n            viewModel.setUiMode(if (index == 0) UiMode.Miuix.value else UiMode.Material.value)\n        },\n        onOpenProfileTemplate = { navigator.push(Route.AppProfileTemplate) },\n        onSetSuCompatMode = viewModel::setSuCompatMode,\n        onSetKernelUmountEnabled = viewModel::setKernelUmountEnabled,\n        onSetDefaultUmountModules = viewModel::setDefaultUmountModules,\n        onSetEnableWebDebugging = viewModel::setEnableWebDebugging,\n        onSetAutoJailbreak = viewModel::setAutoJailbreak,\n        onOpenAbout = { navigator.push(Route.About) },\n    )\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> SettingPagerMiuix(uiState, actions, bottomInnerPadding)\n        UiMode.Material -> SettingPagerMaterial(uiState, actions, bottomInnerPadding)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/settings/SettingsUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.settings\n\nimport androidx.compose.runtime.Immutable\nimport com.materialkolor.PaletteStyle\nimport com.materialkolor.dynamiccolor.ColorSpec\nimport me.weishu.kernelsu.ui.UiMode\n\ndata class SettingsUiState(\n    val uiMode: String = UiMode.DEFAULT_VALUE,\n    val checkUpdate: Boolean = true,\n    val checkModuleUpdate: Boolean = true,\n    val themeMode: Int = 0,\n    val miuixMonet: Boolean = false,\n    val keyColor: Int = 0,\n    val colorStyle: String = PaletteStyle.TonalSpot.name,\n    val colorSpec: String = ColorSpec.SpecVersion.Default.name,\n    val enablePredictiveBack: Boolean = false,\n    val enableBlur: Boolean = true,\n    val enableFloatingBottomBar: Boolean = false,\n    val enableFloatingBottomBarBlur: Boolean = false,\n    val pageScale: Float = 1.0f,\n    val enableWebDebugging: Boolean = false,\n\n    // Su Compat\n    val suCompatStatus: String = \"\",\n    val suCompatMode: Int = 0, // 0: enable default, 1: disable until reboot, 2: disable always\n    val isSuEnabled: Boolean = false,\n\n    // Kernel Umount\n    val kernelUmountStatus: String = \"\",\n    val isKernelUmountEnabled: Boolean = false,\n\n    // Umount Modules\n    val isDefaultUmountModules: Boolean = false,\n\n    val isLkmMode: Boolean = false,\n    val isLateLoadMode: Boolean = false,\n\n    // Auto Jailbreak\n    val autoJailbreak: Boolean = false\n)\n\n@Immutable\ndata class SettingsScreenActions(\n    val onSetCheckUpdate: (Boolean) -> Unit,\n    val onSetCheckModuleUpdate: (Boolean) -> Unit,\n    val onOpenTheme: () -> Unit,\n    val onSetUiModeIndex: (Int) -> Unit,\n    val onOpenProfileTemplate: () -> Unit,\n    val onSetSuCompatMode: (Int) -> Unit,\n    val onSetKernelUmountEnabled: (Boolean) -> Unit,\n    val onSetDefaultUmountModules: (Boolean) -> Unit,\n    val onSetEnableWebDebugging: (Boolean) -> Unit,\n    val onSetAutoJailbreak: (Boolean) -> Unit,\n    val onOpenAbout: () -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/superuser/SuperUserMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.superuser\n\nimport android.content.pm.ApplicationInfo\nimport androidx.compose.animation.AnimatedVisibility\nimport androidx.compose.animation.core.LinearOutSlowInEasing\nimport androidx.compose.animation.expandVertically\nimport androidx.compose.animation.fadeIn\nimport androidx.compose.animation.fadeOut\nimport androidx.compose.animation.shrinkVertically\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.filled.MoreVert\nimport androidx.compose.material.icons.filled.Remove\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.ListItem\nimport androidx.compose.material3.ListItemDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.pulltorefresh.PullToRefreshDefaults\nimport androidx.compose.material3.pulltorefresh.pullToRefresh\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.material3.surfaceColorAtElevation\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextOverflow\nimport androidx.compose.ui.unit.Dp\nimport androidx.compose.ui.unit.dp\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.data.model.AppInfo\nimport me.weishu.kernelsu.ui.component.AppIconImage\nimport me.weishu.kernelsu.ui.component.material.SearchAppBar\nimport me.weishu.kernelsu.ui.component.material.SegmentedLazyColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedListItem\nimport me.weishu.kernelsu.ui.component.statustag.StatusTag\nimport me.weishu.kernelsu.ui.util.ownerNameForUid\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun SuperUserPagerMaterial(\n    uiState: SuperUserUiState,\n    actions: SuperUserActions,\n    bottomInnerPadding: Dp,\n) {\n    val scope = rememberCoroutineScope()\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n    val listState = rememberLazyListState()\n    val searchListState = rememberLazyListState()\n    val pullToRefreshState = rememberPullToRefreshState()\n\n    val scaleFraction = {\n        if (uiState.isRefreshing) 1f\n        else LinearOutSlowInEasing.transform(pullToRefreshState.distanceFraction).coerceIn(0f, 1f)\n    }\n\n    var localSearchText by remember { mutableStateOf(uiState.searchStatus.searchText) }\n    LaunchedEffect(uiState.searchStatus.searchText) {\n        localSearchText = uiState.searchStatus.searchText\n    }\n\n    Scaffold(\n        modifier = Modifier\n            .nestedScroll(scrollBehavior.nestedScrollConnection)\n            .pullToRefresh(\n                state = pullToRefreshState,\n                isRefreshing = uiState.isRefreshing,\n                onRefresh = actions.onRefresh,\n            ),\n        topBar = {\n            SearchAppBar(\n                title = { Text(stringResource(R.string.superuser)) },\n                searchText = localSearchText,\n                onSearchTextChange = {\n                    localSearchText = it\n                    actions.onSearchTextChange(it)\n                    scope.launch { listState.scrollToItem(0) }\n                },\n                onClearClick = {\n                    localSearchText = \"\"\n                    actions.onClearSearch()\n                },\n                actions = {\n                    var showDropdown by remember { mutableStateOf(false) }\n\n                    IconButton(onClick = { showDropdown = true }) {\n                        Icon(\n                            imageVector = Icons.Filled.MoreVert,\n                            contentDescription = stringResource(id = R.string.settings)\n                        )\n\n                        DropdownMenu(\n                            expanded = showDropdown,\n                            onDismissRequest = { showDropdown = false }\n                        ) {\n                            DropdownMenuItem(\n                                text = { Text(stringResource(R.string.show_system_apps)) },\n                                trailingIcon = { Checkbox(uiState.showSystemApps, null) },\n                                onClick = {\n                                    actions.onToggleShowSystemApps()\n                                    showDropdown = false\n                                }\n                            )\n                            if (uiState.userIds.size > 1) {\n                                DropdownMenuItem(\n                                    text = { Text(stringResource(R.string.show_only_primary_user_apps)) },\n                                    trailingIcon = { Checkbox(uiState.showOnlyPrimaryUserApps, null) },\n                                    onClick = {\n                                        actions.onToggleShowOnlyPrimaryUserApps()\n                                        showDropdown = false\n                                    }\n                                )\n                            }\n                        }\n                    }\n                },\n                scrollBehavior = scrollBehavior,\n                searchContent = { bottomPadding, closeSearch ->\n                    LaunchedEffect(localSearchText) {\n                        searchListState.scrollToItem(0)\n                    }\n                    SegmentedLazyColumn(\n                        state = searchListState,\n                        modifier = Modifier\n                            .fillMaxSize()\n                            .nestedScroll(scrollBehavior.nestedScrollConnection),\n                        contentPadding = PaddingValues(\n                            start = 16.dp,\n                            end = 16.dp,\n                            top = 8.dp,\n                            bottom = 16.dp + bottomPadding\n                        ),\n                        key = { it.uid },\n                        items = uiState.searchResults,\n                    ) { group ->\n                        Column {\n                            GroupItem(\n                                group = group,\n                                selected = false,\n                                onToggleExpand = {},\n                            ) {\n                                closeSearch()\n                                actions.onOpenProfile(group)\n                            }\n                            AnimatedVisibility(\n                                visible = group.apps.size > 1,\n                                enter = expandVertically() + fadeIn(),\n                                exit = shrinkVertically() + fadeOut()\n                            ) {\n                                Column {\n                                    group.apps.forEach { app ->\n                                        SimpleAppItem(\n                                            app = app,\n                                            matched = group.matchedPackageNames.contains(app.packageName),\n                                        ) {\n                                            closeSearch()\n                                            actions.onOpenProfile(group)\n                                        }\n                                    }\n                                }\n                            }\n                        }\n                    }\n                }\n            )\n        },\n        contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        Box(modifier = Modifier.padding(innerPadding)) {\n            val expandedSearchUids = remember { mutableStateOf(setOf<Int>()) }\n\n            SegmentedLazyColumn(\n                state = listState,\n                modifier = Modifier\n                    .fillMaxSize()\n                    .nestedScroll(scrollBehavior.nestedScrollConnection),\n                contentPadding = PaddingValues(\n                    start = 16.dp,\n                    end = 16.dp,\n                    top = 8.dp,\n                    bottom = 16.dp + bottomInnerPadding\n                ),\n                key = { it.uid },\n                items = uiState.groupedApps,\n            ) { group ->\n                val expanded = expandedSearchUids.value.contains(group.uid)\n                val onToggleExpand = {\n                    if (group.apps.size > 1) {\n                        expandedSearchUids.value = if (expandedSearchUids.value.contains(group.uid)) {\n                            expandedSearchUids.value - group.uid\n                        } else {\n                            expandedSearchUids.value + group.uid\n                        }\n                    }\n                }\n                Column {\n                    GroupItem(\n                        group = group,\n                        selected = expanded,\n                        onToggleExpand = onToggleExpand,\n                    ) {\n                        actions.onOpenProfile(group)\n                    }\n                    AnimatedVisibility(\n                        visible = expanded && group.apps.size > 1,\n                        enter = expandVertically() + fadeIn(),\n                        exit = shrinkVertically() + fadeOut()\n                    ) {\n                        Column {\n                            group.apps.forEach { app ->\n                                SimpleAppItem(app = app) {\n                                    actions.onOpenProfile(group)\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n            Box(\n                modifier = Modifier\n                    .align(Alignment.TopCenter)\n                    .graphicsLayer {\n                        scaleX = scaleFraction()\n                        scaleY = scaleFraction()\n                    }\n            ) {\n                PullToRefreshDefaults.LoadingIndicator(\n                    state = pullToRefreshState,\n                    isRefreshing = uiState.isRefreshing\n                )\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun SimpleAppItem(\n    app: AppInfo,\n    matched: Boolean = false,\n    onNavigate: () -> Unit,\n) {\n    ListItem(\n        onClick = onNavigate,\n        modifier = Modifier.padding(horizontal = 4.dp),\n        shapes = ListItemDefaults.shapes(shape = RoundedCornerShape(0.dp)),\n        colors = ListItemDefaults.colors(\n            containerColor = if (matched) {\n                MaterialTheme.colorScheme.secondaryContainer\n            } else {\n                MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)\n            }\n        ),\n        content = { Text(app.label, overflow = TextOverflow.Ellipsis, maxLines = 1) },\n        supportingContent = { Text(app.packageName, overflow = TextOverflow.Ellipsis, maxLines = 1) },\n        leadingContent = {\n            AppIconImage(\n                packageInfo = app.packageInfo,\n                label = app.label,\n                modifier = Modifier\n                    .size(40.dp)\n                    .padding(start = 4.dp)\n            )\n        },\n        trailingContent = {\n            Icon(\n                Icons.Filled.Remove,\n                contentDescription = null,\n                modifier = Modifier.padding(end = 4.dp)\n            )\n        }\n    )\n}\n\n@Composable\nprivate fun GroupItem(\n    group: GroupedApps,\n    selected: Boolean,\n    onToggleExpand: () -> Unit,\n    onClickPrimary: () -> Unit,\n) {\n    val summaryText = if (group.apps.size > 1) {\n        stringResource(R.string.group_contains_apps, group.apps.size)\n    } else {\n        group.primary.packageName\n    }\n    SegmentedListItem(\n        selected = selected,\n        onClick = onClickPrimary,\n        onLongClick = if (group.apps.size > 1) onToggleExpand else null,\n        headlineContent = {\n            Text(\n                text = if (group.apps.size > 1) ownerNameForUid(group.uid) else group.primary.label,\n                overflow = TextOverflow.Ellipsis,\n                maxLines = 1\n            )\n        },\n        supportingContent = {\n            Column {\n                Text(\n                    text = summaryText,\n                    color = MaterialTheme.colorScheme.outline,\n                    overflow = TextOverflow.Ellipsis,\n                    maxLines = 1\n                )\n                FlowRow {\n                    val userId = group.uid / 100000\n                    val packageInfo = group.primary.packageInfo\n                    val applicationInfo = packageInfo.applicationInfo\n\n                    if (group.anyAllowSu) {\n                        StatusTag(\n                            label = \"ROOT\",\n                            modifier = Modifier.padding(top = 4.dp),\n                            contentColor = MaterialTheme.colorScheme.onPrimary,\n                            backgroundColor = MaterialTheme.colorScheme.primary\n                        )\n                    } else if (group.shouldUmount) {\n                        StatusTag(\n                            label = \"UMOUNT\",\n                            modifier = Modifier.padding(top = 4.dp),\n                            contentColor = MaterialTheme.colorScheme.onSecondary,\n                            backgroundColor = MaterialTheme.colorScheme.secondary\n                        )\n                    }\n                    if (group.anyCustom) {\n                        StatusTag(\n                            label = \"CUSTOM\",\n                            modifier = Modifier.padding(top = 4.dp),\n                            contentColor = MaterialTheme.colorScheme.onSecondaryContainer,\n                            backgroundColor = MaterialTheme.colorScheme.secondaryContainer\n                        )\n                    }\n                    if (userId != 0) {\n                        StatusTag(\n                            label = \"USER $userId\",\n                            modifier = Modifier.padding(top = 4.dp),\n                            contentColor = MaterialTheme.colorScheme.onTertiary,\n                            backgroundColor = MaterialTheme.colorScheme.tertiary\n                        )\n                    }\n                    if (applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0\n                        || applicationInfo.flags.and(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0\n                    ) {\n                        StatusTag(\n                            label = \"SYSTEM\",\n                            modifier = Modifier.padding(top = 4.dp),\n                            contentColor = MaterialTheme.colorScheme.onTertiary,\n                            backgroundColor = MaterialTheme.colorScheme.tertiary\n                        )\n                    }\n                    if (!packageInfo.sharedUserId.isNullOrEmpty()) {\n                        StatusTag(\n                            label = \"SHARED UID\",\n                            modifier = Modifier.padding(top = 4.dp),\n                            contentColor = MaterialTheme.colorScheme.onTertiary,\n                            backgroundColor = MaterialTheme.colorScheme.tertiary\n                        )\n                    }\n                }\n            }\n        },\n        leadingContent = {\n            AppIconImage(\n                packageInfo = group.primary.packageInfo,\n                label = group.primary.label,\n                modifier = Modifier.size(48.dp)\n            )\n        },\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/superuser/SuperUserMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.superuser\r\n\r\nimport android.content.pm.ApplicationInfo\r\nimport androidx.compose.animation.AnimatedVisibility\r\nimport androidx.compose.animation.expandVertically\r\nimport androidx.compose.animation.fadeIn\r\nimport androidx.compose.animation.fadeOut\r\nimport androidx.compose.animation.shrinkVertically\r\nimport androidx.compose.foundation.Image\r\nimport androidx.compose.foundation.background\r\nimport androidx.compose.foundation.basicMarquee\r\nimport androidx.compose.foundation.layout.Arrangement\r\nimport androidx.compose.foundation.layout.Box\r\nimport androidx.compose.foundation.layout.Column\r\nimport androidx.compose.foundation.layout.FlowRow\r\nimport androidx.compose.foundation.layout.PaddingValues\r\nimport androidx.compose.foundation.layout.Row\r\nimport androidx.compose.foundation.layout.Spacer\r\nimport androidx.compose.foundation.layout.WindowInsets\r\nimport androidx.compose.foundation.layout.WindowInsetsSides\r\nimport androidx.compose.foundation.layout.add\r\nimport androidx.compose.foundation.layout.asPaddingValues\r\nimport androidx.compose.foundation.layout.calculateEndPadding\r\nimport androidx.compose.foundation.layout.calculateStartPadding\r\nimport androidx.compose.foundation.layout.displayCutout\r\nimport androidx.compose.foundation.layout.fillMaxHeight\r\nimport androidx.compose.foundation.layout.fillMaxSize\r\nimport androidx.compose.foundation.layout.height\r\nimport androidx.compose.foundation.layout.ime\r\nimport androidx.compose.foundation.layout.only\r\nimport androidx.compose.foundation.layout.padding\r\nimport androidx.compose.foundation.layout.size\r\nimport androidx.compose.foundation.layout.systemBars\r\nimport androidx.compose.foundation.layout.width\r\nimport androidx.compose.foundation.lazy.LazyColumn\r\nimport androidx.compose.foundation.lazy.items\r\nimport androidx.compose.runtime.Composable\r\nimport androidx.compose.runtime.Immutable\r\nimport androidx.compose.runtime.LaunchedEffect\r\nimport androidx.compose.runtime.derivedStateOf\r\nimport androidx.compose.runtime.getValue\r\nimport androidx.compose.runtime.mutableStateOf\r\nimport androidx.compose.runtime.remember\r\nimport androidx.compose.ui.Alignment\r\nimport androidx.compose.ui.Modifier\r\nimport androidx.compose.ui.draw.clip\r\nimport androidx.compose.ui.graphics.Color\r\nimport androidx.compose.ui.graphics.ColorFilter\r\nimport androidx.compose.ui.graphics.graphicsLayer\r\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\r\nimport androidx.compose.ui.platform.LocalLayoutDirection\r\nimport androidx.compose.ui.res.stringResource\r\nimport androidx.compose.ui.text.font.FontWeight\r\nimport androidx.compose.ui.unit.Dp\r\nimport androidx.compose.ui.unit.LayoutDirection\r\nimport androidx.compose.ui.unit.dp\r\nimport androidx.compose.ui.unit.sp\r\nimport com.kyant.capsule.ContinuousRoundedRectangle\r\nimport dev.chrisbanes.haze.HazeState\r\nimport dev.chrisbanes.haze.HazeStyle\r\nimport dev.chrisbanes.haze.HazeTint\r\nimport dev.chrisbanes.haze.hazeSource\r\nimport me.weishu.kernelsu.R\r\nimport me.weishu.kernelsu.data.model.AppInfo\r\nimport me.weishu.kernelsu.ui.component.AppIconImage\r\nimport me.weishu.kernelsu.ui.component.miuix.SearchBox\r\nimport me.weishu.kernelsu.ui.component.miuix.SearchPager\r\nimport me.weishu.kernelsu.ui.component.statustag.StatusTag\r\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\r\nimport me.weishu.kernelsu.ui.theme.isInDarkTheme\r\nimport me.weishu.kernelsu.ui.util.ownerNameForUid\r\nimport top.yukonga.miuix.kmp.basic.BasicComponent\r\nimport top.yukonga.miuix.kmp.basic.Card\r\nimport top.yukonga.miuix.kmp.basic.DropdownImpl\r\nimport top.yukonga.miuix.kmp.basic.Icon\r\nimport top.yukonga.miuix.kmp.basic.IconButton\r\nimport top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator\r\nimport top.yukonga.miuix.kmp.basic.ListPopupColumn\r\nimport top.yukonga.miuix.kmp.basic.ListPopupDefaults\r\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\r\nimport top.yukonga.miuix.kmp.basic.PopupPositionProvider\r\nimport top.yukonga.miuix.kmp.basic.PullToRefresh\r\nimport top.yukonga.miuix.kmp.basic.Scaffold\r\nimport top.yukonga.miuix.kmp.basic.Text\r\nimport top.yukonga.miuix.kmp.basic.TopAppBar\r\nimport top.yukonga.miuix.kmp.basic.rememberPullToRefreshState\r\nimport top.yukonga.miuix.kmp.extra.SuperListPopup\r\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\r\nimport top.yukonga.miuix.kmp.icon.basic.ArrowRight\r\nimport top.yukonga.miuix.kmp.icon.extended.MoreCircle\r\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\r\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\r\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\r\n\r\n@Composable\r\nfun SuperUserPagerMiuix(\r\n    uiState: SuperUserUiState,\r\n    actions: SuperUserActions,\r\n    bottomInnerPadding: Dp,\r\n) {\r\n    val searchStatus = uiState.searchStatus\r\n    val enableBlur = LocalEnableBlur.current\r\n\r\n    val scrollBehavior = MiuixScrollBehavior()\r\n    val dynamicTopPadding by remember {\r\n        derivedStateOf { 12.dp * (1f - scrollBehavior.state.collapsedFraction) }\r\n    }\r\n\r\n    val hazeState = remember { HazeState() }\r\n    val hazeStyle = if (enableBlur) {\r\n        HazeStyle(\r\n            backgroundColor = colorScheme.surface,\r\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\r\n        )\r\n    } else {\r\n        HazeStyle.Unspecified\r\n    }\r\n\r\n    Scaffold(\r\n        topBar = {\r\n            searchStatus.TopAppBarAnim(hazeState = hazeState, hazeStyle = hazeStyle) {\r\n                TopAppBar(\r\n                    color = if (enableBlur) Color.Transparent else colorScheme.surface,\r\n                    title = stringResource(R.string.superuser),\r\n                    actions = {\r\n                        val showTopPopup = remember { mutableStateOf(false) }\r\n                        SuperListPopup(\r\n                            show = showTopPopup.value,\r\n                            popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,\r\n                            alignment = PopupPositionProvider.Align.TopEnd,\r\n                            onDismissRequest = {\r\n                                showTopPopup.value = false\r\n                            },\r\n                            content = {\r\n                                val isMultiUser = uiState.userIds.size > 1\r\n                                val size = if (isMultiUser) 2 else 1\r\n                                ListPopupColumn {\r\n                                    DropdownImpl(\r\n                                        text = stringResource(R.string.show_system_apps),\r\n                                        isSelected = uiState.showSystemApps,\r\n                                        optionSize = size,\r\n                                        onSelectedIndexChange = {\r\n                                            actions.onToggleShowSystemApps()\r\n                                            showTopPopup.value = false\r\n                                        },\r\n                                        index = 0\r\n                                    )\r\n                                    if (isMultiUser) {\r\n                                        DropdownImpl(\r\n                                            text = stringResource(R.string.show_only_primary_user_apps),\r\n                                            isSelected = uiState.showOnlyPrimaryUserApps,\r\n                                            optionSize = size,\r\n                                            onSelectedIndexChange = {\r\n                                                actions.onToggleShowOnlyPrimaryUserApps()\r\n                                                showTopPopup.value = false\r\n                                            },\r\n                                            index = 1\r\n                                        )\r\n                                    }\r\n                                }\r\n                            }\r\n                        )\r\n                        IconButton(\r\n                            modifier = Modifier.padding(end = 16.dp),\r\n                            onClick = {\r\n                                showTopPopup.value = true\r\n                            },\r\n                            holdDownState = showTopPopup.value\r\n                        ) {\r\n                            Icon(\r\n                                imageVector = MiuixIcons.MoreCircle,\r\n                                tint = colorScheme.onSurface,\r\n                                contentDescription = null\r\n                            )\r\n                        }\r\n                    },\r\n                    scrollBehavior = scrollBehavior\r\n                )\r\n            }\r\n        },\r\n        popupHost = {\r\n            val expandedSearchUids = remember { mutableStateOf(setOf<Int>()) }\r\n            LaunchedEffect(uiState.searchResults) {\r\n                expandedSearchUids.value = uiState.searchResults\r\n                    .filter { it.apps.size > 1 }\r\n                    .map { it.uid }\r\n                    .toSet()\r\n            }\r\n            searchStatus.SearchPager(\r\n                onSearchStatusChange = actions.onSearchStatusChange,\r\n                defaultResult = {},\r\n                searchBarTopPadding = dynamicTopPadding,\r\n            ) {\r\n                val imeBottomPadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding()\r\n                LazyColumn(\r\n                    modifier = Modifier\r\n                        .fillMaxSize()\r\n                        .overScrollVertical(),\r\n                ) {\r\n                    item {\r\n                        Spacer(Modifier.height(6.dp))\r\n                    }\r\n                    items(uiState.searchResults, key = { it.uid }) { group ->\r\n                        val expanded = expandedSearchUids.value.contains(group.uid)\r\n                        AnimatedVisibility(\r\n                            visible = uiState.searchResults.isNotEmpty(),\r\n                            enter = fadeIn() + expandVertically(),\r\n                            exit = fadeOut() + shrinkVertically()\r\n                        ) {\r\n                            Column {\r\n                                GroupItem(\r\n                                    group = group,\r\n                                    onToggleExpand = {\r\n                                        if (group.apps.size > 1) {\r\n                                            expandedSearchUids.value =\r\n                                                if (expanded) expandedSearchUids.value - group.uid else expandedSearchUids.value + group.uid\r\n                                        }\r\n                                    },\r\n                                ) {\r\n                                    actions.onOpenProfile(group)\r\n                                }\r\n                                AnimatedVisibility(\r\n                                    visible = expanded && group.apps.size > 1,\r\n                                    enter = expandVertically() + fadeIn(),\r\n                                    exit = shrinkVertically() + fadeOut()\r\n                                ) {\r\n                                    Column {\r\n                                        group.apps.forEach { app ->\r\n                                            SimpleAppItem(\r\n                                                app = app,\r\n                                                matched = group.matchedPackageNames.contains(app.packageName),\r\n                                            )\r\n                                        }\r\n                                        Spacer(Modifier.height(6.dp))\r\n                                    }\r\n                                }\r\n                            }\r\n                        }\r\n                    }\r\n                    item {\r\n                        Spacer(Modifier.height(maxOf(bottomInnerPadding, imeBottomPadding)))\r\n                    }\r\n                }\r\n            }\r\n        },\r\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\r\n    ) { innerPadding ->\r\n        val layoutDirection = LocalLayoutDirection.current\r\n        searchStatus.SearchBox(\r\n            onSearchStatusChange = actions.onSearchStatusChange,\r\n            searchBarTopPadding = dynamicTopPadding,\r\n            contentPadding = PaddingValues(\r\n                top = innerPadding.calculateTopPadding(),\r\n                start = innerPadding.calculateStartPadding(layoutDirection),\r\n                end = innerPadding.calculateEndPadding(layoutDirection)\r\n            ),\r\n            hazeState = hazeState,\r\n            hazeStyle = hazeStyle\r\n        ) { boxHeight ->\r\n            val pullToRefreshState = rememberPullToRefreshState()\r\n            val refreshTexts = listOf(\r\n                stringResource(R.string.refresh_pulling),\r\n                stringResource(R.string.refresh_release),\r\n                stringResource(R.string.refresh_refresh),\r\n                stringResource(R.string.refresh_complete),\r\n            )\r\n            if (uiState.groupedApps.isEmpty() && uiState.isRefreshing) {\r\n                Box(\r\n                    modifier = Modifier\r\n                        .fillMaxSize()\r\n                        .padding(\r\n                            top = innerPadding.calculateTopPadding(),\r\n                            start = innerPadding.calculateStartPadding(layoutDirection),\r\n                            end = innerPadding.calculateEndPadding(layoutDirection),\r\n                            bottom = bottomInnerPadding\r\n                        ),\r\n                    contentAlignment = Alignment.Center\r\n                ) {\r\n                    InfiniteProgressIndicator()\r\n                }\r\n            } else {\r\n                val expandedUids = remember { mutableStateOf(setOf<Int>()) }\r\n                PullToRefresh(\r\n                    isRefreshing = uiState.isRefreshing,\r\n                    pullToRefreshState = pullToRefreshState,\r\n                    onRefresh = actions.onRefresh,\r\n                    refreshTexts = refreshTexts,\r\n                    contentPadding = PaddingValues(\r\n                        top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp,\r\n                        start = innerPadding.calculateStartPadding(layoutDirection),\r\n                        end = innerPadding.calculateEndPadding(layoutDirection)\r\n                    ),\r\n                ) {\r\n                    LazyColumn(\r\n                        modifier = Modifier\r\n                            .fillMaxHeight()\r\n                            .scrollEndHaptic()\r\n                            .overScrollVertical()\r\n                            .nestedScroll(scrollBehavior.nestedScrollConnection)\r\n                            .let { if (enableBlur) it.hazeSource(state = hazeState) else it },\r\n                        contentPadding = PaddingValues(\r\n                            top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp,\r\n                            start = innerPadding.calculateStartPadding(layoutDirection),\r\n                            end = innerPadding.calculateEndPadding(layoutDirection)\r\n                        ),\r\n                        overscrollEffect = null,\r\n                    ) {\r\n                        items(uiState.groupedApps, key = { it.uid }) { group ->\r\n                            val expanded = expandedUids.value.contains(group.uid)\r\n                            AnimatedVisibility(\r\n                                visible = true,\r\n                                enter = expandVertically() + fadeIn(),\r\n                                exit = shrinkVertically() + fadeOut()\r\n                            ) {\r\n                                Column {\r\n                                    GroupItem(\r\n                                        group = group,\r\n                                        onToggleExpand = {\r\n                                            if (group.apps.size > 1) {\r\n                                                expandedUids.value =\r\n                                                    if (expanded) expandedUids.value - group.uid else expandedUids.value + group.uid\r\n                                            }\r\n                                        }\r\n                                    ) {\r\n                                        actions.onOpenProfile(group)\r\n                                    }\r\n                                    AnimatedVisibility(\r\n                                        visible = expanded && group.apps.size > 1,\r\n                                        enter = expandVertically() + fadeIn(),\r\n                                        exit = shrinkVertically() + fadeOut()\r\n                                    ) {\r\n                                        Column {\r\n                                            group.apps.forEach { app ->\r\n                                                SimpleAppItem(app = app)\r\n                                            }\r\n                                            Spacer(Modifier.height(6.dp))\r\n                                        }\r\n                                    }\r\n                                }\r\n                            }\r\n                        }\r\n                        item {\r\n                            Spacer(Modifier.height(bottomInnerPadding))\r\n                        }\r\n                    }\r\n                }\r\n            }\r\n        }\r\n    }\r\n}\r\n\r\n@Composable\r\nprivate fun SimpleAppItem(\r\n    app: AppInfo,\r\n    matched: Boolean = false,\r\n) {\r\n    Row {\r\n        Box(\r\n            modifier = Modifier\r\n                .padding(start = 12.dp)\r\n                .width(6.dp)\r\n                .height(24.dp)\r\n                .align(Alignment.CenterVertically)\r\n                .clip(ContinuousRoundedRectangle(16.dp))\r\n                .background(if (matched) colorScheme.primary else colorScheme.primaryContainer)\r\n        )\r\n        Card(\r\n            modifier = Modifier\r\n                .padding(start = 6.dp, end = 12.dp, bottom = 6.dp)\r\n        ) {\r\n            BasicComponent(\r\n                title = app.label,\r\n                summary = app.packageName,\r\n                startAction = {\r\n                    AppIconImage(\r\n                        packageInfo = app.packageInfo,\r\n                        label = app.label,\r\n                        modifier = Modifier\r\n                            .padding(end = 9.dp)\r\n                            .size(40.dp)\r\n                    )\r\n                },\r\n                insideMargin = PaddingValues(horizontal = 9.dp)\r\n            )\r\n        }\r\n    }\r\n}\r\n\r\n@Composable\r\nprivate fun GroupItem(\r\n    group: GroupedApps,\r\n    onToggleExpand: () -> Unit,\r\n    onClickPrimary: () -> Unit,\r\n) {\r\n    val isInDarkTheme = isInDarkTheme()\r\n    val bg = colorScheme.secondaryContainer.copy(alpha = 0.8f)\r\n    val rootBg = colorScheme.tertiaryContainer.copy(alpha = 0.6f)\r\n    val unmountBg = if (isInDarkTheme) Color.White.copy(alpha = 0.4f) else Color.Black.copy(alpha = 0.3f)\r\n    val fg = colorScheme.onSecondaryContainer\r\n    val rootFg = colorScheme.onTertiaryContainer.copy(alpha = 0.8f)\r\n    val unmountFg = if (isInDarkTheme) Color.Black.copy(alpha = 0.4f) else Color.White.copy(alpha = 0.8f)\r\n\r\n    val userId = group.uid / 100000\r\n    val packageInfo = group.primary.packageInfo\r\n    val applicationInfo = packageInfo.applicationInfo\r\n    val hasSharedUserId = !packageInfo.sharedUserId.isNullOrEmpty()\r\n    val isSystemApp = applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0\r\n            || applicationInfo.flags.and(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0\r\n    val tags = buildList {\r\n        if (group.anyAllowSu) add(StatusMeta(\"ROOT\", rootBg, rootFg))\r\n        if (group.shouldUmount) add(StatusMeta(\"UMOUNT\", unmountBg, unmountFg))\r\n        if (group.anyCustom) add(StatusMeta(\"CUSTOM\", bg, fg))\r\n        if (userId != 0) add(StatusMeta(\"USER $userId\", bg, fg))\r\n        if (isSystemApp) add(StatusMeta(\"SYSTEM\", bg, fg))\r\n        if (hasSharedUserId) add(StatusMeta(\"SHARED UID\", bg, fg))\r\n    }\r\n    Card(\r\n        modifier = Modifier\r\n            .padding(horizontal = 12.dp)\r\n            .padding(bottom = 12.dp),\r\n        onClick = onClickPrimary,\r\n        onLongPress = if (group.apps.size > 1) onToggleExpand else null,\r\n        showIndication = true,\r\n        insideMargin = PaddingValues(vertical = 8.dp, horizontal = 16.dp)\r\n    ) {\r\n        Row(\r\n            verticalAlignment = Alignment.CenterVertically\r\n        ) {\r\n            AppIconImage(\r\n                packageInfo = group.primary.packageInfo,\r\n                label = group.primary.label,\r\n                modifier = Modifier\r\n                    .padding(end = 14.dp)\r\n                    .size(48.dp)\r\n            )\r\n            Column(\r\n                modifier = Modifier\r\n                    .weight(1f),\r\n            ) {\r\n                Text(\r\n                    text = if (group.apps.size > 1) ownerNameForUid(group.uid) else group.primary.label,\r\n                    modifier = Modifier.basicMarquee(),\r\n                    fontWeight = FontWeight(550),\r\n                    color = colorScheme.onSurface,\r\n                    maxLines = 1,\r\n                    softWrap = false\r\n                )\r\n                Text(\r\n                    text = if (group.apps.size > 1) {\r\n                        stringResource(R.string.group_contains_apps, group.apps.size)\r\n                    } else {\r\n                        group.primary.packageName\r\n                    },\r\n                    modifier = Modifier\r\n                        .basicMarquee(),\r\n                    fontSize = 12.sp,\r\n                    fontWeight = FontWeight(550),\r\n                    color = colorScheme.onSurfaceVariantSummary,\r\n                    maxLines = 1,\r\n                    softWrap = false\r\n                )\r\n                FlowRow(\r\n                    modifier = Modifier.padding(top = 3.dp, bottom = 3.dp),\r\n                    horizontalArrangement = Arrangement.spacedBy(8.dp),\r\n                    verticalArrangement = Arrangement.spacedBy(4.dp)\r\n                ) {\r\n                    tags.forEach { tag ->\r\n                        StatusTag(\r\n                            label = tag.label,\r\n                            backgroundColor = tag.bg,\r\n                            contentColor = tag.fg\r\n                        )\r\n                    }\r\n                }\r\n            }\r\n            val layoutDirection = LocalLayoutDirection.current\r\n            Image(\r\n                modifier = Modifier\r\n                    .graphicsLayer {\r\n                        if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\r\n                    }\r\n                    .padding(start = 8.dp)\r\n                    .size(width = 10.dp, height = 16.dp),\r\n                imageVector = MiuixIcons.Basic.ArrowRight,\r\n                contentDescription = null,\r\n                colorFilter = ColorFilter.tint(colorScheme.onSurfaceVariantActions),\r\n            )\r\n        }\r\n    }\r\n}\r\n\r\n@Immutable\r\nprivate data class StatusMeta(\r\n    val label: String,\r\n    val bg: Color,\r\n    val fg: Color\r\n)\r\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/superuser/SuperUserScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.superuser\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.ui.unit.Dp\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.Navigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel\n\n@Composable\nfun SuperUserPager(\n    navigator: Navigator,\n    bottomInnerPadding: Dp\n) {\n    val viewModel = viewModel<SuperUserViewModel>()\n    val uiState by viewModel.uiState.collectAsState()\n\n    LaunchedEffect(Unit) {\n        if (uiState.groupedApps.isEmpty()) {\n            viewModel.initializePreferences()\n            viewModel.loadAppList().join()\n        } else if (viewModel.isNeedRefresh) {\n            viewModel.loadAppList(resort = false).join()\n        }\n    }\n\n    val onSearchTextChange: (String) -> Unit = viewModel::updateSearchText\n    val onToggleShowSystemApps: () -> Unit = {\n        viewModel.toggleShowSystemApps()\n    }\n    val onToggleShowOnlyPrimaryUserApps: () -> Unit = {\n        viewModel.toggleShowOnlyPrimaryUserApps()\n    }\n    val onOpenProfile: (GroupedApps) -> Unit = { group ->\n        navigator.push(Route.AppProfile(group.uid))\n        viewModel.markNeedRefresh()\n    }\n    val actions = SuperUserActions(\n        onRefresh = { viewModel.loadAppList(force = true) },\n        onSearchTextChange = onSearchTextChange,\n        onSearchStatusChange = viewModel::updateSearchStatus,\n        onClearSearch = { onSearchTextChange(\"\") },\n        onToggleShowSystemApps = onToggleShowSystemApps,\n        onToggleShowOnlyPrimaryUserApps = onToggleShowOnlyPrimaryUserApps,\n        onOpenProfile = onOpenProfile,\n    )\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> SuperUserPagerMiuix(\n            uiState = uiState,\n            actions = actions,\n            bottomInnerPadding = bottomInnerPadding,\n        )\n\n        UiMode.Material -> SuperUserPagerMaterial(\n            uiState = uiState,\n            actions = actions,\n            bottomInnerPadding = bottomInnerPadding,\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/superuser/SuperUserUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.superuser\n\nimport androidx.compose.runtime.Immutable\nimport me.weishu.kernelsu.data.model.AppInfo\nimport me.weishu.kernelsu.ui.component.SearchStatus\n\n@Immutable\ndata class GroupedApps(\n    val uid: Int,\n    val apps: List<AppInfo>,\n    val primary: AppInfo,\n    val anyAllowSu: Boolean,\n    val anyCustom: Boolean,\n    val shouldUmount: Boolean,\n    val ownerName: String? = null,\n    val matchedPackageNames: Set<String> = emptySet(),\n)\n\ndata class SuperUserUiState(\n    val isRefreshing: Boolean = false,\n    val groupedApps: List<GroupedApps> = emptyList(),\n    val userIds: List<Int> = emptyList(),\n    val searchStatus: SearchStatus = SearchStatus(\"\"),\n    val searchResults: List<GroupedApps> = emptyList(),\n    val showSystemApps: Boolean = false,\n    val showOnlyPrimaryUserApps: Boolean = false,\n    val error: Throwable? = null\n)\n\n@Immutable\ndata class SuperUserActions(\n    val onRefresh: () -> Unit,\n    val onSearchTextChange: (String) -> Unit,\n    val onSearchStatusChange: (SearchStatus) -> Unit,\n    val onClearSearch: () -> Unit,\n    val onToggleShowSystemApps: () -> Unit,\n    val onToggleShowOnlyPrimaryUserApps: () -> Unit,\n    val onOpenProfile: (GroupedApps) -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/template/TemplateMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.template\n\nimport androidx.compose.animation.core.LinearOutSlowInEasing\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.ExperimentalLayoutApi\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.Add\nimport androidx.compose.material.icons.filled.ContentCopy\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.DropdownMenu\nimport androidx.compose.material3.DropdownMenuItem\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.ExtendedFloatingActionButton\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeFlexibleTopAppBar\nimport androidx.compose.material3.LoadingIndicator\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.material3.pulltorefresh.PullToRefreshDefaults\nimport androidx.compose.material3.pulltorefresh.pullToRefresh\nimport androidx.compose.material3.pulltorefresh.rememberPullToRefreshState\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.derivedStateOf\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.data.model.TemplateInfo\nimport me.weishu.kernelsu.ui.component.material.SegmentedLazyColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedListItem\nimport me.weishu.kernelsu.ui.component.statustag.StatusTag\n\n/**\n * @author weishu\n * @date 2023/10/20.\n */\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun AppProfileTemplateScreenMaterial(\n    state: TemplateUiState,\n    actions: TemplateActions,\n) {\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n    val pullToRefreshState = rememberPullToRefreshState()\n    val listState = rememberLazyListState()\n    val threshold = with(LocalDensity.current) { 100.dp.toPx() }\n    val fabExpanded by remember {\n        var lastIndex = 0\n        var lastOffset = 0\n        var scrollDelta = 0f\n        var expanded = true\n        derivedStateOf {\n            val currentIndex = listState.firstVisibleItemIndex\n            val currentOffset = listState.firstVisibleItemScrollOffset\n            val delta = if (currentIndex == lastIndex) {\n                (currentOffset - lastOffset).toFloat()\n            } else if (currentIndex > lastIndex) {\n                100f\n            } else {\n                -100f\n            }\n            scrollDelta = (scrollDelta + delta).coerceIn(-threshold, threshold)\n            lastIndex = currentIndex\n            lastOffset = currentOffset\n            if (currentIndex == 0) {\n                expanded = true\n                scrollDelta = 0f\n            } else if (expanded && scrollDelta >= threshold) {\n                expanded = false\n                scrollDelta = 0f\n            } else if (!expanded && scrollDelta <= -threshold) {\n                expanded = true\n                scrollDelta = 0f\n            }\n            expanded\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit\n    }\n\n    val scaleFraction = {\n        if (state.isRefreshing) 1f\n        else LinearOutSlowInEasing.transform(pullToRefreshState.distanceFraction).coerceIn(0f, 1f)\n    }\n\n    Scaffold(\n        modifier = Modifier.pullToRefresh(\n            state = pullToRefreshState,\n            isRefreshing = state.isRefreshing,\n            onRefresh = { actions.onRefresh(false) },\n        ),\n        topBar = {\n            TopBar(\n                onBack = actions.onBack,\n                onImport = actions.onImport,\n                onExport = actions.onExport,\n                scrollBehavior = scrollBehavior\n            )\n        },\n        floatingActionButton = {\n            ExtendedFloatingActionButton(\n                expanded = fabExpanded,\n                onClick = actions.onCreateTemplate,\n                icon = { Icon(Icons.Filled.Add, null) },\n                text = { Text(stringResource(id = R.string.app_profile_template_create)) },\n                modifier = Modifier.padding(\n                    bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                            WindowInsets.captionBar.asPaddingValues().calculateBottomPadding(),\n                ),\n            )\n        },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        val isLoading = state.templateList.isEmpty()\n\n        if (isLoading && !state.isRefreshing) {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(innerPadding),\n                contentAlignment = Alignment.Center\n            ) {\n                if (state.offline) {\n                    Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                        Text(text = stringResource(R.string.network_offline), color = MaterialTheme.colorScheme.outline)\n                        Spacer(Modifier.height(12.dp))\n                        Button(\n                            onClick = { actions.onRefresh(false) },\n                        ) {\n                            Text(stringResource(R.string.network_retry))\n                        }\n                    }\n                } else {\n                    LoadingIndicator()\n                }\n            }\n        } else {\n            val templateList = state.templateList\n            val navBars = WindowInsets.navigationBars.asPaddingValues()\n            val captionBar = WindowInsets.captionBar.asPaddingValues()\n            Box(Modifier.padding(innerPadding)) {\n                SegmentedLazyColumn(\n                    modifier = Modifier\n                        .fillMaxSize()\n                        .nestedScroll(scrollBehavior.nestedScrollConnection),\n                    state = listState,\n                    contentPadding = PaddingValues(\n                        start = 16.dp,\n                        top = 8.dp,\n                        end = 16.dp,\n                        bottom = 16.dp + 56.dp + 16.dp + navBars.calculateBottomPadding() + captionBar.calculateBottomPadding()\n                    ),\n                    items = templateList,\n                    itemContent = { template ->\n                        TemplateItem(\n                            template = template,\n                            onClick = { actions.onOpenTemplate(template) },\n                        )\n                    }\n                )\n                Box(\n                    modifier = Modifier\n                        .align(Alignment.TopCenter)\n                        .graphicsLayer {\n                            scaleX = scaleFraction()\n                            scaleY = scaleFraction()\n                        }\n                ) {\n                    PullToRefreshDefaults.LoadingIndicator(state = pullToRefreshState, isRefreshing = state.isRefreshing)\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalLayoutApi::class)\n@Composable\nprivate fun TemplateItem(\n    template: TemplateInfo,\n    onClick: () -> Unit,\n) {\n    SegmentedListItem(\n        onClick = onClick,\n        headlineContent = { Text(template.name) },\n        supportingContent = {\n            Column {\n                Text(\n                    text = \"${template.id}${if (template.author.isEmpty()) \"\" else \"@${template.author}\"}\",\n                    style = MaterialTheme.typography.bodyMedium,\n                    fontSize = MaterialTheme.typography.bodyMedium.fontSize,\n                )\n                Text(template.description, color = MaterialTheme.colorScheme.outline)\n                FlowRow(modifier = Modifier.padding(top = 4.dp)) {\n                    StatusTag(\n                        label = \"UID: ${template.uid}\",\n                        contentColor = MaterialTheme.colorScheme.onPrimary,\n                        backgroundColor = MaterialTheme.colorScheme.primary\n                    )\n                    StatusTag(\n                        label = \"GID: ${template.gid}\",\n                        contentColor = MaterialTheme.colorScheme.onPrimary,\n                        backgroundColor = MaterialTheme.colorScheme.primary\n                    )\n                    StatusTag(\n                        label = template.context,\n                        contentColor = MaterialTheme.colorScheme.onPrimary,\n                        backgroundColor = MaterialTheme.colorScheme.primary\n                    )\n                    if (template.local) {\n                        StatusTag(\n                            label = \"local\",\n                            contentColor = MaterialTheme.colorScheme.onPrimary,\n                            backgroundColor = MaterialTheme.colorScheme.primary\n                        )\n                    } else {\n                        StatusTag(\n                            label = \"remote\",\n                            contentColor = MaterialTheme.colorScheme.onPrimary,\n                            backgroundColor = MaterialTheme.colorScheme.primary\n                        )\n                    }\n                }\n            }\n        },\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun TopBar(\n    onBack: () -> Unit,\n    onImport: () -> Unit = {},\n    onExport: () -> Unit = {},\n    scrollBehavior: TopAppBarScrollBehavior? = null\n) {\n    LargeFlexibleTopAppBar(\n        title = {\n            Text(stringResource(R.string.settings_profile_template))\n        },\n        navigationIcon = {\n            IconButton(\n                onClick = onBack\n            ) {\n                Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)\n            }\n        },\n        actions = {\n            var showDropdown by remember { mutableStateOf(false) }\n            IconButton(\n                onClick = { showDropdown = true }\n            ) {\n                Icon(\n                    imageVector = Icons.Filled.ContentCopy,\n                    contentDescription = stringResource(id = R.string.app_profile_import_export)\n                )\n                DropdownMenu(\n                    expanded = showDropdown,\n                    onDismissRequest = { showDropdown = false }\n                ) {\n                    DropdownMenuItem(\n                        text = { Text(stringResource(id = R.string.app_profile_import_from_clipboard)) },\n                        onClick = {\n                            onImport()\n                            showDropdown = false\n                        }\n                    )\n                    DropdownMenuItem(\n                        text = { Text(stringResource(id = R.string.app_profile_export_to_clipboard)) },\n                        onClick = {\n                            onExport()\n                            showDropdown = false\n                        }\n                    )\n                }\n            }\n        },\n        colors = TopAppBarDefaults.topAppBarColors(\n            containerColor = MaterialTheme.colorScheme.surface,\n            scrolledContainerColor = MaterialTheme.colorScheme.surface\n        ),\n        scrollBehavior = scrollBehavior\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/template/TemplateMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.template\n\nimport android.annotation.SuppressLint\nimport androidx.compose.animation.core.animateDpAsState\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.border\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.FlowRow\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.calculateEndPadding\nimport androidx.compose.foundation.layout.calculateStartPadding\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.offset\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.size\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.foundation.lazy.rememberLazyListState\nimport androidx.compose.foundation.shape.CircleShape\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.outlined.Fingerprint\nimport androidx.compose.material.icons.outlined.Group\nimport androidx.compose.material.icons.outlined.Shield\nimport androidx.compose.material.icons.rounded.Add\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableFloatStateOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.geometry.Offset\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.graphics.vector.ImageVector\nimport androidx.compose.ui.input.nestedscroll.NestedScrollConnection\nimport androidx.compose.ui.input.nestedscroll.NestedScrollSource\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.unit.IntOffset\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.unit.sp\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.data.model.TemplateInfo\nimport me.weishu.kernelsu.ui.component.miuix.DropdownItem\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport top.yukonga.miuix.kmp.basic.Card\nimport top.yukonga.miuix.kmp.basic.FloatingActionButton\nimport top.yukonga.miuix.kmp.basic.HorizontalDivider\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.IconButton\nimport top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator\nimport top.yukonga.miuix.kmp.basic.ListPopupColumn\nimport top.yukonga.miuix.kmp.basic.ListPopupDefaults\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\nimport top.yukonga.miuix.kmp.basic.PopupPositionProvider\nimport top.yukonga.miuix.kmp.basic.PullToRefresh\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.ScrollBehavior\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.basic.TopAppBar\nimport top.yukonga.miuix.kmp.basic.rememberPullToRefreshState\nimport top.yukonga.miuix.kmp.extra.SuperListPopup\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.extended.Back\nimport top.yukonga.miuix.kmp.icon.extended.Copy\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.utils.PressFeedbackType\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\n\n/**\n * @author weishu\n * @date 2023/10/20.\n */\n@SuppressLint(\"LocalContextGetResourceValueCall\")\n@Composable\nfun AppProfileTemplateScreenMiuix(\n    state: TemplateUiState,\n    actions: TemplateActions,\n) {\n    val scrollBehavior = MiuixScrollBehavior()\n\n    val listState = rememberLazyListState()\n    var fabVisible by remember { mutableStateOf(true) }\n    var scrollDistance by remember { mutableFloatStateOf(0f) }\n    val nestedScrollConnection = remember {\n        object : NestedScrollConnection {\n            override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {\n                val isScrolledToEnd =\n                    (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1\n                            && (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.size\n                        ?: 0) < listState.layoutInfo.viewportEndOffset)\n                val delta = available.y\n                if (!isScrolledToEnd) {\n                    scrollDistance += delta\n                    if (scrollDistance < -50f) {\n                        if (fabVisible) fabVisible = false\n                        scrollDistance = 0f\n                    } else if (scrollDistance > 50f) {\n                        if (!fabVisible) fabVisible = true\n                        scrollDistance = 0f\n                    }\n                }\n                return Offset.Zero\n            }\n        }\n    }\n    val offsetHeight by animateDpAsState(\n        targetValue = if (fabVisible) 0.dp else 100.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(),\n        animationSpec = tween(durationMillis = 350)\n    )\n    val enableBlur = LocalEnableBlur.current\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n\n    Scaffold(\n        topBar = {\n            TopBar(\n                onBack = actions.onBack,\n                onImport = actions.onImport,\n                onExport = actions.onExport,\n                scrollBehavior = scrollBehavior,\n                hazeState = hazeState,\n                hazeStyle = hazeStyle,\n                enableBlur = enableBlur,\n            )\n        },\n        floatingActionButton = {\n            FloatingActionButton(\n                containerColor = colorScheme.primary,\n                shadowElevation = 0.dp,\n                onClick = actions.onCreateTemplate,\n                modifier = Modifier\n                    .offset {\n                        IntOffset(x = 0, y = offsetHeight.roundToPx())\n                    }\n                    .padding(\n                        bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                                WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + 20.dp,\n                        end = 20.dp\n                    )\n                    .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape),\n                content = {\n                    Icon(\n                        Icons.Rounded.Add,\n                        null,\n                        Modifier.size(40.dp),\n                        tint = colorScheme.onPrimary\n                    )\n                },\n            )\n        },\n        popupHost = { },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        if (state.templateList.isEmpty() && !state.isRefreshing) {\n            val layoutDirection = LocalLayoutDirection.current\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .padding(\n                        start = innerPadding.calculateStartPadding(layoutDirection),\n                        end = innerPadding.calculateEndPadding(layoutDirection),\n                        bottom = innerPadding.calculateBottomPadding(),\n                    ),\n                contentAlignment = Alignment.Center\n            ) {\n                if (state.offline) {\n                    Column(horizontalAlignment = Alignment.CenterHorizontally) {\n                        Text(text = stringResource(R.string.network_offline), color = colorScheme.onSurfaceVariantSummary, fontSize = 16.sp)\n                        Spacer(Modifier.height(12.dp))\n                        TextButton(\n                            modifier = Modifier\n                                .padding(horizontal = 24.dp)\n                                .fillMaxWidth(),\n                            text = stringResource(R.string.network_retry),\n                            onClick = { actions.onRefresh(false) },\n                        )\n                    }\n                } else {\n                    InfiniteProgressIndicator()\n                }\n            }\n        }\n        val pullToRefreshState = rememberPullToRefreshState()\n        val refreshTexts = listOf(\n            stringResource(R.string.refresh_pulling),\n            stringResource(R.string.refresh_release),\n            stringResource(R.string.refresh_refresh),\n            stringResource(R.string.refresh_complete),\n        )\n        val layoutDirection = LocalLayoutDirection.current\n        PullToRefresh(\n            isRefreshing = state.isRefreshing,\n            pullToRefreshState = pullToRefreshState,\n            onRefresh = { actions.onRefresh(true) },\n            refreshTexts = refreshTexts,\n            contentPadding = PaddingValues(\n                top = innerPadding.calculateTopPadding() + 12.dp,\n                start = innerPadding.calculateStartPadding(layoutDirection),\n                end = innerPadding.calculateEndPadding(layoutDirection)\n            ),\n        ) {\n            LazyColumn(\n                modifier = Modifier\n                    .fillMaxHeight()\n                    .scrollEndHaptic()\n                    .overScrollVertical()\n                    .nestedScroll(nestedScrollConnection)\n                    .nestedScroll(scrollBehavior.nestedScrollConnection)\n                    .let { if (enableBlur) it.hazeSource(state = hazeState) else it }\n                    .padding(horizontal = 12.dp),\n                contentPadding = innerPadding,\n                overscrollEffect = null\n            ) {\n                item {\n                    Spacer(Modifier.height(12.dp))\n                }\n                items(state.templateList, key = { it.id }) { app ->\n                    TemplateItem(\n                        template = app,\n                        onClick = { actions.onOpenTemplate(app) },\n                    )\n                }\n                item {\n                    Spacer(\n                        Modifier.height(\n                            WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                                    WindowInsets.captionBar.asPaddingValues().calculateBottomPadding()\n                        )\n                    )\n                }\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun TemplateItem(\n    template: TemplateInfo,\n    onClick: () -> Unit,\n) {\n    Card(\n        modifier = Modifier.padding(bottom = 12.dp),\n        onClick = onClick,\n        showIndication = true,\n        pressFeedbackType = PressFeedbackType.Sink\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(16.dp)\n        ) {\n            Row(\n                verticalAlignment = Alignment.CenterVertically,\n                modifier = Modifier.fillMaxWidth()\n            ) {\n                Text(\n                    text = template.name,\n                    fontWeight = FontWeight(550),\n                    color = colorScheme.onSurface,\n                )\n                Spacer(modifier = Modifier.weight(1f))\n                if (template.local) {\n                    Text(\n                        text = \"LOCAL\",\n                        color = colorScheme.onTertiaryContainer,\n                        fontWeight = FontWeight(750),\n                        style = MiuixTheme.textStyles.footnote1\n                    )\n                } else {\n                    Text(\n                        text = \"REMOTE\",\n                        color = colorScheme.onSurfaceSecondary,\n                        fontWeight = FontWeight(750),\n                        style = MiuixTheme.textStyles.footnote1\n                    )\n                }\n            }\n\n            Text(\n                text = \"${template.id}${if (template.author.isEmpty()) \"\" else \" by @${template.author}\"}\",\n                modifier = Modifier.padding(top = 1.dp),\n                fontSize = 12.sp,\n                fontWeight = FontWeight(550),\n                color = colorScheme.onSurfaceVariantSummary,\n            )\n\n            Spacer(modifier = Modifier.height(4.dp))\n\n            Text(\n                text = template.description,\n                fontSize = 14.sp,\n                color = colorScheme.onSurfaceVariantSummary,\n            )\n\n            HorizontalDivider(\n                modifier = Modifier.padding(vertical = 8.dp),\n                thickness = 0.5.dp,\n                color = colorScheme.outline.copy(alpha = 0.5f)\n            )\n\n            FlowRow(\n                horizontalArrangement = Arrangement.spacedBy(8.dp)\n            ) {\n                InfoChip(\n                    icon = Icons.Outlined.Fingerprint,\n                    text = \"UID: ${template.uid}\"\n                )\n                InfoChip(\n                    icon = Icons.Outlined.Group,\n                    text = \"GID: ${template.gid}\"\n                )\n                InfoChip(\n                    icon = Icons.Outlined.Shield,\n                    text = template.context\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun InfoChip(icon: ImageVector, text: String) {\n    Row(verticalAlignment = Alignment.CenterVertically) {\n        Icon(\n            imageVector = icon,\n            contentDescription = null,\n            modifier = Modifier.size(14.dp),\n            tint = colorScheme.onSurfaceSecondary.copy(alpha = 0.8f)\n        )\n        Text(\n            modifier = Modifier.padding(start = 4.dp),\n            text = text,\n            fontSize = 12.sp,\n            fontWeight = FontWeight(550),\n            color = colorScheme.onSurfaceSecondary\n        )\n    }\n}\n\n@Composable\nprivate fun TopBar(\n    onBack: () -> Unit,\n    onImport: () -> Unit = {},\n    onExport: () -> Unit = {},\n    scrollBehavior: ScrollBehavior,\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n    enableBlur: Boolean\n) {\n    TopAppBar(\n        modifier = if (enableBlur) {\n            Modifier.defaultHazeEffect(hazeState, hazeStyle)\n        } else {\n            Modifier\n        },\n        color = if (enableBlur) Color.Transparent else colorScheme.surface,\n        title = stringResource(R.string.settings_profile_template),\n        navigationIcon = {\n            IconButton(\n                modifier = Modifier.padding(start = 16.dp),\n                onClick = onBack\n            ) {\n                val layoutDirection = LocalLayoutDirection.current\n                Icon(\n                    modifier = Modifier.graphicsLayer {\n                        if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\n                    },\n                    imageVector = MiuixIcons.Back,\n                    contentDescription = null,\n                    tint = colorScheme.onBackground\n                )\n            }\n        },\n        actions = {\n            val showTopPopup = remember { mutableStateOf(false) }\n            SuperListPopup(\n                show = showTopPopup.value,\n                popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider,\n                alignment = PopupPositionProvider.Align.TopEnd,\n                onDismissRequest = {\n                    showTopPopup.value = false\n                },\n                content = {\n                    ListPopupColumn {\n                        val items = listOf(\n                            stringResource(id = R.string.app_profile_import_from_clipboard),\n                            stringResource(id = R.string.app_profile_export_to_clipboard)\n                        )\n                        items.forEachIndexed { index, text ->\n                            DropdownItem(\n                                text = text,\n                                optionSize = items.size,\n                                index = index,\n                                onSelectedIndexChange = { selectedIndex ->\n                                    if (selectedIndex == 0) {\n                                        onImport()\n                                    } else {\n                                        onExport()\n                                    }\n                                    showTopPopup.value = false\n                                }\n                            )\n                        }\n                    }\n                }\n            )\n            IconButton(\n                modifier = Modifier.padding(end = 16.dp),\n                onClick = { showTopPopup.value = true },\n                holdDownState = showTopPopup.value\n            ) {\n                Icon(\n                    imageVector = MiuixIcons.Copy,\n                    contentDescription = stringResource(id = R.string.app_profile_import_export),\n                    tint = colorScheme.onBackground\n                )\n            }\n        },\n        scrollBehavior = scrollBehavior\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/template/TemplateScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.template\n\nimport android.content.ClipData\nimport android.widget.Toast\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.platform.ClipEntry\nimport androidx.compose.ui.platform.LocalClipboard\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.compose.dropUnlessResumed\nimport androidx.lifecycle.viewmodel.compose.viewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.navigation3.Route\nimport me.weishu.kernelsu.ui.util.isNetworkAvailable\nimport me.weishu.kernelsu.ui.viewmodel.TemplateViewModel\n\n@Composable\nfun AppProfileTemplateScreen() {\n    val uiMode = LocalUiMode.current\n    val navigator = LocalNavigator.current\n    val viewModel = viewModel<TemplateViewModel>()\n    val screenState by viewModel.uiState.collectAsState()\n    val clipboard = LocalClipboard.current\n    val context = LocalContext.current\n    val scope = rememberCoroutineScope()\n    val requestKey = \"template_edit\"\n\n    LaunchedEffect(Unit) {\n        if (screenState.templateList.isEmpty()) {\n            viewModel.fetchTemplates()\n        }\n    }\n\n    LaunchedEffect(Unit) {\n        navigator.observeResult<Boolean>(requestKey).collect { success ->\n            if (success) {\n                if (uiMode == UiMode.Miuix) {\n                    navigator.clearResult(requestKey)\n                }\n                viewModel.fetchTemplates()\n            }\n        }\n    }\n\n    val importEmptyText = stringResource(R.string.app_profile_template_import_empty)\n    val importSuccessText = stringResource(R.string.app_profile_template_import_success)\n    val exportEmptyText = stringResource(R.string.app_profile_template_export_empty)\n\n    val showToast: (String) -> Unit = { message ->\n        scope.launch(Dispatchers.Main) {\n            Toast.makeText(context, message, Toast.LENGTH_SHORT).show()\n        }\n    }\n\n    val uiState = screenState.copy(offline = !isNetworkAvailable(context))\n    val actions = TemplateActions(\n        onBack = dropUnlessResumed { navigator.pop() },\n        onRefresh = { forceSync ->\n            scope.launch {\n                viewModel.fetchTemplates(forceSync)\n            }\n        },\n        onImport = {\n            scope.launch {\n                clipboard.getClipEntry()?.clipData?.getItemAt(0)?.text?.toString()?.let { templateText ->\n                    if (templateText.isEmpty()) {\n                        showToast(importEmptyText)\n                        return@let\n                    }\n                    viewModel.importTemplates(\n                        templateText,\n                        onSuccess = {\n                            showToast(importSuccessText)\n                            viewModel.fetchTemplates(false)\n                        },\n                        onFailure = showToast,\n                    )\n                }\n            }\n        },\n        onExport = {\n            scope.launch {\n                viewModel.exportTemplates(\n                    onTemplateEmpty = {\n                        showToast(exportEmptyText)\n                    },\n                    callback = { templateText ->\n                        clipboard.setClipEntry(\n                            ClipEntry(ClipData.newPlainText(\"template\", templateText))\n                        )\n                    },\n                )\n            }\n        },\n        onCreateTemplate = {\n            when (uiMode) {\n                UiMode.Miuix -> navigator.navigateForResult(\n                    Route.TemplateEditor(TemplateViewModel.TemplateInfo(), false),\n                    requestKey,\n                )\n\n                UiMode.Material -> navigator.push(\n                    Route.TemplateEditor(TemplateViewModel.TemplateInfo(), false)\n                )\n            }\n        },\n        onOpenTemplate = { template ->\n            when (uiMode) {\n                UiMode.Miuix -> navigator.navigateForResult(\n                    Route.TemplateEditor(template, !template.local),\n                    requestKey,\n                )\n\n                UiMode.Material -> navigator.push(\n                    Route.TemplateEditor(template, !template.local)\n                )\n            }\n        },\n    )\n\n    when (uiMode) {\n        UiMode.Miuix -> AppProfileTemplateScreenMiuix(\n            state = uiState,\n            actions = actions,\n        )\n\n        UiMode.Material -> AppProfileTemplateScreenMaterial(\n            state = uiState,\n            actions = actions,\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/template/TemplateUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.template\n\nimport androidx.compose.runtime.Immutable\nimport me.weishu.kernelsu.data.model.TemplateInfo\n\n@Immutable\ndata class TemplateUiState(\n    val isRefreshing: Boolean = false,\n    val offline: Boolean = false,\n    val templates: List<TemplateInfo> = emptyList(),\n    val templateList: List<TemplateInfo> = emptyList(),\n    val error: Throwable? = null,\n)\n\n@Immutable\ndata class TemplateActions(\n    val onBack: () -> Unit,\n    val onRefresh: (Boolean) -> Unit,\n    val onImport: () -> Unit,\n    val onExport: () -> Unit,\n    val onCreateTemplate: () -> Unit,\n    val onOpenTemplate: (TemplateInfo) -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/templateeditor/TemplateEditorMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.screen.templateeditor\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.imePadding\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.foundation.rememberScrollState\nimport androidx.compose.foundation.verticalScroll\nimport androidx.compose.material.icons.Icons\nimport androidx.compose.material.icons.automirrored.filled.ArrowBack\nimport androidx.compose.material.icons.filled.DeleteForever\nimport androidx.compose.material.icons.filled.Save\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.Icon\nimport androidx.compose.material3.IconButton\nimport androidx.compose.material3.LargeFlexibleTopAppBar\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Scaffold\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TopAppBarDefaults\nimport androidx.compose.material3.TopAppBarScrollBehavior\nimport androidx.compose.material3.rememberTopAppBarState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.ExperimentalComposeUiApi\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.material.SegmentedColumn\nimport me.weishu.kernelsu.ui.component.material.SegmentedTextField\nimport me.weishu.kernelsu.ui.component.profile.RootProfileConfig\n\n@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)\n@Composable\nfun TemplateEditorScreenMaterial(\n    state: TemplateEditorUiState,\n    actions: TemplateEditorActions,\n) {\n    val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState())\n\n    LaunchedEffect(Unit) {\n        scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit\n    }\n\n    Scaffold(\n        topBar = {\n            TopBar(\n                title = if (state.isCreation) {\n                    stringResource(R.string.app_profile_template_create)\n                } else if (state.readOnly) {\n                    stringResource(R.string.app_profile_template_view)\n                } else {\n                    stringResource(R.string.app_profile_template_edit)\n                },\n                readOnly = state.readOnly,\n                summary = state.titleSummary,\n                onBack = actions.onBack,\n                onDelete = actions.onDelete,\n                onSave = actions.onSave,\n                scrollBehavior = scrollBehavior\n            )\n        },\n        contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        Column(\n            modifier = Modifier\n                .padding(innerPadding)\n                .imePadding()\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .verticalScroll(rememberScrollState())\n        ) {\n            SegmentedColumn(\n                modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),\n                content = buildList {\n                    add(\n                        {\n                            TemplateEditorListItem(\n                                label = stringResource(id = R.string.app_profile_template_name),\n                                value = state.template.name,\n                                readOnly = state.readOnly,\n                                onValueChange = actions.onNameChange,\n                            )\n                        }\n                    )\n                    if (state.isCreation) {\n                        add(\n                            {\n                                TemplateEditorListItem(\n                                    label = stringResource(id = R.string.app_profile_template_id),\n                                    value = state.template.id,\n                                    errorHint = state.idErrorHint,\n                                    isError = state.idErrorHint.isNotEmpty(),\n                                    readOnly = state.readOnly,\n                                    onValueChange = actions.onIdChange,\n                                )\n                            }\n                        )\n                    }\n                    add(\n                        {\n                            TemplateEditorListItem(\n                                label = stringResource(id = R.string.module_author),\n                                value = state.template.author,\n                                readOnly = state.readOnly,\n                                onValueChange = actions.onAuthorChange,\n                            )\n                        }\n                    )\n                    add(\n                        {\n                            TemplateEditorListItem(\n                                label = stringResource(id = R.string.app_profile_template_description),\n                                value = state.template.description,\n                                multiline = true,\n                                readOnly = state.readOnly,\n                                onValueChange = actions.onDescriptionChange,\n                            )\n                        }\n                    )\n                }\n            )\n\n            RootProfileConfig(\n                fixedName = true,\n                enabled = !state.readOnly,\n                profile = toNativeProfile(state.template),\n                onProfileChange = actions.onProfileChange,\n            )\n\n            Spacer(\n                Modifier.height(\n                    WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()\n                )\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun TemplateEditorListItem(\n    label: String,\n    value: String,\n    errorHint: String = \"\",\n    isError: Boolean = false,\n    multiline: Boolean = false,\n    readOnly: Boolean = false,\n    onValueChange: (String) -> Unit\n) {\n    SegmentedTextField(\n        value = value,\n        onValueChange = onValueChange,\n        label = label,\n        supportingContent = if (isError && errorHint.isNotEmpty()) {\n            { Text(errorHint, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelSmall) }\n        } else null,\n        isError = isError,\n        singleLine = !multiline,\n        minLines = 1,\n        maxLines = if (multiline) 100 else 1,\n        readOnly = readOnly\n    )\n}\n\n@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun TopBar(\n    title: String,\n    readOnly: Boolean,\n    summary: String = \"\",\n    onBack: () -> Unit,\n    onDelete: () -> Unit = {},\n    onSave: () -> Unit = {},\n    scrollBehavior: TopAppBarScrollBehavior? = null\n) {\n    LargeFlexibleTopAppBar(\n        title = {\n            Column {\n                Text(title)\n                if (summary.isNotBlank()) {\n                    Text(\n                        text = summary,\n                        style = MaterialTheme.typography.bodyMedium,\n                    )\n                }\n            }\n        },\n        navigationIcon = {\n            IconButton(onClick = onBack) {\n                Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)\n            }\n        },\n        actions = {\n            if (readOnly) return@LargeFlexibleTopAppBar\n            IconButton(onClick = onDelete) {\n                Icon(\n                    Icons.Filled.DeleteForever,\n                    contentDescription = stringResource(id = R.string.app_profile_template_delete)\n                )\n            }\n            IconButton(onClick = onSave) {\n                Icon(\n                    imageVector = Icons.Filled.Save,\n                    contentDescription = stringResource(id = R.string.app_profile_template_save)\n                )\n            }\n        },\n        colors = TopAppBarDefaults.topAppBarColors(\n            containerColor = MaterialTheme.colorScheme.surface,\n            scrolledContainerColor = MaterialTheme.colorScheme.surface\n        ),\n        windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),\n        scrollBehavior = scrollBehavior\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/templateeditor/TemplateEditorMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.screen.templateeditor\n\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.WindowInsetsSides\nimport androidx.compose.foundation.layout.add\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.captionBar\nimport androidx.compose.foundation.layout.displayCutout\nimport androidx.compose.foundation.layout.fillMaxHeight\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.navigationBars\nimport androidx.compose.foundation.layout.only\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.systemBars\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.text.KeyboardOptions\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.graphicsLayer\nimport androidx.compose.ui.input.nestedscroll.nestedScroll\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.input.KeyboardType\nimport androidx.compose.ui.unit.LayoutDirection\nimport androidx.compose.ui.unit.dp\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.HazeTint\nimport dev.chrisbanes.haze.hazeSource\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.component.miuix.EditText\nimport me.weishu.kernelsu.ui.component.profile.RootProfileConfig\nimport me.weishu.kernelsu.ui.theme.LocalEnableBlur\nimport me.weishu.kernelsu.ui.util.defaultHazeEffect\nimport top.yukonga.miuix.kmp.basic.Card\nimport top.yukonga.miuix.kmp.basic.Icon\nimport top.yukonga.miuix.kmp.basic.IconButton\nimport top.yukonga.miuix.kmp.basic.MiuixScrollBehavior\nimport top.yukonga.miuix.kmp.basic.Scaffold\nimport top.yukonga.miuix.kmp.basic.ScrollBehavior\nimport top.yukonga.miuix.kmp.basic.TopAppBar\nimport top.yukonga.miuix.kmp.icon.MiuixIcons\nimport top.yukonga.miuix.kmp.icon.extended.Back\nimport top.yukonga.miuix.kmp.icon.extended.Delete\nimport top.yukonga.miuix.kmp.icon.extended.Ok\nimport top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme\nimport top.yukonga.miuix.kmp.utils.overScrollVertical\nimport top.yukonga.miuix.kmp.utils.scrollEndHaptic\n\n/**\n * @author weishu\n * @date 2023/10/20.\n */\n@Composable\nfun TemplateEditorScreenMiuix(\n    state: TemplateEditorUiState,\n    actions: TemplateEditorActions,\n) {\n    val scrollBehavior = MiuixScrollBehavior()\n    val enableBlur = LocalEnableBlur.current\n    val hazeState = remember { HazeState() }\n    val hazeStyle = if (enableBlur) {\n        HazeStyle(\n            backgroundColor = colorScheme.surface,\n            tint = HazeTint(colorScheme.surface.copy(0.8f))\n        )\n    } else {\n        HazeStyle.Unspecified\n    }\n\n    Scaffold(\n        topBar = {\n            TopBar(\n                title = if (state.isCreation) {\n                    stringResource(R.string.app_profile_template_create)\n                } else if (state.readOnly) {\n                    stringResource(R.string.app_profile_template_view)\n                } else {\n                    stringResource(R.string.app_profile_template_edit)\n                },\n                readOnly = state.readOnly,\n                isCreation = state.isCreation,\n                onBack = actions.onBack,\n                onDelete = actions.onDelete,\n                onSave = actions.onSave,\n                scrollBehavior = scrollBehavior,\n                hazeState = hazeState,\n                hazeStyle = hazeStyle,\n                enableBlur = enableBlur,\n            )\n        },\n        popupHost = { },\n        contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)\n    ) { innerPadding ->\n        LazyColumn(\n            modifier = Modifier\n                .fillMaxHeight()\n                .scrollEndHaptic()\n                .overScrollVertical()\n                .nestedScroll(scrollBehavior.nestedScrollConnection)\n                .let { if (enableBlur) it.hazeSource(state = hazeState) else it },\n            contentPadding = innerPadding,\n            overscrollEffect = null\n        ) {\n            item {\n                Card(\n                    modifier = Modifier\n                        .fillMaxWidth()\n                        .padding(12.dp),\n                ) {\n                    TextEdit(\n                        label = stringResource(id = R.string.app_profile_template_name),\n                        text = state.template.name,\n                        enabled = !state.readOnly,\n                        onValueChange = actions.onNameChange,\n                    )\n\n                    TextEdit(\n                        label = stringResource(id = R.string.app_profile_template_id),\n                        text = state.template.id,\n                        isError = state.idErrorHint.isNotEmpty(),\n                        enabled = !state.readOnly,\n                        onValueChange = actions.onIdChange,\n                    )\n                    TextEdit(\n                        label = stringResource(R.string.module_author),\n                        text = state.template.author,\n                        enabled = !state.readOnly,\n                        onValueChange = actions.onAuthorChange,\n                    )\n\n                    TextEdit(\n                        label = stringResource(id = R.string.app_profile_template_description),\n                        text = state.template.description,\n                        enabled = !state.readOnly,\n                        onValueChange = actions.onDescriptionChange,\n                    )\n\n                    RootProfileConfig(\n                        fixedName = true,\n                        enabled = !state.readOnly,\n                        profile = toNativeProfile(state.template),\n                        onProfileChange = actions.onProfileChange,\n                    )\n                }\n                Spacer(\n                    Modifier.height(\n                        WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() +\n                                WindowInsets.captionBar.asPaddingValues().calculateBottomPadding()\n                    )\n                )\n            }\n        }\n    }\n}\n\n\n@Composable\nprivate fun TopBar(\n    title: String,\n    readOnly: Boolean,\n    isCreation: Boolean,\n    onBack: () -> Unit,\n    onDelete: () -> Unit = {},\n    onSave: () -> Unit = {},\n    scrollBehavior: ScrollBehavior,\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n    enableBlur: Boolean\n) {\n    TopAppBar(\n        modifier = if (enableBlur) {\n            Modifier.defaultHazeEffect(hazeState, hazeStyle)\n        } else {\n            Modifier\n        },\n        color = if (enableBlur) Color.Transparent else colorScheme.surface,\n        title = title,\n        navigationIcon = {\n            IconButton(\n                modifier = Modifier.padding(start = 16.dp),\n                onClick = onBack\n            ) {\n                val layoutDirection = LocalLayoutDirection.current\n                Icon(\n                    modifier = Modifier.graphicsLayer {\n                        if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f\n                    },\n                    imageVector = MiuixIcons.Back,\n                    contentDescription = null,\n                    tint = colorScheme.onSurface\n                )\n            }\n        },\n        actions = {\n            when {\n                !readOnly && !isCreation -> {\n                    IconButton(\n                        modifier = Modifier.padding(end = 16.dp),\n                        onClick = onDelete\n                    ) {\n                        Icon(\n                            imageVector = MiuixIcons.Delete,\n                            contentDescription = stringResource(id = R.string.app_profile_template_delete),\n                            tint = colorScheme.onBackground\n                        )\n                    }\n                }\n\n                isCreation -> {\n                    IconButton(\n                        modifier = Modifier.padding(end = 16.dp),\n                        onClick = onSave\n                    ) {\n                        Icon(\n                            imageVector = MiuixIcons.Ok,\n                            contentDescription = stringResource(id = R.string.app_profile_template_save),\n                            tint = colorScheme.onBackground\n                        )\n                    }\n                }\n            }\n        },\n        scrollBehavior = scrollBehavior\n    )\n}\n\n@Composable\nprivate fun TextEdit(\n    label: String,\n    text: String,\n    isError: Boolean = false,\n    enabled: Boolean = true,\n    onValueChange: (String) -> Unit = {}\n) {\n    val editText = remember(text) { mutableStateOf(text) }\n    EditText(\n        title = label.uppercase(),\n        textValue = editText,\n        onTextValueChange = { newText ->\n            editText.value = newText\n            onValueChange(newText)\n        },\n        keyboardOptions = KeyboardOptions(\n            keyboardType = KeyboardType.Ascii,\n        ),\n        isError = isError,\n        enabled = enabled,\n    )\n}\n\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/templateeditor/TemplateEditorScreen.kt",
    "content": "package me.weishu.kernelsu.ui.screen.templateeditor\n\nimport android.widget.Toast\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.saveable.rememberSaveable\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.lifecycle.compose.dropUnlessResumed\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.navigation3.LocalNavigator\nimport me.weishu.kernelsu.ui.util.deleteAppProfileTemplate\nimport me.weishu.kernelsu.ui.viewmodel.TemplateViewModel\n\n@Composable\nfun TemplateEditorScreen(template: TemplateViewModel.TemplateInfo, readOnly: Boolean) {\n    val navigator = LocalNavigator.current\n    val context = LocalContext.current\n    val uiMode = LocalUiMode.current\n    val isCreation = template.id.isBlank()\n    val autoSave = uiMode == UiMode.Miuix && !isCreation\n\n    var currentTemplate by rememberSaveable { mutableStateOf(template) }\n    var idErrorHint by remember { mutableStateOf(\"\") }\n\n    val saveTemplateFailed = stringResource(id = R.string.app_profile_template_save_failed)\n    val idConflictError = stringResource(id = R.string.app_profile_template_id_exist)\n    val idInvalidError = stringResource(id = R.string.app_profile_template_id_invalid)\n\n    fun showToast(message: String) {\n        Toast.makeText(context, message, Toast.LENGTH_SHORT).show()\n    }\n\n    fun finishEditing() {\n        if (!readOnly) {\n            navigator.setResult(\"template_edit\", true)\n        } else {\n            navigator.pop()\n        }\n    }\n\n    fun updateTemplate(updatedTemplate: TemplateViewModel.TemplateInfo) {\n        if (autoSave) {\n            if (!saveTemplate(updatedTemplate)) {\n                return\n            }\n        }\n        currentTemplate = updatedTemplate\n    }\n\n    val uiState = TemplateEditorUiState(\n        template = currentTemplate,\n        initialTemplate = template,\n        readOnly = readOnly,\n        isCreation = isCreation,\n        idErrorHint = idErrorHint,\n    )\n\n    fun saveCurrentTemplate() {\n        if (uiMode == UiMode.Miuix) {\n            when (idCheck(currentTemplate.id)) {\n                1 -> {\n                    showToast(idConflictError)\n                    return\n                }\n\n                2 -> {\n                    showToast(idInvalidError)\n                    return\n                }\n            }\n        }\n\n        if (saveTemplate(currentTemplate, isCreation)) {\n            navigator.setResult(\"template_edit\", true)\n        } else {\n            showToast(saveTemplateFailed)\n        }\n    }\n\n    val actions = TemplateEditorActions(\n        onBack = dropUnlessResumed { finishEditing() },\n        onDelete = {\n            if (deleteAppProfileTemplate(currentTemplate.id)) {\n                navigator.setResult(\"template_edit\", true)\n            }\n        },\n        onSave = ::saveCurrentTemplate,\n        onNameChange = { value ->\n            updateTemplate(currentTemplate.copy(name = value))\n        },\n        onIdChange = { value ->\n            idErrorHint = if (isTemplateExist(value)) {\n                idConflictError\n            } else if (!isValidTemplateId(value)) {\n                idInvalidError\n            } else {\n                \"\"\n            }\n            currentTemplate = currentTemplate.copy(id = value)\n        },\n        onAuthorChange = { value ->\n            updateTemplate(currentTemplate.copy(author = value))\n        },\n        onDescriptionChange = { value ->\n            updateTemplate(currentTemplate.copy(description = value))\n        },\n        onProfileChange = { profile ->\n            updateTemplate(\n                currentTemplate.copy(\n                    uid = profile.uid,\n                    gid = profile.gid,\n                    groups = profile.groups,\n                    capabilities = profile.capabilities,\n                    context = profile.context,\n                    namespace = profile.namespace,\n                    rules = profile.rules.split(\"\\n\"),\n                )\n            )\n        },\n    )\n\n    when (uiMode) {\n        UiMode.Miuix -> TemplateEditorScreenMiuix(\n            state = uiState,\n            actions = actions,\n        )\n\n        UiMode.Material -> TemplateEditorScreenMaterial(\n            state = uiState,\n            actions = actions,\n        )\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/templateeditor/TemplateEditorUiState.kt",
    "content": "package me.weishu.kernelsu.ui.screen.templateeditor\n\nimport androidx.compose.runtime.Immutable\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.data.model.TemplateInfo\n\n@Immutable\ndata class TemplateEditorUiState(\n    val template: TemplateInfo,\n    val initialTemplate: TemplateInfo,\n    val readOnly: Boolean,\n    val isCreation: Boolean,\n    val idErrorHint: String = \"\",\n) {\n    val titleSummary: String\n        get() = initialTemplate.id + if (initialTemplate.author.isNotEmpty()) \"@${initialTemplate.author}\" else \"\"\n}\n\n@Immutable\ndata class TemplateEditorActions(\n    val onBack: () -> Unit,\n    val onDelete: () -> Unit,\n    val onSave: () -> Unit,\n    val onNameChange: (String) -> Unit,\n    val onIdChange: (String) -> Unit,\n    val onAuthorChange: (String) -> Unit,\n    val onDescriptionChange: (String) -> Unit,\n    val onProfileChange: (Natives.Profile) -> Unit,\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/screen/templateeditor/TemplateEditorUtils.kt",
    "content": "package me.weishu.kernelsu.ui.screen.templateeditor\n\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.data.model.TemplateInfo\nimport me.weishu.kernelsu.ui.util.getAppProfileTemplate\nimport me.weishu.kernelsu.ui.util.setAppProfileTemplate\n\nfun toNativeProfile(templateInfo: TemplateInfo): Natives.Profile {\n    return Natives.Profile().copy(\n        rootTemplate = templateInfo.id,\n        uid = templateInfo.uid,\n        gid = templateInfo.gid,\n        groups = templateInfo.groups,\n        capabilities = templateInfo.capabilities,\n        context = templateInfo.context,\n        namespace = templateInfo.namespace,\n        rules = templateInfo.rules.joinToString(\"\\n\").ifBlank { \"\" }\n    )\n}\n\nfun isTemplateValid(template: TemplateInfo): Boolean {\n    if (template.id.isBlank()) {\n        return false\n    }\n    if (!isValidTemplateId(template.id)) {\n        return false\n    }\n    return true\n}\n\nfun idCheck(value: String): Int {\n    return if (value.isEmpty()) 0 else if (isTemplateExist(value)) 1 else if (!isValidTemplateId(value)) 2 else 0\n}\n\nfun saveTemplate(template: TemplateInfo, isCreation: Boolean = false): Boolean {\n    if (!isTemplateValid(template)) {\n        return false\n    }\n    if (isCreation && isTemplateExist(template.id)) {\n        return false\n    }\n    val json = template.toJSON()\n    json.put(\"local\", true)\n    return setAppProfileTemplate(template.id, json.toString())\n}\n\nfun isValidTemplateId(id: String): Boolean {\n    return Regex(\"\"\"^([A-Za-z][A-Za-z\\d_]*\\.)*[A-Za-z][A-Za-z\\d_]*$\"\"\").matches(id)\n}\n\nfun isTemplateExist(id: String): Boolean {\n    return getAppProfileTemplate(id).isNotBlank()\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Colors.kt",
    "content": "package me.weishu.kernelsu.ui.theme\n\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.graphics.toArgb\n\nval keyColorOptions = listOf(\n    Color(0xFFF44336).toArgb(),\n    Color(0xFFE91E63).toArgb(),\n    Color(0xFF9C27B0).toArgb(),\n    Color(0xFF673AB7).toArgb(),\n    Color(0xFF3F51B5).toArgb(),\n    Color(0xFF2196F3).toArgb(),\n    Color(0xFF00BCD4).toArgb(),\n    Color(0xFF009688).toArgb(),\n    Color(0xFF4FAF50).toArgb(),\n    Color(0xFFFFEB3B).toArgb(),\n    Color(0xFFFFC107).toArgb(),\n    Color(0xFFFF9800).toArgb(),\n    Color(0xFF795548).toArgb(),\n    Color(0xFF607D8F).toArgb(),\n    Color(0xFFFF9CA8).toArgb(),\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/theme/MaterialTheme.kt",
    "content": "package me.weishu.kernelsu.ui.theme\n\nimport android.app.Activity\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.MaterialExpressiveTheme\nimport androidx.compose.material3.MotionScheme\nimport androidx.compose.material3.dynamicDarkColorScheme\nimport androidx.compose.material3.dynamicLightColorScheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.core.view.WindowInsetsControllerCompat\nimport com.materialkolor.rememberDynamicColorScheme\nimport me.weishu.kernelsu.ui.webui.MonetColorsProvider\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nfun MaterialKernelSUTheme(\n    appSettings: AppSettings,\n    content: @Composable () -> Unit\n) {\n    val context = LocalContext.current\n    val systemDarkTheme = isSystemInDarkTheme()\n    val darkTheme = appSettings.colorMode.isDark || (appSettings.colorMode.isSystem && systemDarkTheme)\n    val amoledMode = appSettings.colorMode.isAmoled\n    val dynamicColor = appSettings.keyColor == 0\n    val colorStyle = appSettings.paletteStyle\n    val colorSpec = appSettings.colorSpec\n\n    val colorScheme = if (dynamicColor) {\n        val baseScheme = if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)\n        rememberDynamicColorScheme(\n            seedColor = Color.Unspecified,\n            isDark = darkTheme,\n            isAmoled = amoledMode,\n            style = colorStyle,\n            specVersion = colorSpec,\n            primary = baseScheme.primary,\n            secondary = baseScheme.secondary,\n            tertiary = baseScheme.tertiary,\n            neutral = baseScheme.surface,\n            neutralVariant = baseScheme.surfaceVariant,\n            error = baseScheme.error\n        )\n    } else {\n        rememberDynamicColorScheme(\n            seedColor = Color(appSettings.keyColor),\n            isDark = darkTheme,\n            isAmoled = amoledMode,\n            style = colorStyle,\n            specVersion = colorSpec,\n        )\n    }\n\n    LaunchedEffect(darkTheme) {\n        val window = (context as? Activity)?.window ?: return@LaunchedEffect\n        WindowInsetsControllerCompat(window, window.decorView).apply {\n            isAppearanceLightStatusBars = !darkTheme\n            isAppearanceLightNavigationBars = !darkTheme\n        }\n    }\n\n    MaterialExpressiveTheme(\n        colorScheme = colorScheme,\n        motionScheme = MotionScheme.expressive(),\n        content = {\n            MonetColorsProvider.UpdateCss()\n            content()\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/theme/MiuixTheme.kt",
    "content": "package me.weishu.kernelsu.ui.theme\n\nimport android.app.Activity\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.core.view.WindowInsetsControllerCompat\nimport com.materialkolor.dynamiccolor.ColorSpec\nimport me.weishu.kernelsu.ui.webui.MonetColorsProvider\nimport top.yukonga.miuix.kmp.theme.ColorSchemeMode\nimport top.yukonga.miuix.kmp.theme.LocalContentColor\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\nimport top.yukonga.miuix.kmp.theme.ThemeColorSpec\nimport top.yukonga.miuix.kmp.theme.ThemeController\nimport top.yukonga.miuix.kmp.theme.ThemePaletteStyle\n\n@Composable\nfun MiuixKernelSUTheme(\n    appSettings: AppSettings,\n    content: @Composable () -> Unit\n) {\n    val context = LocalContext.current\n    val systemDarkTheme = isSystemInDarkTheme()\n    val darkTheme = appSettings.colorMode.isDark || (appSettings.colorMode.isSystem && systemDarkTheme)\n    val colorStyle = appSettings.paletteStyle\n    val colorSpec = appSettings.colorSpec\n\n    val miuixPaletteStyle = try {\n        ThemePaletteStyle.valueOf(colorStyle.name)\n    } catch (_: Exception) {\n        ThemePaletteStyle.TonalSpot\n    }\n\n    val miuixColorSpec = if (colorSpec == ColorSpec.SpecVersion.SPEC_2025) {\n        ThemeColorSpec.Spec2025\n    } else {\n        ThemeColorSpec.Spec2021\n    }\n\n    val controller = ThemeController(\n        when (appSettings.colorMode) {\n            ColorMode.SYSTEM -> ColorSchemeMode.System\n            ColorMode.LIGHT -> ColorSchemeMode.Light\n            ColorMode.DARK -> ColorSchemeMode.Dark\n            ColorMode.MONET_SYSTEM -> ColorSchemeMode.MonetSystem\n            ColorMode.MONET_LIGHT -> ColorSchemeMode.MonetLight\n            ColorMode.MONET_DARK, ColorMode.DARK_AMOLED -> ColorSchemeMode.MonetDark\n        },\n        keyColor = if (appSettings.keyColor == 0) null else Color(appSettings.keyColor),\n        isDark = darkTheme,\n        paletteStyle = miuixPaletteStyle,\n        colorSpec = miuixColorSpec,\n    )\n\n    MiuixTheme(\n        controller = controller,\n        content = {\n            LaunchedEffect(darkTheme) {\n                val window = (context as? Activity)?.window ?: return@LaunchedEffect\n                WindowInsetsControllerCompat(window, window.decorView).apply {\n                    isAppearanceLightStatusBars = !darkTheme\n                    isAppearanceLightNavigationBars = !darkTheme\n                }\n            }\n            MonetColorsProvider.UpdateCss()\n            CompositionLocalProvider(\n                LocalContentColor provides MiuixTheme.colorScheme.onBackground,\n            ) {\n                content()\n            }\n        }\n    )\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Theme.kt",
    "content": "package me.weishu.kernelsu.ui.theme\n\nimport android.content.Context\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.ReadOnlyComposable\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.platform.LocalContext\nimport com.materialkolor.PaletteStyle\nimport com.materialkolor.dynamiccolor.ColorSpec\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\n\nenum class ColorMode(val value: Int) {\n    SYSTEM(0),\n    LIGHT(1),\n    DARK(2),\n    MONET_SYSTEM(3),\n    MONET_LIGHT(4),\n    MONET_DARK(5),\n    DARK_AMOLED(6);\n\n    companion object {\n        fun fromValue(value: Int) = entries.find { it.value == value } ?: SYSTEM\n    }\n\n    val isSystem: Boolean get() = value == 0 || value == 3\n    val isDark: Boolean get() = value == 2 || value == 5 || value == 6\n    val isAmoled: Boolean get() = value == 6\n    val isMonet: Boolean get() = value >= 3\n\n    fun toNonMonetMode(): Int = when (this) {\n        MONET_SYSTEM -> 0\n        MONET_LIGHT -> 1\n        MONET_DARK, DARK_AMOLED -> 2\n        else -> value\n    }\n\n    fun toMonetMode(): Int = when (this) {\n        SYSTEM -> 3\n        LIGHT -> 4\n        DARK -> 5\n        else -> value\n    }\n}\n\ndata class AppSettings(\n    val colorMode: ColorMode,\n    val keyColor: Int,\n    val paletteStyle: PaletteStyle,\n    val colorSpec: ColorSpec.SpecVersion,\n)\n\nobject ThemeController {\n    fun getAppSettings(context: Context): AppSettings {\n        val prefs = context.getSharedPreferences(\"settings\", Context.MODE_PRIVATE)\n        val uiMode = prefs.getString(\"ui_mode\", UiMode.DEFAULT_VALUE) ?: UiMode.DEFAULT_VALUE\n        var colorModeValue = prefs.getInt(\"color_mode\", ColorMode.SYSTEM.value)\n\n        if (uiMode == \"miuix\") {\n            val miuixMonet = prefs.getBoolean(\"miuix_monet\", false)\n            val colorMode = ColorMode.fromValue(colorModeValue)\n            colorModeValue = if (!miuixMonet && colorMode.isMonet) {\n                colorMode.toNonMonetMode()\n            } else if (miuixMonet && !colorMode.isMonet) {\n                colorMode.toMonetMode()\n            } else {\n                colorModeValue\n            }\n        }\n\n        val colorMode = ColorMode.fromValue(colorModeValue)\n        val keyColor = prefs.getInt(\"key_color\", 0)\n        val paletteStyleStr = prefs.getString(\"color_style\", PaletteStyle.TonalSpot.name)\n        val paletteStyle = try {\n            PaletteStyle.valueOf(paletteStyleStr!!)\n        } catch (_: Exception) {\n            PaletteStyle.TonalSpot\n        }\n        val colorSpecStr = prefs.getString(\"color_spec\", ColorSpec.SpecVersion.Default.name)\n        val colorSpec = try {\n            ColorSpec.SpecVersion.valueOf(colorSpecStr!!)\n        } catch (_: Exception) {\n            ColorSpec.SpecVersion.Default\n        }\n\n        return AppSettings(colorMode, keyColor, paletteStyle, colorSpec)\n    }\n}\n\n@Composable\nfun KernelSUTheme(\n    appSettings: AppSettings? = null,\n    uiMode: UiMode = LocalUiMode.current,\n    content: @Composable () -> Unit\n) {\n    val context = LocalContext.current\n    val currentAppSettings = appSettings ?: ThemeController.getAppSettings(context)\n\n    when (uiMode) {\n        UiMode.Miuix -> MiuixKernelSUTheme(\n            appSettings = currentAppSettings,\n            content = content\n        )\n\n        UiMode.Material -> MaterialKernelSUTheme(\n            appSettings = currentAppSettings,\n            content = content\n        )\n    }\n}\n\n@Composable\n@ReadOnlyComposable\nfun isInDarkTheme(): Boolean {\n    return when (LocalColorMode.current) {\n        1, 4 -> false  // Force light mode\n        2, 5, 6 -> true   // Force dark mode\n        else -> isSystemInDarkTheme()  // Follow system (0 or default)\n    }\n}\n\n\nval LocalColorMode = staticCompositionLocalOf { 0 }\n\nval LocalEnableBlur = staticCompositionLocalOf { false }\n\nval LocalEnableFloatingBottomBar = staticCompositionLocalOf { false }\n\nval LocalEnableFloatingBottomBarBlur = staticCompositionLocalOf { false }\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/AppIconCache.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport android.content.Context\nimport android.content.pm.ApplicationInfo\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.graphics.Canvas\nimport android.graphics.drawable.BitmapDrawable\nimport android.graphics.drawable.Drawable\nimport android.os.UserHandle\nimport android.util.LruCache\nimport androidx.core.graphics.createBitmap\nimport androidx.core.graphics.drawable.toDrawable\nimport androidx.core.graphics.scale\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\n\nobject AppIconCache {\n    private val maxMemory = Runtime.getRuntime().maxMemory() / 1024\n    private val cacheSize = (maxMemory / 8).toInt()\n\n    private val lruCache = object : LruCache<String, Bitmap>(cacheSize) {\n        override fun sizeOf(key: String, value: Bitmap): Int {\n            return value.byteCount / 1024\n        }\n    }\n\n    suspend fun loadIcon(context: Context, applicationInfo: ApplicationInfo, size: Int): Bitmap {\n        val key = \"${applicationInfo.packageName}:${applicationInfo.uid}\"\n\n        synchronized(lruCache) {\n            val cachedBitmap = lruCache.get(key)\n            if (cachedBitmap != null) return cachedBitmap\n        }\n\n        return withContext(Dispatchers.IO) {\n            val pm = context.packageManager\n            var finalDrawable: Drawable? = null\n\n            try {\n                val appRes = pm.getResourcesForApplication(applicationInfo)\n                val iconId = applicationInfo.icon\n\n                if (iconId != 0) {\n                    val options = BitmapFactory.Options().apply {\n                        inJustDecodeBounds = true\n                    }\n                    BitmapFactory.decodeResource(appRes, iconId, options)\n\n                    if (options.outWidth > size * 6 || options.outHeight > size * 6) {\n                        options.inSampleSize = calculateInSampleSize(options, size, size)\n                        options.inJustDecodeBounds = false\n\n                        val scaledBitmap = BitmapFactory.decodeResource(appRes, iconId, options)\n                        if (scaledBitmap != null) {\n                            finalDrawable = scaledBitmap.toDrawable(context.resources)\n                        }\n                    }\n                }\n            } catch (_: Exception) {\n                // Ignore\n            }\n\n            if (finalDrawable == null) {\n                finalDrawable = applicationInfo.loadUnbadgedIcon(pm)\n            }\n\n            // Add system badges\n            val handle = UserHandle.getUserHandleForUid(applicationInfo.uid)\n            val badgedDrawable = try {\n                pm.getUserBadgedIcon(finalDrawable, handle)\n            } catch (_: Exception) {\n                finalDrawable\n            }\n\n            // Convert to Bitmap for caching\n            val bitmap = if (badgedDrawable is BitmapDrawable) {\n                badgedDrawable.bitmap\n            } else {\n                val w = if (badgedDrawable.intrinsicWidth > 0) badgedDrawable.intrinsicWidth else size\n                val h = if (badgedDrawable.intrinsicHeight > 0) badgedDrawable.intrinsicHeight else size\n                val bmp = createBitmap(w, h)\n                val canvas = Canvas(bmp)\n                badgedDrawable.setBounds(0, 0, canvas.width, canvas.height)\n                badgedDrawable.draw(canvas)\n                bmp\n            }\n\n            // Resize if too large (consistent with original logic)\n            val resultBitmap = if (bitmap.width > size * 2 || bitmap.height > size * 2) {\n                val scaled = bitmap.scale(size, size)\n                scaled\n            } else {\n                bitmap\n            }\n\n            synchronized(lruCache) {\n                lruCache.put(key, resultBitmap)\n            }\n\n            resultBitmap\n        }\n    }\n\n    private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {\n        val height: Int = options.outHeight\n        val width: Int = options.outWidth\n        var inSampleSize = 1\n\n        if (height > reqHeight || width > reqWidth) {\n            while ((height / (inSampleSize * 2)) >= reqHeight && (width / (inSampleSize * 2)) >= reqWidth) {\n                inSampleSize *= 2\n            }\n        }\n        return inSampleSize\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/Colors.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport kotlin.math.pow\n\nfun cssColorFromArgb(argb: Int): String {\n    val a = ((argb ushr 24) and 0xFF) / 255f\n    val r = (argb ushr 16) and 0xFF\n    val g = (argb ushr 8) and 0xFF\n    val b = argb and 0xFF\n    return \"rgba(${r},${g},${b},${\"%.3f\".format(a)})\"\n}\n\nfun mixArgb(c1: Int, c2: Int, ratio: Float): Int {\n    val r1 = (c1 ushr 16) and 0xFF\n    val g1 = (c1 ushr 8) and 0xFF\n    val b1 = c1 and 0xFF\n    val a1 = (c1 ushr 24) and 0xFF\n\n    val r2 = (c2 ushr 16) and 0xFF\n    val g2 = (c2 ushr 8) and 0xFF\n    val b2 = c2 and 0xFF\n    val a2 = (c2 ushr 24) and 0xFF\n\n    val r = (r1 * (1 - ratio) + r2 * ratio).toInt().coerceIn(0, 255)\n    val g = (g1 * (1 - ratio) + g2 * ratio).toInt().coerceIn(0, 255)\n    val b = (b1 * (1 - ratio) + b2 * ratio).toInt().coerceIn(0, 255)\n    val a = (a1 * (1 - ratio) + a2 * ratio).toInt().coerceIn(0, 255)\n\n    return (a shl 24) or (r shl 16) or (g shl 8) or b\n}\n\nfun relativeLuminance(argb: Int): Double {\n    fun linearize(c: Int): Double {\n        val s = c / 255.0\n        return if (s <= 0.03928) s / 12.92 else ((s + 0.055) / 1.055).pow(2.4)\n    }\n\n    val r = linearize((argb ushr 16) and 0xFF)\n    val g = linearize((argb ushr 8) and 0xFF)\n    val b = linearize(argb and 0xFF)\n    return 0.2126 * r + 0.7152 * g + 0.0722 * b\n}\n\nfun contrastRatio(a: Int, b: Int): Double {\n    val l1 = relativeLuminance(a)\n    val l2 = relativeLuminance(b)\n    val (hi, lo) = if (l1 >= l2) Pair(l1, l2) else Pair(l2, l1)\n    return (hi + 0.05) / (lo + 0.05)\n}\n\nfun argbToHsl(argb: Int): Triple<Float, Float, Float> {\n    val r = ((argb ushr 16) and 0xFF) / 255f\n    val g = ((argb ushr 8) and 0xFF) / 255f\n    val b = (argb and 0xFF) / 255f\n    val max = maxOf(r, g, b)\n    val min = minOf(r, g, b)\n    val l = (max + min) / 2f\n    val d = max - min\n    val s = if (d == 0f) 0f else d / (1f - kotlin.math.abs(2f * l - 1f))\n    val h = when {\n        d == 0f -> 0f\n        max == r -> ((g - b) / d % 6f) * 60f\n        max == g -> ((b - r) / d + 2f) * 60f\n        else -> ((r - g) / d + 4f) * 60f\n    }.let { if (it < 0f) it + 360f else it }\n    return Triple(h, s, l)\n}\n\nfun hslToArgb(h: Float, s: Float, l: Float, alpha: Int = 0xFF): Int {\n    val c = (1f - kotlin.math.abs(2f * l - 1f)) * s\n    val x = c * (1f - kotlin.math.abs((h / 60f) % 2f - 1f))\n    val m = l - c / 2f\n    val (r1, g1, b1) = when {\n        h < 60f -> Triple(c, x, 0f)\n        h < 120f -> Triple(x, c, 0f)\n        h < 180f -> Triple(0f, c, x)\n        h < 240f -> Triple(0f, x, c)\n        h < 300f -> Triple(x, 0f, c)\n        else -> Triple(c, 0f, x)\n    }\n    val r = ((r1 + m) * 255f).toInt().coerceIn(0, 255)\n    val g = ((g1 + m) * 255f).toInt().coerceIn(0, 255)\n    val b = ((b1 + m) * 255f).toInt().coerceIn(0, 255)\n    return (alpha shl 24) or (r shl 16) or (g shl 8) or b\n}\n\nfun adjustLightnessArgb(argb: Int, delta: Float): Int {\n    val (h, s, l) = argbToHsl(argb)\n    val nl = (l + delta).coerceIn(0f, 1f)\n    val alpha = (argb ushr 24) and 0xFF\n    return hslToArgb(h, s, nl, alpha)\n}\n\nfun ensureVisibleByMix(original: Int, candidate: Int, minRatio: Double, mixWithWhiteIfLighter: Boolean): Int {\n    if (contrastRatio(original, candidate) >= minRatio) return candidate\n    val target = if (mixWithWhiteIfLighter) 0xFFFFFFFF.toInt() else 0xFF000000.toInt()\n    var lo = 0f\n    var hi = 1f\n    var best = candidate\n    for (i in 0 until 12) {\n        val mid = (lo + hi) / 2f\n        val mixed = mixArgb(candidate, target, mid)\n        if (contrastRatio(original, mixed) >= minRatio) {\n            best = mixed\n            hi = mid\n        } else {\n            lo = mid\n        }\n    }\n    return best\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport androidx.compose.material3.SnackbarHostState\nimport androidx.compose.runtime.compositionLocalOf\n\nval LocalSnackbarHost = compositionLocalOf<SnackbarHostState> {\n    error(\"CompositionLocal LocalSnackbarHost not present\")\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/Downloader.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport android.annotation.SuppressLint\nimport android.net.Uri\nimport android.os.Environment\nimport android.os.Handler\nimport android.os.Looper\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.util.module.LatestVersionInfo\nimport okhttp3.Request\nimport java.io.File\nimport java.io.FileOutputStream\nimport java.io.IOException\n\n/**\n * @author weishu\n * @date 2023/6/22.\n */\n@SuppressLint(\"Range\")\nfun download(\n    url: String,\n    fileName: String,\n    onDownloaded: (Uri) -> Unit = {},\n    onDownloading: () -> Unit = {},\n    onProgress: (Int) -> Unit = {}\n) {\n    onDownloading()\n    Thread {\n        val target = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName)\n        try {\n            ksuApp.okhttpClient.newCall(Request.Builder().url(url).build()).execute().use { resp ->\n                if (!resp.isSuccessful) throw IOException(\"HTTP ${resp.code}\")\n                val body = resp.body\n                val total = body.contentLength()\n                target.parentFile?.mkdirs()\n                FileOutputStream(target).use { fos ->\n                    val buf = ByteArray(8 * 1024)\n                    var read: Int\n                    var soFar = 0L\n                    val source = body.byteStream()\n                    while (true) {\n                        read = source.read(buf)\n                        if (read == -1) break\n                        fos.write(buf, 0, read)\n                        soFar += read\n                        if (total > 0) {\n                            val percent = ((soFar * 100L) / total).toInt().coerceIn(0, 100)\n                            onProgress(percent)\n                        }\n                    }\n                    fos.flush()\n                }\n            }\n            Handler(Looper.getMainLooper()).post {\n                onDownloaded(Uri.fromFile(target))\n            }\n        } catch (_: Exception) {\n            // ignore, keep UI state\n        }\n    }.start()\n}\n\nfun checkNewVersion(): LatestVersionInfo {\n    if (!isNetworkAvailable(ksuApp)) return LatestVersionInfo()\n    val url = \"https://api.github.com/repos/tiann/KernelSU/releases/latest\"\n    // default null value if failed\n    val defaultValue = LatestVersionInfo()\n    runCatching {\n        ksuApp.okhttpClient.newCall(Request.Builder().url(url).build()).execute()\n            .use { response ->\n                if (!response.isSuccessful) {\n                    return defaultValue\n                }\n                val body = response.body.string()\n                val json = org.json.JSONObject(body)\n                val changelog = json.optString(\"body\")\n\n                val assets = json.getJSONArray(\"assets\")\n                for (i in 0 until assets.length()) {\n                    val asset = assets.getJSONObject(i)\n                    val name = asset.getString(\"name\")\n                    if (!name.endsWith(\".apk\")) {\n                        continue\n                    }\n\n                    val regex = Regex(\"v(.+?)_(\\\\d+)-\")\n                    val matchResult = regex.find(name) ?: continue\n                    matchResult.groupValues[1]\n                    val versionCode = matchResult.groupValues[2].toInt()\n                    val downloadUrl = asset.getString(\"browser_download_url\")\n\n                    return LatestVersionInfo(\n                        versionCode,\n                        downloadUrl,\n                        changelog\n                    )\n                }\n\n            }\n    }\n    return defaultValue\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/HanziToPinyin.java",
    "content": "package me.weishu.kernelsu.ui.util;\n/*\n * Copyright (C) 2009 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport android.text.TextUtils;\nimport android.util.Log;\n\nimport java.text.Collator;\nimport java.util.ArrayList;\nimport java.util.Locale;\n\n/**\n * An object to convert Chinese character to its corresponding pinyin string. For characters with\n * multiple possible pinyin string, only one is selected according to collator. Polyphone is not\n * supported in this implementation. This class is implemented to achieve the best runtime\n * performance and minimum runtime resources with tolerable sacrifice of accuracy. This\n * implementation highly depends on zh_CN ICU collation data and must be always synchronized with\n * ICU.\n * <p>\n * Currently this file is aligned to zh.txt in ICU 4.6\n */\npublic class HanziToPinyin {\n    /**\n     * Unihans array.\n     * <p>\n     * Each unihans is the first one within same pinyin when collator is zh_CN.\n     */\n    public static final char[] UNIHANS = {\n            '\\u963f', '\\u54ce', '\\u5b89', '\\u80ae', '\\u51f9', '\\u516b',\n            '\\u6300', '\\u6273', '\\u90a6', '\\u52f9', '\\u9642', '\\u5954',\n            '\\u4f3b', '\\u5c44', '\\u8fb9', '\\u706c', '\\u618b', '\\u6c43',\n            '\\u51ab', '\\u7676', '\\u5cec', '\\u5693', '\\u5072', '\\u53c2',\n            '\\u4ed3', '\\u64a1', '\\u518a', '\\u5d7e', '\\u66fd', '\\u66fe',\n            '\\u5c64', '\\u53c9', '\\u8286', '\\u8fbf', '\\u4f25', '\\u6284',\n            '\\u8f66', '\\u62bb', '\\u6c88', '\\u6c89', '\\u9637', '\\u5403',\n            '\\u5145', '\\u62bd', '\\u51fa', '\\u6b3b', '\\u63e3', '\\u5ddb',\n            '\\u5205', '\\u5439', '\\u65fe', '\\u9034', '\\u5472', '\\u5306',\n            '\\u51d1', '\\u7c97', '\\u6c46', '\\u5d14', '\\u90a8', '\\u6413',\n            '\\u5491', '\\u5446', '\\u4e39', '\\u5f53', '\\u5200', '\\u561a',\n            '\\u6265', '\\u706f', '\\u6c10', '\\u55f2', '\\u7538', '\\u5201',\n            '\\u7239', '\\u4e01', '\\u4e1f', '\\u4e1c', '\\u543a', '\\u53be',\n            '\\u8011', '\\u8968', '\\u5428', '\\u591a', '\\u59b8', '\\u8bf6',\n            '\\u5940', '\\u97a5', '\\u513f', '\\u53d1', '\\u5e06', '\\u531a',\n            '\\u98de', '\\u5206', '\\u4e30', '\\u8985', '\\u4ecf', '\\u7d11',\n            '\\u4f15', '\\u65ee', '\\u4f85', '\\u7518', '\\u5188', '\\u768b',\n            '\\u6208', '\\u7ed9', '\\u6839', '\\u522f', '\\u5de5', '\\u52fe',\n            '\\u4f30', '\\u74dc', '\\u4e56', '\\u5173', '\\u5149', '\\u5f52',\n            '\\u4e28', '\\u5459', '\\u54c8', '\\u548d', '\\u4f44', '\\u592f',\n            '\\u8320', '\\u8bc3', '\\u9ed2', '\\u62eb', '\\u4ea8', '\\u5677',\n            '\\u53ff', '\\u9f41', '\\u4e6f', '\\u82b1', '\\u6000', '\\u72bf',\n            '\\u5ddf', '\\u7070', '\\u660f', '\\u5419', '\\u4e0c', '\\u52a0',\n            '\\u620b', '\\u6c5f', '\\u827d', '\\u9636', '\\u5dfe', '\\u5755',\n            '\\u5182', '\\u4e29', '\\u51e5', '\\u59e2', '\\u5658', '\\u519b',\n            '\\u5494', '\\u5f00', '\\u520a', '\\u5ffc', '\\u5c3b', '\\u533c',\n            '\\u808e', '\\u52a5', '\\u7a7a', '\\u62a0', '\\u625d', '\\u5938',\n            '\\u84af', '\\u5bbd', '\\u5321', '\\u4e8f', '\\u5764', '\\u6269',\n            '\\u5783', '\\u6765', '\\u5170', '\\u5577', '\\u635e', '\\u808b',\n            '\\u52d2', '\\u5d1a', '\\u5215', '\\u4fe9', '\\u5941', '\\u826f',\n            '\\u64a9', '\\u5217', '\\u62ce', '\\u5222', '\\u6e9c', '\\u56d6',\n            '\\u9f99', '\\u779c', '\\u565c', '\\u5a08', '\\u7567', '\\u62a1',\n            '\\u7f57', '\\u5463', '\\u5988', '\\u57cb', '\\u5ada', '\\u7264',\n            '\\u732b', '\\u4e48', '\\u5445', '\\u95e8', '\\u753f', '\\u54aa',\n            '\\u5b80', '\\u55b5', '\\u4e5c', '\\u6c11', '\\u540d', '\\u8c2c',\n            '\\u6478', '\\u54de', '\\u6bea', '\\u55ef', '\\u62cf', '\\u8149',\n            '\\u56e1', '\\u56d4', '\\u5b6c', '\\u7592', '\\u5a1e', '\\u6041',\n            '\\u80fd', '\\u59ae', '\\u62c8', '\\u5b22', '\\u9e1f', '\\u634f',\n            '\\u56dc', '\\u5b81', '\\u599e', '\\u519c', '\\u7fba', '\\u5974',\n            '\\u597b', '\\u759f', '\\u9ec1', '\\u90cd', '\\u5594', '\\u8bb4',\n            '\\u5991', '\\u62cd', '\\u7705', '\\u4e53', '\\u629b', '\\u5478',\n            '\\u55b7', '\\u5309', '\\u4e15', '\\u56e8', '\\u527d', '\\u6c15',\n            '\\u59d8', '\\u4e52', '\\u948b', '\\u5256', '\\u4ec6', '\\u4e03',\n            '\\u6390', '\\u5343', '\\u545b', '\\u6084', '\\u767f', '\\u4eb2',\n            '\\u72c5', '\\u828e', '\\u4e18', '\\u533a', '\\u5cd1', '\\u7f3a',\n            '\\u590b', '\\u5465', '\\u7a63', '\\u5a06', '\\u60f9', '\\u4eba',\n            '\\u6254', '\\u65e5', '\\u8338', '\\u53b9', '\\u909a', '\\u633c',\n            '\\u5827', '\\u5a51', '\\u77a4', '\\u637c', '\\u4ee8', '\\u6be2',\n            '\\u4e09', '\\u6852', '\\u63bb', '\\u95aa', '\\u68ee', '\\u50e7',\n            '\\u6740', '\\u7b5b', '\\u5c71', '\\u4f24', '\\u5f30', '\\u5962',\n            '\\u7533', '\\u8398', '\\u6552', '\\u5347', '\\u5c38', '\\u53ce',\n            '\\u4e66', '\\u5237', '\\u8870', '\\u95e9', '\\u53cc', '\\u8c01',\n            '\\u542e', '\\u8bf4', '\\u53b6', '\\u5fea', '\\u635c', '\\u82cf',\n            '\\u72fb', '\\u590a', '\\u5b59', '\\u5506', '\\u4ed6', '\\u56fc',\n            '\\u574d', '\\u6c64', '\\u5932', '\\u5fd1', '\\u71a5', '\\u5254',\n            '\\u5929', '\\u65eb', '\\u5e16', '\\u5385', '\\u56f2', '\\u5077',\n            '\\u51f8', '\\u6e4d', '\\u63a8', '\\u541e', '\\u4e47', '\\u7a75',\n            '\\u6b6a', '\\u5f2f', '\\u5c23', '\\u5371', '\\u6637', '\\u7fc1',\n            '\\u631d', '\\u4e4c', '\\u5915', '\\u8672', '\\u4eda', '\\u4e61',\n            '\\u7071', '\\u4e9b', '\\u5fc3', '\\u661f', '\\u51f6', '\\u4f11',\n            '\\u5401', '\\u5405', '\\u524a', '\\u5743', '\\u4e2b', '\\u6079',\n            '\\u592e', '\\u5e7a', '\\u503b', '\\u4e00', '\\u56d9', '\\u5e94',\n            '\\u54df', '\\u4f63', '\\u4f18', '\\u625c', '\\u56e6', '\\u66f0',\n            '\\u6655', '\\u7b60', '\\u7b7c', '\\u5e00', '\\u707d', '\\u5142',\n            '\\u5328', '\\u50ae', '\\u5219', '\\u8d3c', '\\u600e', '\\u5897',\n            '\\u624e', '\\u635a', '\\u6cbe', '\\u5f20', '\\u957f', '\\u9577',\n            '\\u4f4b', '\\u8707', '\\u8d1e', '\\u4e89', '\\u4e4b', '\\u5cd9',\n            '\\u5ea2', '\\u4e2d', '\\u5dde', '\\u6731', '\\u6293', '\\u62fd',\n            '\\u4e13', '\\u5986', '\\u96b9', '\\u5b92', '\\u5353', '\\u4e72',\n            '\\u5b97', '\\u90b9', '\\u79df', '\\u94bb', '\\u539c', '\\u5c0a',\n            '\\u6628', '\\u5159', '\\u9fc3', '\\u9fc4',};\n    /**\n     * Pinyin array.\n     * <p>\n     * Each pinyin is corresponding to unihans of same\n     * offset in the unihans array.\n     */\n    public static final byte[][] PINYINS = {\n            {65, 0, 0, 0, 0, 0}, {65, 73, 0, 0, 0, 0},\n            {65, 78, 0, 0, 0, 0}, {65, 78, 71, 0, 0, 0},\n            {65, 79, 0, 0, 0, 0}, {66, 65, 0, 0, 0, 0},\n            {66, 65, 73, 0, 0, 0}, {66, 65, 78, 0, 0, 0},\n            {66, 65, 78, 71, 0, 0}, {66, 65, 79, 0, 0, 0},\n            {66, 69, 73, 0, 0, 0}, {66, 69, 78, 0, 0, 0},\n            {66, 69, 78, 71, 0, 0}, {66, 73, 0, 0, 0, 0},\n            {66, 73, 65, 78, 0, 0}, {66, 73, 65, 79, 0, 0},\n            {66, 73, 69, 0, 0, 0}, {66, 73, 78, 0, 0, 0},\n            {66, 73, 78, 71, 0, 0}, {66, 79, 0, 0, 0, 0},\n            {66, 85, 0, 0, 0, 0}, {67, 65, 0, 0, 0, 0},\n            {67, 65, 73, 0, 0, 0}, {67, 65, 78, 0, 0, 0},\n            {67, 65, 78, 71, 0, 0}, {67, 65, 79, 0, 0, 0},\n            {67, 69, 0, 0, 0, 0}, {67, 69, 78, 0, 0, 0},\n            {67, 69, 78, 71, 0, 0}, {90, 69, 78, 71, 0, 0},\n            {67, 69, 78, 71, 0, 0}, {67, 72, 65, 0, 0, 0},\n            {67, 72, 65, 73, 0, 0}, {67, 72, 65, 78, 0, 0},\n            {67, 72, 65, 78, 71, 0}, {67, 72, 65, 79, 0, 0},\n            {67, 72, 69, 0, 0, 0}, {67, 72, 69, 78, 0, 0},\n            {83, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 0, 0},\n            {67, 72, 69, 78, 71, 0}, {67, 72, 73, 0, 0, 0},\n            {67, 72, 79, 78, 71, 0}, {67, 72, 79, 85, 0, 0},\n            {67, 72, 85, 0, 0, 0}, {67, 72, 85, 65, 0, 0},\n            {67, 72, 85, 65, 73, 0}, {67, 72, 85, 65, 78, 0},\n            {67, 72, 85, 65, 78, 71}, {67, 72, 85, 73, 0, 0},\n            {67, 72, 85, 78, 0, 0}, {67, 72, 85, 79, 0, 0},\n            {67, 73, 0, 0, 0, 0}, {67, 79, 78, 71, 0, 0},\n            {67, 79, 85, 0, 0, 0}, {67, 85, 0, 0, 0, 0},\n            {67, 85, 65, 78, 0, 0}, {67, 85, 73, 0, 0, 0},\n            {67, 85, 78, 0, 0, 0}, {67, 85, 79, 0, 0, 0},\n            {68, 65, 0, 0, 0, 0}, {68, 65, 73, 0, 0, 0},\n            {68, 65, 78, 0, 0, 0}, {68, 65, 78, 71, 0, 0},\n            {68, 65, 79, 0, 0, 0}, {68, 69, 0, 0, 0, 0},\n            {68, 69, 78, 0, 0, 0}, {68, 69, 78, 71, 0, 0},\n            {68, 73, 0, 0, 0, 0}, {68, 73, 65, 0, 0, 0},\n            {68, 73, 65, 78, 0, 0}, {68, 73, 65, 79, 0, 0},\n            {68, 73, 69, 0, 0, 0}, {68, 73, 78, 71, 0, 0},\n            {68, 73, 85, 0, 0, 0}, {68, 79, 78, 71, 0, 0},\n            {68, 79, 85, 0, 0, 0}, {68, 85, 0, 0, 0, 0},\n            {68, 85, 65, 78, 0, 0}, {68, 85, 73, 0, 0, 0},\n            {68, 85, 78, 0, 0, 0}, {68, 85, 79, 0, 0, 0},\n            {69, 0, 0, 0, 0, 0}, {69, 73, 0, 0, 0, 0},\n            {69, 78, 0, 0, 0, 0}, {69, 78, 71, 0, 0, 0},\n            {69, 82, 0, 0, 0, 0}, {70, 65, 0, 0, 0, 0},\n            {70, 65, 78, 0, 0, 0}, {70, 65, 78, 71, 0, 0},\n            {70, 69, 73, 0, 0, 0}, {70, 69, 78, 0, 0, 0},\n            {70, 69, 78, 71, 0, 0}, {70, 73, 65, 79, 0, 0},\n            {70, 79, 0, 0, 0, 0}, {70, 79, 85, 0, 0, 0},\n            {70, 85, 0, 0, 0, 0}, {71, 65, 0, 0, 0, 0},\n            {71, 65, 73, 0, 0, 0}, {71, 65, 78, 0, 0, 0},\n            {71, 65, 78, 71, 0, 0}, {71, 65, 79, 0, 0, 0},\n            {71, 69, 0, 0, 0, 0}, {71, 69, 73, 0, 0, 0},\n            {71, 69, 78, 0, 0, 0}, {71, 69, 78, 71, 0, 0},\n            {71, 79, 78, 71, 0, 0}, {71, 79, 85, 0, 0, 0},\n            {71, 85, 0, 0, 0, 0}, {71, 85, 65, 0, 0, 0},\n            {71, 85, 65, 73, 0, 0}, {71, 85, 65, 78, 0, 0},\n            {71, 85, 65, 78, 71, 0}, {71, 85, 73, 0, 0, 0},\n            {71, 85, 78, 0, 0, 0}, {71, 85, 79, 0, 0, 0},\n            {72, 65, 0, 0, 0, 0}, {72, 65, 73, 0, 0, 0},\n            {72, 65, 78, 0, 0, 0}, {72, 65, 78, 71, 0, 0},\n            {72, 65, 79, 0, 0, 0}, {72, 69, 0, 0, 0, 0},\n            {72, 69, 73, 0, 0, 0}, {72, 69, 78, 0, 0, 0},\n            {72, 69, 78, 71, 0, 0}, {72, 77, 0, 0, 0, 0},\n            {72, 79, 78, 71, 0, 0}, {72, 79, 85, 0, 0, 0},\n            {72, 85, 0, 0, 0, 0}, {72, 85, 65, 0, 0, 0},\n            {72, 85, 65, 73, 0, 0}, {72, 85, 65, 78, 0, 0},\n            {72, 85, 65, 78, 71, 0}, {72, 85, 73, 0, 0, 0},\n            {72, 85, 78, 0, 0, 0}, {72, 85, 79, 0, 0, 0},\n            {74, 73, 0, 0, 0, 0}, {74, 73, 65, 0, 0, 0},\n            {74, 73, 65, 78, 0, 0}, {74, 73, 65, 78, 71, 0},\n            {74, 73, 65, 79, 0, 0}, {74, 73, 69, 0, 0, 0},\n            {74, 73, 78, 0, 0, 0}, {74, 73, 78, 71, 0, 0},\n            {74, 73, 79, 78, 71, 0}, {74, 73, 85, 0, 0, 0},\n            {74, 85, 0, 0, 0, 0}, {74, 85, 65, 78, 0, 0},\n            {74, 85, 69, 0, 0, 0}, {74, 85, 78, 0, 0, 0},\n            {75, 65, 0, 0, 0, 0}, {75, 65, 73, 0, 0, 0},\n            {75, 65, 78, 0, 0, 0}, {75, 65, 78, 71, 0, 0},\n            {75, 65, 79, 0, 0, 0}, {75, 69, 0, 0, 0, 0},\n            {75, 69, 78, 0, 0, 0}, {75, 69, 78, 71, 0, 0},\n            {75, 79, 78, 71, 0, 0}, {75, 79, 85, 0, 0, 0},\n            {75, 85, 0, 0, 0, 0}, {75, 85, 65, 0, 0, 0},\n            {75, 85, 65, 73, 0, 0}, {75, 85, 65, 78, 0, 0},\n            {75, 85, 65, 78, 71, 0}, {75, 85, 73, 0, 0, 0},\n            {75, 85, 78, 0, 0, 0}, {75, 85, 79, 0, 0, 0},\n            {76, 65, 0, 0, 0, 0}, {76, 65, 73, 0, 0, 0},\n            {76, 65, 78, 0, 0, 0}, {76, 65, 78, 71, 0, 0},\n            {76, 65, 79, 0, 0, 0}, {76, 69, 0, 0, 0, 0},\n            {76, 69, 73, 0, 0, 0}, {76, 69, 78, 71, 0, 0},\n            {76, 73, 0, 0, 0, 0}, {76, 73, 65, 0, 0, 0},\n            {76, 73, 65, 78, 0, 0}, {76, 73, 65, 78, 71, 0},\n            {76, 73, 65, 79, 0, 0}, {76, 73, 69, 0, 0, 0},\n            {76, 73, 78, 0, 0, 0}, {76, 73, 78, 71, 0, 0},\n            {76, 73, 85, 0, 0, 0}, {76, 79, 0, 0, 0, 0},\n            {76, 79, 78, 71, 0, 0}, {76, 79, 85, 0, 0, 0},\n            {76, 85, 0, 0, 0, 0}, {76, 85, 65, 78, 0, 0},\n            {76, 85, 69, 0, 0, 0}, {76, 85, 78, 0, 0, 0},\n            {76, 85, 79, 0, 0, 0}, {77, 0, 0, 0, 0, 0},\n            {77, 65, 0, 0, 0, 0}, {77, 65, 73, 0, 0, 0},\n            {77, 65, 78, 0, 0, 0}, {77, 65, 78, 71, 0, 0},\n            {77, 65, 79, 0, 0, 0}, {77, 69, 0, 0, 0, 0},\n            {77, 69, 73, 0, 0, 0}, {77, 69, 78, 0, 0, 0},\n            {77, 69, 78, 71, 0, 0}, {77, 73, 0, 0, 0, 0},\n            {77, 73, 65, 78, 0, 0}, {77, 73, 65, 79, 0, 0},\n            {77, 73, 69, 0, 0, 0}, {77, 73, 78, 0, 0, 0},\n            {77, 73, 78, 71, 0, 0}, {77, 73, 85, 0, 0, 0},\n            {77, 79, 0, 0, 0, 0}, {77, 79, 85, 0, 0, 0},\n            {77, 85, 0, 0, 0, 0}, {78, 0, 0, 0, 0, 0},\n            {78, 65, 0, 0, 0, 0}, {78, 65, 73, 0, 0, 0},\n            {78, 65, 78, 0, 0, 0}, {78, 65, 78, 71, 0, 0},\n            {78, 65, 79, 0, 0, 0}, {78, 69, 0, 0, 0, 0},\n            {78, 69, 73, 0, 0, 0}, {78, 69, 78, 0, 0, 0},\n            {78, 69, 78, 71, 0, 0}, {78, 73, 0, 0, 0, 0},\n            {78, 73, 65, 78, 0, 0}, {78, 73, 65, 78, 71, 0},\n            {78, 73, 65, 79, 0, 0}, {78, 73, 69, 0, 0, 0},\n            {78, 73, 78, 0, 0, 0}, {78, 73, 78, 71, 0, 0},\n            {78, 73, 85, 0, 0, 0}, {78, 79, 78, 71, 0, 0},\n            {78, 79, 85, 0, 0, 0}, {78, 85, 0, 0, 0, 0},\n            {78, 85, 65, 78, 0, 0}, {78, 85, 69, 0, 0, 0},\n            {78, 85, 78, 0, 0, 0}, {78, 85, 79, 0, 0, 0},\n            {79, 0, 0, 0, 0, 0}, {79, 85, 0, 0, 0, 0},\n            {80, 65, 0, 0, 0, 0}, {80, 65, 73, 0, 0, 0},\n            {80, 65, 78, 0, 0, 0}, {80, 65, 78, 71, 0, 0},\n            {80, 65, 79, 0, 0, 0}, {80, 69, 73, 0, 0, 0},\n            {80, 69, 78, 0, 0, 0}, {80, 69, 78, 71, 0, 0},\n            {80, 73, 0, 0, 0, 0}, {80, 73, 65, 78, 0, 0},\n            {80, 73, 65, 79, 0, 0}, {80, 73, 69, 0, 0, 0},\n            {80, 73, 78, 0, 0, 0}, {80, 73, 78, 71, 0, 0},\n            {80, 79, 0, 0, 0, 0}, {80, 79, 85, 0, 0, 0},\n            {80, 85, 0, 0, 0, 0}, {81, 73, 0, 0, 0, 0},\n            {81, 73, 65, 0, 0, 0}, {81, 73, 65, 78, 0, 0},\n            {81, 73, 65, 78, 71, 0}, {81, 73, 65, 79, 0, 0},\n            {81, 73, 69, 0, 0, 0}, {81, 73, 78, 0, 0, 0},\n            {81, 73, 78, 71, 0, 0}, {81, 73, 79, 78, 71, 0},\n            {81, 73, 85, 0, 0, 0}, {81, 85, 0, 0, 0, 0},\n            {81, 85, 65, 78, 0, 0}, {81, 85, 69, 0, 0, 0},\n            {81, 85, 78, 0, 0, 0}, {82, 65, 78, 0, 0, 0},\n            {82, 65, 78, 71, 0, 0}, {82, 65, 79, 0, 0, 0},\n            {82, 69, 0, 0, 0, 0}, {82, 69, 78, 0, 0, 0},\n            {82, 69, 78, 71, 0, 0}, {82, 73, 0, 0, 0, 0},\n            {82, 79, 78, 71, 0, 0}, {82, 79, 85, 0, 0, 0},\n            {82, 85, 0, 0, 0, 0}, {82, 85, 65, 0, 0, 0},\n            {82, 85, 65, 78, 0, 0}, {82, 85, 73, 0, 0, 0},\n            {82, 85, 78, 0, 0, 0}, {82, 85, 79, 0, 0, 0},\n            {83, 65, 0, 0, 0, 0}, {83, 65, 73, 0, 0, 0},\n            {83, 65, 78, 0, 0, 0}, {83, 65, 78, 71, 0, 0},\n            {83, 65, 79, 0, 0, 0}, {83, 69, 0, 0, 0, 0},\n            {83, 69, 78, 0, 0, 0}, {83, 69, 78, 71, 0, 0},\n            {83, 72, 65, 0, 0, 0}, {83, 72, 65, 73, 0, 0},\n            {83, 72, 65, 78, 0, 0}, {83, 72, 65, 78, 71, 0},\n            {83, 72, 65, 79, 0, 0}, {83, 72, 69, 0, 0, 0},\n            {83, 72, 69, 78, 0, 0}, {88, 73, 78, 0, 0, 0},\n            {83, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 71, 0},\n            {83, 72, 73, 0, 0, 0}, {83, 72, 79, 85, 0, 0},\n            {83, 72, 85, 0, 0, 0}, {83, 72, 85, 65, 0, 0},\n            {83, 72, 85, 65, 73, 0}, {83, 72, 85, 65, 78, 0},\n            {83, 72, 85, 65, 78, 71}, {83, 72, 85, 73, 0, 0},\n            {83, 72, 85, 78, 0, 0}, {83, 72, 85, 79, 0, 0},\n            {83, 73, 0, 0, 0, 0}, {83, 79, 78, 71, 0, 0},\n            {83, 79, 85, 0, 0, 0}, {83, 85, 0, 0, 0, 0},\n            {83, 85, 65, 78, 0, 0}, {83, 85, 73, 0, 0, 0},\n            {83, 85, 78, 0, 0, 0}, {83, 85, 79, 0, 0, 0},\n            {84, 65, 0, 0, 0, 0}, {84, 65, 73, 0, 0, 0},\n            {84, 65, 78, 0, 0, 0}, {84, 65, 78, 71, 0, 0},\n            {84, 65, 79, 0, 0, 0}, {84, 69, 0, 0, 0, 0},\n            {84, 69, 78, 71, 0, 0}, {84, 73, 0, 0, 0, 0},\n            {84, 73, 65, 78, 0, 0}, {84, 73, 65, 79, 0, 0},\n            {84, 73, 69, 0, 0, 0}, {84, 73, 78, 71, 0, 0},\n            {84, 79, 78, 71, 0, 0}, {84, 79, 85, 0, 0, 0},\n            {84, 85, 0, 0, 0, 0}, {84, 85, 65, 78, 0, 0},\n            {84, 85, 73, 0, 0, 0}, {84, 85, 78, 0, 0, 0},\n            {84, 85, 79, 0, 0, 0}, {87, 65, 0, 0, 0, 0},\n            {87, 65, 73, 0, 0, 0}, {87, 65, 78, 0, 0, 0},\n            {87, 65, 78, 71, 0, 0}, {87, 69, 73, 0, 0, 0},\n            {87, 69, 78, 0, 0, 0}, {87, 69, 78, 71, 0, 0},\n            {87, 79, 0, 0, 0, 0}, {87, 85, 0, 0, 0, 0},\n            {88, 73, 0, 0, 0, 0}, {88, 73, 65, 0, 0, 0},\n            {88, 73, 65, 78, 0, 0}, {88, 73, 65, 78, 71, 0},\n            {88, 73, 65, 79, 0, 0}, {88, 73, 69, 0, 0, 0},\n            {88, 73, 78, 0, 0, 0}, {88, 73, 78, 71, 0, 0},\n            {88, 73, 79, 78, 71, 0}, {88, 73, 85, 0, 0, 0},\n            {88, 85, 0, 0, 0, 0}, {88, 85, 65, 78, 0, 0},\n            {88, 85, 69, 0, 0, 0}, {88, 85, 78, 0, 0, 0},\n            {89, 65, 0, 0, 0, 0}, {89, 65, 78, 0, 0, 0},\n            {89, 65, 78, 71, 0, 0}, {89, 65, 79, 0, 0, 0},\n            {89, 69, 0, 0, 0, 0}, {89, 73, 0, 0, 0, 0},\n            {89, 73, 78, 0, 0, 0}, {89, 73, 78, 71, 0, 0},\n            {89, 79, 0, 0, 0, 0}, {89, 79, 78, 71, 0, 0},\n            {89, 79, 85, 0, 0, 0}, {89, 85, 0, 0, 0, 0},\n            {89, 85, 65, 78, 0, 0}, {89, 85, 69, 0, 0, 0},\n            {89, 85, 78, 0, 0, 0}, {74, 85, 78, 0, 0, 0},\n            {89, 85, 78, 0, 0, 0}, {90, 65, 0, 0, 0, 0},\n            {90, 65, 73, 0, 0, 0}, {90, 65, 78, 0, 0, 0},\n            {90, 65, 78, 71, 0, 0}, {90, 65, 79, 0, 0, 0},\n            {90, 69, 0, 0, 0, 0}, {90, 69, 73, 0, 0, 0},\n            {90, 69, 78, 0, 0, 0}, {90, 69, 78, 71, 0, 0},\n            {90, 72, 65, 0, 0, 0}, {90, 72, 65, 73, 0, 0},\n            {90, 72, 65, 78, 0, 0}, {90, 72, 65, 78, 71, 0},\n            {67, 72, 65, 78, 71, 0}, {90, 72, 65, 78, 71, 0},\n            {90, 72, 65, 79, 0, 0}, {90, 72, 69, 0, 0, 0},\n            {90, 72, 69, 78, 0, 0}, {90, 72, 69, 78, 71, 0},\n            {90, 72, 73, 0, 0, 0}, {83, 72, 73, 0, 0, 0},\n            {90, 72, 73, 0, 0, 0}, {90, 72, 79, 78, 71, 0},\n            {90, 72, 79, 85, 0, 0}, {90, 72, 85, 0, 0, 0},\n            {90, 72, 85, 65, 0, 0}, {90, 72, 85, 65, 73, 0},\n            {90, 72, 85, 65, 78, 0}, {90, 72, 85, 65, 78, 71},\n            {90, 72, 85, 73, 0, 0}, {90, 72, 85, 78, 0, 0},\n            {90, 72, 85, 79, 0, 0}, {90, 73, 0, 0, 0, 0},\n            {90, 79, 78, 71, 0, 0}, {90, 79, 85, 0, 0, 0},\n            {90, 85, 0, 0, 0, 0}, {90, 85, 65, 78, 0, 0},\n            {90, 85, 73, 0, 0, 0}, {90, 85, 78, 0, 0, 0},\n            {90, 85, 79, 0, 0, 0}, {0, 0, 0, 0, 0, 0},\n            {83, 72, 65, 78, 0, 0}, {0, 0, 0, 0, 0, 0},};\n    private static final String TAG = \"HanziToPinyin\";\n    // Turn on this flag when we want to check internal data structure.\n    private static final boolean DEBUG = false;\n    /**\n     * First and last Chinese character with known Pinyin according to zh collation\n     */\n    private static final String FIRST_PINYIN_UNIHAN = \"\\u963F\";\n    private static final String LAST_PINYIN_UNIHAN = \"\\u9FFF\";\n\n    private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA);\n\n    private static HanziToPinyin sInstance;\n    private final boolean mHasChinaCollator;\n\n    protected HanziToPinyin(boolean hasChinaCollator) {\n        mHasChinaCollator = hasChinaCollator;\n    }\n\n    public static HanziToPinyin getInstance() {\n        synchronized (HanziToPinyin.class) {\n            if (sInstance != null) {\n                return sInstance;\n            }\n            // Check if zh_CN collation data is available\n            final Locale[] locale = Collator.getAvailableLocales();\n            for (Locale value : locale) {\n                if (value.equals(Locale.CHINA) || value.getLanguage().contains(\"zh\")) {\n                    // Do self validation just once.\n                    if (DEBUG) {\n                        Log.d(TAG, \"Self validation. Result: \" + doSelfValidation());\n                    }\n                    sInstance = new HanziToPinyin(true);\n                    return sInstance;\n                }\n            }\n            if (sInstance == null) {//这个判断是用于处理国产ROM的兼容性问题\n                if (Locale.CHINA.equals(Locale.getDefault())) {\n                    sInstance = new HanziToPinyin(true);\n                    return sInstance;\n                }\n            }\n            Log.w(TAG, \"There is no Chinese collator, HanziToPinyin is disabled\");\n            sInstance = new HanziToPinyin(false);\n            return sInstance;\n        }\n    }\n\n    /**\n     * Validate if our internal table has some wrong value.\n     *\n     * @return true when the table looks correct.\n     */\n    private static boolean doSelfValidation() {\n        char lastChar = UNIHANS[0];\n        String lastString = Character.toString(lastChar);\n        for (char c : UNIHANS) {\n            if (lastChar == c) {\n                continue;\n            }\n            final String curString = Character.toString(c);\n            int cmp = COLLATOR.compare(lastString, curString);\n            if (cmp >= 0) {\n                Log.e(TAG, \"Internal error in Unihan table. \" + \"The last string \\\"\" + lastString\n                        + \"\\\" is greater than current string \\\"\" + curString + \"\\\".\");\n                return false;\n            }\n            lastString = curString;\n        }\n        return true;\n    }\n\n    private Token getToken(char character) {\n        Token token = new Token();\n        final String letter = Character.toString(character);\n        token.source = letter;\n        int offset = -1;\n        int cmp;\n        if (character < 256) {\n            token.type = Token.LATIN;\n            token.target = letter;\n            return token;\n        } else {\n            cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN);\n            if (cmp < 0) {\n                token.type = Token.UNKNOWN;\n                token.target = letter;\n                return token;\n            } else if (cmp == 0) {\n                token.type = Token.PINYIN;\n                offset = 0;\n            } else {\n                cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN);\n                if (cmp > 0) {\n                    token.type = Token.UNKNOWN;\n                    token.target = letter;\n                    return token;\n                } else if (cmp == 0) {\n                    token.type = Token.PINYIN;\n                    offset = UNIHANS.length - 1;\n                }\n            }\n        }\n\n        token.type = Token.PINYIN;\n        if (offset < 0) {\n            int begin = 0;\n            int end = UNIHANS.length - 1;\n            while (begin <= end) {\n                offset = (begin + end) / 2;\n                final String unihan = Character.toString(UNIHANS[offset]);\n                cmp = COLLATOR.compare(letter, unihan);\n                if (cmp == 0) {\n                    break;\n                } else if (cmp > 0) {\n                    begin = offset + 1;\n                } else {\n                    end = offset - 1;\n                }\n            }\n        }\n        if (cmp < 0) {\n            offset--;\n        }\n        StringBuilder pinyin = new StringBuilder();\n        for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) {\n            pinyin.append((char) PINYINS[offset][j]);\n        }\n        token.target = pinyin.toString();\n        if (TextUtils.isEmpty(token.target)) {\n            token.type = Token.UNKNOWN;\n            token.target = token.source;\n        }\n        return token;\n    }\n\n    /**\n     * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without\n     * space will be put into a Token, One Hanzi character which has pinyin will be treated as a\n     * Token. If these is no China collator, the empty token array is returned.\n     */\n    public ArrayList<Token> get(final String input) {\n        ArrayList<Token> tokens = new ArrayList<>();\n        if (!mHasChinaCollator || TextUtils.isEmpty(input)) {\n            // return empty tokens.\n            return tokens;\n        }\n        final int inputLength = input.length();\n        final StringBuilder sb = new StringBuilder();\n        int tokenType = Token.LATIN;\n        // Go through the input, create a new token when\n        // a. Token type changed\n        // b. Get the Pinyin of current charater.\n        // c. current character is space.\n        for (int i = 0; i < inputLength; i++) {\n            final char character = input.charAt(i);\n            if (character == ' ') {\n                if (sb.length() > 0) {\n                    addToken(sb, tokens, tokenType);\n                }\n            } else if (character < 256) {\n                if (tokenType != Token.LATIN && sb.length() > 0) {\n                    addToken(sb, tokens, tokenType);\n                }\n                tokenType = Token.LATIN;\n                sb.append(character);\n            } else {\n                Token t = getToken(character);\n                if (t.type == Token.PINYIN) {\n                    if (sb.length() > 0) {\n                        addToken(sb, tokens, tokenType);\n                    }\n                    tokens.add(t);\n                    tokenType = Token.PINYIN;\n                } else {\n                    if (tokenType != t.type && sb.length() > 0) {\n                        addToken(sb, tokens, tokenType);\n                    }\n                    tokenType = t.type;\n                    sb.append(character);\n                }\n            }\n        }\n        if (sb.length() > 0) {\n            addToken(sb, tokens, tokenType);\n        }\n        return tokens;\n    }\n\n    private void addToken(\n            final StringBuilder sb, final ArrayList<Token> tokens, final int tokenType) {\n        String str = sb.toString();\n        tokens.add(new Token(tokenType, str, str));\n        sb.setLength(0);\n    }\n\n    public String toPinyinString(String string) {\n        if (string == null) {\n            return null;\n        }\n        StringBuilder sb = new StringBuilder();\n        ArrayList<Token> tokens = get(string);\n        for (Token token : tokens) {\n            sb.append(token.target);\n        }\n        return sb.toString().toLowerCase();\n    }\n\n    public static class Token {\n        /**\n         * Separator between target string for each source char\n         */\n        public static final String SEPARATOR = \" \";\n\n        public static final int LATIN = 1;\n        public static final int PINYIN = 2;\n        public static final int UNKNOWN = 3;\n        /**\n         * Type of this token, ASCII, PINYIN or UNKNOWN.\n         */\n        public int type;\n        /**\n         * Original string before translation.\n         */\n        public String source;\n        /**\n         * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is\n         * original string in source.\n         */\n        public String target;\n\n        public Token() {\n        }\n\n        public Token(int type, String source, String target) {\n            this.type = type;\n            this.source = source;\n            this.target = target;\n        }\n    }\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/HazeExt.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.unit.dp\nimport dev.chrisbanes.haze.ExperimentalHazeApi\nimport dev.chrisbanes.haze.HazeInputScale\nimport dev.chrisbanes.haze.HazeState\nimport dev.chrisbanes.haze.HazeStyle\nimport dev.chrisbanes.haze.hazeEffect\n\n@OptIn(ExperimentalHazeApi::class)\nfun Modifier.defaultHazeEffect(\n    hazeState: HazeState,\n    hazeStyle: HazeStyle,\n): Modifier = this.hazeEffect(\n    state = hazeState,\n    style = hazeStyle\n) {\n    blurRadius = 20.dp\n    inputScale = HazeInputScale.Fixed(0.25f)\n    noiseFactor = 0f\n    forceInvalidateOnPreDraw = false\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport android.content.ContentResolver\nimport android.content.Context\nimport android.database.Cursor\nimport android.net.Uri\nimport android.os.Environment\nimport android.os.Parcelable\nimport android.os.SystemClock\nimport android.provider.OpenableColumns\nimport android.system.Os\nimport android.util.Log\nimport com.topjohnwu.superuser.CallbackList\nimport com.topjohnwu.superuser.Shell\nimport com.topjohnwu.superuser.ShellUtils\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport kotlinx.parcelize.Parcelize\nimport me.weishu.kernelsu.BuildConfig\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.ksuApp\nimport org.json.JSONArray\nimport java.io.File\n\n\n/**\n * @author weishu\n * @date 2023/1/1.\n */\nprivate const val TAG = \"KsuCli\"\n\nprivate fun getKsuDaemonPath(): String {\n    return ksuApp.applicationInfo.nativeLibraryDir + File.separator + \"libksud.so\"\n}\n\ndata class FlashResult(val code: Int, val err: String, val showReboot: Boolean) {\n    constructor(result: Shell.Result, showReboot: Boolean) : this(result.code, result.err.joinToString(\"\\n\"), showReboot)\n    constructor(result: Shell.Result) : this(result, result.isSuccess)\n}\n\nobject KsuCli {\n    val SHELL: Shell = createRootShell()\n    val GLOBAL_MNT_SHELL: Shell = createRootShell(true)\n}\n\nfun getRootShell(globalMnt: Boolean = false): Shell {\n    return if (globalMnt) KsuCli.GLOBAL_MNT_SHELL else {\n        KsuCli.SHELL\n    }\n}\n\ninline fun <T> withNewRootShell(\n    globalMnt: Boolean = false,\n    block: Shell.() -> T\n): T {\n    return createRootShell(globalMnt).use(block)\n}\n\nfun Uri.getFileName(context: Context): String? {\n    var fileName: String? = null\n    val contentResolver: ContentResolver = context.contentResolver\n    val cursor: Cursor? = contentResolver.query(this, null, null, null, null)\n    cursor?.use {\n        if (it.moveToFirst()) {\n            fileName = it.getString(it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))\n        }\n    }\n    return fileName\n}\n\nfun createRootShell(globalMnt: Boolean = false): Shell {\n    Shell.enableVerboseLogging = BuildConfig.DEBUG\n    val builder = Shell.Builder.create()\n    return try {\n        if (globalMnt) {\n            builder.build(getKsuDaemonPath(), \"debug\", \"su\", \"-g\")\n        } else {\n            builder.build(getKsuDaemonPath(), \"debug\", \"su\")\n        }\n    } catch (e: Throwable) {\n        Log.w(TAG, \"ksu failed: \", e)\n        try {\n            if (globalMnt) {\n                builder.build(\"su\", \"-mm\")\n            } else {\n                builder.build(\"su\")\n            }\n        } catch (e: Throwable) {\n            Log.e(TAG, \"su failed: \", e)\n            builder.build(\"sh\")\n        }\n    }\n}\n\nfun execKsud(args: String, newShell: Boolean = false): Boolean {\n    return if (newShell) {\n        withNewRootShell {\n            ShellUtils.fastCmdResult(this, \"${getKsuDaemonPath()} $args\")\n        }\n    } else {\n        ShellUtils.fastCmdResult(getRootShell(), \"${getKsuDaemonPath()} $args\")\n    }\n}\n\nsuspend fun getFeatureStatus(feature: String): String = withContext(Dispatchers.IO) {\n    val shell = getRootShell()\n    val out = shell.newJob()\n        .add(\"${getKsuDaemonPath()} feature check $feature\").to(ArrayList<String>(), null).exec().out\n    out.firstOrNull()?.trim().orEmpty()\n}\n\nsuspend fun getFeaturePersistValue(feature: String): Long? = withContext(Dispatchers.IO) {\n    val shell = getRootShell()\n    val out = shell.newJob()\n        .add(\"${getKsuDaemonPath()} feature get --config $feature\").to(ArrayList<String>(), null).exec().out\n    val valueLine = out.firstOrNull { it.trim().startsWith(\"Value:\") } ?: return@withContext null\n    valueLine.substringAfter(\"Value:\").trim().toLongOrNull()\n}\n\nfun install() {\n    val start = SystemClock.elapsedRealtime()\n    val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, \"libmagiskboot.so\").absolutePath\n    val result = execKsud(\"install --magiskboot $magiskboot\", true)\n    Log.w(TAG, \"install result: $result, cost: ${SystemClock.elapsedRealtime() - start}ms\")\n}\n\nfun listModules(): String {\n    val shell = getRootShell()\n\n    val out = shell.newJob()\n        .add(\"${getKsuDaemonPath()} module list\").to(ArrayList(), null).exec().out\n    return out.joinToString(\"\\n\").ifBlank { \"[]\" }\n}\n\nfun getModuleCount(): Int {\n    val result = listModules()\n    runCatching {\n        val array = JSONArray(result)\n        return array.length()\n    }.getOrElse { return 0 }\n}\n\nfun getSuperuserCount(): Int {\n    return Natives.getSuperuserCount()\n}\n\nfun toggleModule(id: String, enable: Boolean): Boolean {\n    val cmd = if (enable) {\n        \"module enable $id\"\n    } else {\n        \"module disable $id\"\n    }\n    val result = execKsud(cmd, true)\n    Log.i(TAG, \"$cmd result: $result\")\n    return result\n}\n\nfun undoUninstallModule(id: String): Boolean {\n    val cmd = \"module undo-uninstall $id\"\n    val result = execKsud(cmd, true)\n    Log.i(TAG, \"undo uninstall module $id result: $result\")\n    return result\n}\n\nfun uninstallModule(id: String): Boolean {\n    val cmd = \"module uninstall $id\"\n    val result = execKsud(cmd, true)\n    Log.i(TAG, \"uninstall module $id result: $result\")\n    return result\n}\n\nprivate fun flashWithIO(\n    cmd: String,\n    onStdout: (String) -> Unit,\n    onStderr: (String) -> Unit\n): Shell.Result {\n\n    val stdoutCallback: CallbackList<String?> = object : CallbackList<String?>() {\n        override fun onAddElement(s: String?) {\n            onStdout(s ?: \"\")\n        }\n    }\n\n    val stderrCallback: CallbackList<String?> = object : CallbackList<String?>() {\n        override fun onAddElement(s: String?) {\n            onStderr(s ?: \"\")\n        }\n    }\n\n    return withNewRootShell {\n        newJob().add(cmd).to(stdoutCallback, stderrCallback).exec()\n    }\n}\n\nfun flashModule(\n    uri: Uri,\n    onStdout: (String) -> Unit,\n    onStderr: (String) -> Unit\n): FlashResult {\n    val resolver = ksuApp.contentResolver\n    with(resolver.openInputStream(uri)) {\n        val file = File(ksuApp.cacheDir, \"module.zip\")\n        file.outputStream().use { output ->\n            this?.copyTo(output)\n        }\n        val cmd = \"module install ${file.absolutePath}\"\n        val result = flashWithIO(\"${getKsuDaemonPath()} $cmd\", onStdout, onStderr)\n        Log.i(\"KernelSU\", \"install module $uri result: $result\")\n\n        file.delete()\n\n        return FlashResult(result)\n    }\n}\n\nfun runModuleAction(\n    moduleId: String, onStdout: (String) -> Unit, onStderr: (String) -> Unit\n): Boolean {\n    val shell = createRootShell(true)\n\n    val stdoutCallback: CallbackList<String?> = object : CallbackList<String?>() {\n        override fun onAddElement(s: String?) {\n            onStdout(s ?: \"\")\n        }\n    }\n\n    val stderrCallback: CallbackList<String?> = object : CallbackList<String?>() {\n        override fun onAddElement(s: String?) {\n            onStderr(s ?: \"\")\n        }\n    }\n\n    val result = shell.newJob().add(\"${getKsuDaemonPath()} module action $moduleId\")\n        .to(stdoutCallback, stderrCallback).exec()\n    Log.i(\"KernelSU\", \"Module runAction result: $result\")\n\n    return result.isSuccess\n}\n\nfun restoreBoot(\n    onStdout: (String) -> Unit, onStderr: (String) -> Unit\n): FlashResult {\n    val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, \"libmagiskboot.so\")\n    val result = flashWithIO(\"${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot\", onStdout, onStderr)\n    return FlashResult(result)\n}\n\nfun uninstallPermanently(\n    onStdout: (String) -> Unit, onStderr: (String) -> Unit\n): FlashResult {\n    val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, \"libmagiskboot.so\")\n    val result = flashWithIO(\"${getKsuDaemonPath()} uninstall --magiskboot $magiskboot\", onStdout, onStderr)\n    return FlashResult(result)\n}\n\n@Parcelize\nsealed class LkmSelection : Parcelable {\n    @Parcelize\n    data class LkmUri(val uri: Uri) : LkmSelection()\n\n    @Parcelize\n    data class KmiString(val value: String) : LkmSelection()\n\n    @Parcelize\n    data object KmiNone : LkmSelection()\n}\n\nfun installBoot(\n    bootUri: Uri?,\n    lkm: LkmSelection,\n    ota: Boolean,\n    partition: String?,\n    allowShell: Boolean,\n    enableAdb: Boolean,\n    onStdout: (String) -> Unit,\n    onStderr: (String) -> Unit,\n): FlashResult {\n    val resolver = ksuApp.contentResolver\n\n    val bootFile = bootUri?.let { uri ->\n        with(resolver.openInputStream(uri)) {\n            val bootFile = File(ksuApp.cacheDir, \"boot.img\")\n            bootFile.outputStream().use { output ->\n                this?.copyTo(output)\n            }\n\n            bootFile\n        }\n    }\n\n    val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, \"libmagiskboot.so\")\n    var cmd = \"boot-patch --magiskboot ${magiskboot.absolutePath}\"\n\n    cmd += if (bootFile == null) {\n        // no boot.img, use -f to flash\n        \" -f\"\n    } else {\n        \" -b ${bootFile.absolutePath}\"\n    }\n\n    if (allowShell) {\n        cmd += \" --allow-shell\"\n    }\n\n    if (enableAdb) {\n        cmd += \" --enable-adbd\"\n    }\n\n    if (ota) {\n        cmd += \" -u\"\n    }\n\n    var lkmFile: File? = null\n    when (lkm) {\n        is LkmSelection.LkmUri -> {\n            lkmFile = with(resolver.openInputStream(lkm.uri)) {\n                val file = File(ksuApp.cacheDir, \"kernelsu-tmp-lkm.ko\")\n                file.outputStream().use { output ->\n                    this?.copyTo(output)\n                }\n\n                file\n            }\n            cmd += \" -m ${lkmFile.absolutePath}\"\n        }\n\n        is LkmSelection.KmiString -> {\n            cmd += \" --kmi ${lkm.value}\"\n        }\n\n        LkmSelection.KmiNone -> {\n            // do nothing\n        }\n    }\n\n    // output dir\n    if (bootFile != null) {\n        val downloadsDir =\n            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)\n        cmd += \" -o $downloadsDir\"\n    }\n\n    partition?.let { part ->\n        cmd += \" --partition $part\"\n    }\n\n    val result = flashWithIO(\"${getKsuDaemonPath()} $cmd\", onStdout, onStderr)\n    Log.i(\"KernelSU\", \"install boot result: ${result.isSuccess}\")\n\n    bootFile?.delete()\n    lkmFile?.delete()\n\n    // if boot uri is empty, it is direct install, when success, we should show reboot button\n    val showReboot = bootUri == null && result.isSuccess // we create a temporary val here, to avoid calc showReboot double\n    if (showReboot) { // because we decide do not update ksud when startActivity\n        install() // install ksud here\n    }\n    return FlashResult(result, showReboot)\n}\n\nfun reboot(reason: String = \"\") {\n    val shell = getRootShell()\n    if (reason == \"soft_reboot\") {\n        ShellUtils.fastCmd(shell, \"setprop ctl.restart zygote\")\n        return\n    }\n    if (reason == \"recovery\") {\n        // KEYCODE_POWER = 26, hide incorrect \"Factory data reset\" message\n        ShellUtils.fastCmd(shell, \"/system/bin/input keyevent 26\")\n    }\n    ShellUtils.fastCmd(shell, \"/system/bin/svc power reboot $reason || /system/bin/reboot $reason\")\n}\n\nfun rootAvailable(): Boolean {\n    val shell = getRootShell()\n    return shell.isRoot\n}\n\nsuspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) {\n    val shell = getRootShell()\n    val cmd = \"boot-info current-kmi\"\n    ShellUtils.fastCmd(shell, \"${getKsuDaemonPath()} $cmd\")\n}\n\nsuspend fun getSupportedKmis(): List<String> = withContext(Dispatchers.IO) {\n    val shell = getRootShell()\n    val cmd = \"boot-info supported-kmis\"\n    val out = shell.newJob().add(\"${getKsuDaemonPath()} $cmd\").to(ArrayList(), null).exec().out\n    out.filter { it.isNotBlank() }.map { it.trim() }\n}\n\nsuspend fun isAbDevice(): Boolean = withContext(Dispatchers.IO) {\n    val shell = getRootShell()\n    val cmd = \"boot-info is-ab-device\"\n    ShellUtils.fastCmd(shell, \"${getKsuDaemonPath()} $cmd\").trim().toBoolean()\n}\n\nsuspend fun getDefaultPartition(): String = withContext(Dispatchers.IO) {\n    val shell = getRootShell()\n    if (shell.isRoot) {\n        val cmd = \"boot-info default-partition\"\n        ShellUtils.fastCmd(shell, \"${getKsuDaemonPath()} $cmd\").trim()\n    } else {\n        if (!Os.uname().release.contains(\"android12-\")) \"init_boot\" else \"boot\"\n    }\n}\n\nsuspend fun getSlotSuffix(ota: Boolean): String = withContext(Dispatchers.IO) {\n    val shell = getRootShell()\n    val cmd = if (ota) {\n        \"boot-info slot-suffix --ota\"\n    } else {\n        \"boot-info slot-suffix\"\n    }\n    ShellUtils.fastCmd(shell, \"${getKsuDaemonPath()} $cmd\").trim()\n}\n\nsuspend fun getAvailablePartitions(): List<String> = withContext(Dispatchers.IO) {\n    val shell = getRootShell()\n    val cmd = \"boot-info available-partitions\"\n    val out = shell.newJob().add(\"${getKsuDaemonPath()} $cmd\").to(ArrayList(), null).exec().out\n    out.filter { it.isNotBlank() }.map { it.trim() }\n}\n\nfun hasMagisk(): Boolean {\n    val shell = getRootShell(true)\n    val result = shell.newJob().add(\"which magisk\").exec()\n    Log.i(TAG, \"has magisk: ${result.isSuccess}\")\n    return result.isSuccess\n}\n\nfun isSepolicyValid(rules: String?): Boolean {\n    if (rules == null) {\n        return true\n    }\n    val shell = getRootShell()\n    val result =\n        shell.newJob().add(\"${getKsuDaemonPath()} sepolicy check '$rules'\").to(ArrayList(), null)\n            .exec()\n    return result.isSuccess\n}\n\nfun getSepolicy(pkg: String): String {\n    val shell = getRootShell()\n    val result =\n        shell.newJob().add(\"${getKsuDaemonPath()} profile get-sepolicy $pkg\").to(ArrayList(), null)\n            .exec()\n    Log.i(TAG, \"code: ${result.code}, out: ${result.out}, err: ${result.err}\")\n    return result.out.joinToString(\"\\n\")\n}\n\nfun setSepolicy(pkg: String, rules: String): Boolean {\n    val shell = getRootShell()\n    val result = shell.newJob().add(\"${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'\")\n        .to(ArrayList(), null).exec()\n    Log.i(TAG, \"set sepolicy result: ${result.code}\")\n    return result.isSuccess\n}\n\nfun listAppProfileTemplates(): List<String> {\n    val shell = getRootShell()\n    return shell.newJob().add(\"${getKsuDaemonPath()} profile list-templates\").to(ArrayList(), null)\n        .exec().out\n}\n\nfun getAppProfileTemplate(id: String): String {\n    val shell = getRootShell()\n    return shell.newJob().add(\"${getKsuDaemonPath()} profile get-template '${id}'\")\n        .to(ArrayList(), null).exec().out.joinToString(\"\\n\")\n}\n\nfun setAppProfileTemplate(id: String, template: String): Boolean {\n    val shell = getRootShell()\n    val escapedTemplate = template.replace(\"\\\"\", \"\\\\\\\"\")\n    val cmd = \"\"\"${getKsuDaemonPath()} profile set-template \"$id\" \"$escapedTemplate'\"\"\"\"\n    return shell.newJob().add(cmd)\n        .to(ArrayList(), null).exec().isSuccess\n}\n\nfun deleteAppProfileTemplate(id: String): Boolean {\n    val shell = getRootShell()\n    return shell.newJob().add(\"${getKsuDaemonPath()} profile delete-template '${id}'\")\n        .to(ArrayList(), null).exec().isSuccess\n}\n\nfun forceStopApp(packageName: String, userId: Int? = null) {\n    val shell = getRootShell()\n    val userArg = userId?.let { \" --user $it\" } ?: \"\"\n    val result = shell.newJob().add(\"am force-stop$userArg $packageName\").exec()\n    Log.i(TAG, \"force stop $packageName result: $result\")\n}\n\nfun launchApp(packageName: String, userId: Int? = null) {\n    val shell = getRootShell()\n    val userArg = userId?.let { \" --user $it\" } ?: \"\"\n    val result =\n        shell.newJob()\n            .add(\"cmd package resolve-activity --brief$userArg $packageName | tail -n 1 | xargs cmd activity start-activity$userArg -n\")\n            .exec()\n    Log.i(TAG, \"launch $packageName result: $result\")\n}\n\nfun restartApp(packageName: String, userId: Int? = null) {\n    forceStopApp(packageName, userId)\n    launchApp(packageName, userId)\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/LogEvent.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport android.content.Context\nimport android.os.Build\nimport android.system.Os\nimport com.topjohnwu.superuser.ShellUtils\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.ui.screen.home.getManagerVersion\nimport java.io.File\nimport java.io.FileWriter\nimport java.io.PrintWriter\nimport java.time.LocalDateTime\nimport java.time.format.DateTimeFormatter\n\nfun getBugreportFile(context: Context): File {\n\n    val bugreportDir = File(context.cacheDir, \"bugreport\")\n    bugreportDir.mkdirs()\n\n    val processFile = File(bugreportDir, \"process.txt\")\n    val dmesgFile = File(bugreportDir, \"dmesg.txt\")\n    val logcatFile = File(bugreportDir, \"logcat.txt\")\n    val tombstonesFile = File(bugreportDir, \"tombstones.tar.gz\")\n    val dropboxFile = File(bugreportDir, \"dropbox.tar.gz\")\n    val pstoreFile = File(bugreportDir, \"pstore.tar.gz\")\n    // Xiaomi/Readmi devices have diag in /data/vendor/diag\n    val diagFile = File(bugreportDir, \"diag.tar.gz\")\n    val oplusFile = File(bugreportDir, \"oplus.tar.gz\")\n    val bootlogFile = File(bugreportDir, \"bootlog.tar.gz\")\n    val mountsFile = File(bugreportDir, \"mounts.txt\")\n    val fileSystemsFile = File(bugreportDir, \"filesystems.txt\")\n    val adbFileTree = File(bugreportDir, \"adb_tree.txt\")\n    val adbFileDetails = File(bugreportDir, \"adb_details.txt\")\n    val ksuFileSize = File(bugreportDir, \"ksu_size.txt\")\n    val appListFile = File(bugreportDir, \"packages.txt\")\n    val propFile = File(bugreportDir, \"props.txt\")\n    val allowListFile = File(bugreportDir, \"allowlist.bin\")\n    val procModules = File(bugreportDir, \"proc_modules.txt\")\n    val bootConfig = File(bugreportDir, \"boot_config.txt\")\n    val kernelConfig = File(bugreportDir, \"defconfig.gz\")\n\n    val shell = getRootShell(true)\n\n    // busybox ps has very few features for embed devices\n    shell.newJob().add(\"toybox ps -T -A -w -o PID,TID,UID,COMM,CMDLINE,CMD,LABEL,STAT,WCHAN > ${processFile.absolutePath}\").exec()\n    shell.newJob().add(\"dmesg -r > ${dmesgFile.absolutePath}\").exec()\n    shell.newJob().add(\"logcat -b all -v uid -d > ${logcatFile.absolutePath}\").exec()\n    shell.newJob().add(\"tar -czf ${tombstonesFile.absolutePath} -C /data/tombstones .\").exec()\n    shell.newJob().add(\"tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .\").exec()\n    shell.newJob().add(\"tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .\").exec()\n    shell.newJob().add(\"tar -czf ${diagFile.absolutePath} -C /data/vendor/diag . --exclude=./minidump.gz\").exec()\n    shell.newJob().add(\"tar -czf ${oplusFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .\").exec()\n    shell.newJob().add(\"tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .\").exec()\n\n    shell.newJob().add(\"cat /proc/1/mountinfo > ${mountsFile.absolutePath}\").exec()\n    shell.newJob().add(\"cat /proc/filesystems > ${fileSystemsFile.absolutePath}\").exec()\n    shell.newJob().add(\"busybox tree /data/adb > ${adbFileTree.absolutePath}\").exec()\n    shell.newJob().add(\"ls -alRZ /data/adb > ${adbFileDetails.absolutePath}\").exec()\n    shell.newJob().add(\"du -sh /data/adb/ksu/* > ${ksuFileSize.absolutePath}\").exec()\n    shell.newJob().add(\"cp /data/system/packages.list ${appListFile.absolutePath}\").exec()\n    shell.newJob().add(\"getprop > ${propFile.absolutePath}\").exec()\n    shell.newJob().add(\"cp /data/adb/ksu/.allowlist ${allowListFile.absolutePath}\").exec()\n    shell.newJob().add(\"cp /proc/modules ${procModules.absolutePath}\").exec()\n    shell.newJob().add(\"cp /proc/bootconfig ${bootConfig.absolutePath}\").exec()\n    shell.newJob().add(\"cp /proc/config.gz ${kernelConfig.absolutePath}\").exec()\n\n    val selinux = ShellUtils.fastCmd(shell, \"getenforce\")\n\n    // basic information\n    val buildInfo = File(bugreportDir, \"basic.txt\")\n    PrintWriter(FileWriter(buildInfo)).use { pw ->\n        pw.println(\"Kernel: ${System.getProperty(\"os.version\")}\")\n        pw.println(\"BRAND: \" + Build.BRAND)\n        pw.println(\"MODEL: \" + Build.MODEL)\n        pw.println(\"PRODUCT: \" + Build.PRODUCT)\n        pw.println(\"MANUFACTURER: \" + Build.MANUFACTURER)\n        pw.println(\"SDK: \" + Build.VERSION.SDK_INT)\n        pw.println(\"PREVIEW_SDK: \" + Build.VERSION.PREVIEW_SDK_INT)\n        pw.println(\"FINGERPRINT: \" + Build.FINGERPRINT)\n        pw.println(\"DEVICE: \" + Build.DEVICE)\n        pw.println(\"Manager: \" + getManagerVersion(context))\n        pw.println(\"SELinux: $selinux\")\n\n        val uname = Os.uname()\n        pw.println(\"KernelRelease: ${uname.release}\")\n        pw.println(\"KernelVersion: ${uname.version}\")\n        pw.println(\"Machine: ${uname.machine}\")\n        pw.println(\"Nodename: ${uname.nodename}\")\n        pw.println(\"Sysname: ${uname.sysname}\")\n\n        val ksuKernel = Natives.version\n        pw.println(\"KernelSU: $ksuKernel\")\n        val safeMode = Natives.isSafeMode\n        pw.println(\"SafeMode: $safeMode\")\n        val lkmMode = Natives.isLkmMode\n        pw.println(\"LKM: $lkmMode\")\n    }\n\n    // modules\n    val modulesFile = File(bugreportDir, \"modules.json\")\n    modulesFile.writeText(listModules())\n\n    val formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd_HH_mm\")\n    val current = LocalDateTime.now().format(formatter)\n\n    val targetFile = File(context.cacheDir, \"KernelSU_bugreport_${current}.tar.gz\")\n\n    shell.newJob().add(\"tar czf ${targetFile.absolutePath} -C ${bugreportDir.absolutePath} .\").exec()\n    shell.newJob().add(\"rm -rf ${bugreportDir.absolutePath}\").exec()\n    shell.newJob().add(\"chmod 0644 ${targetFile.absolutePath}\").exec()\n\n    return targetFile\n}\n\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/Network.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport android.content.Context\nimport android.net.ConnectivityManager\nimport android.net.NetworkCapabilities\n\nfun isNetworkAvailable(context: Context): Boolean {\n    val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager\n\n    val network = cm.activeNetwork ?: return false\n    val caps = cm.getNetworkCapabilities(network) ?: return false\n\n    val hasTransport = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) ||\n            caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) ||\n            caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)\n\n    return hasTransport &&\n            caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&\n            caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/OemHelper.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport android.annotation.SuppressLint\n\n@SuppressLint(\"PrivateApi\")\nprivate fun getSystemProperty(key: String): String {\n    return try {\n        val props = Class.forName(\"android.os.SystemProperties\")\n        props.getMethod(\"get\", String::class.java).invoke(null, key) as? String ?: \"\"\n    } catch (_: Throwable) {\n        \"\"\n    }\n}\n\nfun isMiui(): Boolean {\n    return getSystemProperty(\"ro.miui.ui.version.name\").isNotEmpty()\n}\n\nfun isHyperOS(): Boolean {\n    return getSystemProperty(\"ro.mi.os.version.name\").isNotEmpty()\n}\n\nfun isColorOS(): Boolean {\n    return getSystemProperty(\"ro.build.version.oplus.api\").isNotEmpty() || getSystemProperty(\"ro.vendor.oplus.market.name\").isNotEmpty()\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/SELinuxChecker.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.res.stringResource\nimport com.topjohnwu.superuser.Shell\nimport me.weishu.kernelsu.R\n\nfun isSELinuxPermissive(): Boolean {\n    val shell = Shell.Builder.create().build(\"sh\")\n    val stdoutList = ArrayList<String>()\n    val result = shell.use {\n        it.newJob().add(\"getenforce\").to(stdoutList).exec()\n    }\n    return result.isSuccess && stdoutList.joinToString(\"\").trim() == \"Permissive\"\n}\n\n@Composable\nfun getSELinuxStatus(): String {\n    val shell = Shell.Builder.create().build(\"sh\")\n\n    val stdoutList = ArrayList<String>()\n    val stderrList = ArrayList<String>()\n    val result = shell.use {\n        it.newJob().add(\"getenforce\").to(stdoutList, stderrList).exec()\n    }\n    val stdout = stdoutList.joinToString(\"\\n\").trim()\n    val stderr = stderrList.joinToString(\"\\n\").trim()\n\n    if (result.isSuccess) {\n        return when (stdout) {\n            \"Enforcing\" -> stringResource(R.string.selinux_status_enforcing)\n            \"Permissive\" -> stringResource(R.string.selinux_status_permissive)\n            \"Disabled\" -> stringResource(R.string.selinux_status_disabled)\n            else -> stringResource(R.string.selinux_status_unknown)\n        }\n    }\n\n    return if (stderr.endsWith(\"Permission denied\")) {\n        stringResource(R.string.selinux_status_enforcing)\n    } else {\n        stringResource(R.string.selinux_status_unknown)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/Serialization.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport android.os.Parcel\nimport android.os.Parcelable\nimport kotlinx.serialization.KSerializer\nimport kotlinx.serialization.builtins.ByteArraySerializer\nimport kotlinx.serialization.encoding.Decoder\nimport kotlinx.serialization.encoding.Encoder\nimport me.weishu.kernelsu.ui.screen.flash.FlashIt\nimport me.weishu.kernelsu.ui.screen.modulerepo.RepoModuleArg\nimport me.weishu.kernelsu.ui.viewmodel.TemplateViewModel\n\nobject FlashItSerializer : BaseParcelableSerializer<FlashIt>(FlashIt::class.java)\nobject RepoModuleArgSerializer : BaseParcelableSerializer<RepoModuleArg>(RepoModuleArg::class.java)\nobject TemplateInfoSerializer : BaseParcelableSerializer<TemplateViewModel.TemplateInfo>(TemplateViewModel.TemplateInfo::class.java)\n\nopen class BaseParcelableSerializer<T : Parcelable>(\n    private val clazz: Class<T>\n) : KSerializer<T> {\n\n    private val delegate = ByteArraySerializer()\n    override val descriptor = delegate.descriptor\n\n    private val creator: Parcelable.Creator<T> by lazy {\n        @Suppress(\"UNCHECKED_CAST\")\n        clazz.getField(\"CREATOR\").get(null) as Parcelable.Creator<T>\n    }\n\n    override fun serialize(encoder: Encoder, value: T) {\n        val parcel = Parcel.obtain()\n        try {\n            value.writeToParcel(parcel, 0)\n            val bytes = parcel.marshall()\n            encoder.encodeSerializableValue(delegate, bytes)\n        } finally {\n            parcel.recycle()\n        }\n    }\n\n    override fun deserialize(decoder: Decoder): T {\n        val bytes = decoder.decodeSerializableValue(delegate)\n        val parcel = Parcel.obtain()\n        try {\n            parcel.unmarshall(bytes, 0, bytes.size)\n            parcel.setDataPosition(0)\n            return creator.createFromParcel(parcel)\n        } finally {\n            parcel.recycle()\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/UidGroupUtils.kt",
    "content": "package me.weishu.kernelsu.ui.util\n\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel\nimport java.util.concurrent.ConcurrentHashMap\n\nprivate val PREFERRED_PKG_BY_SUID = mapOf(\n    \"android.uid.system\" to \"android\",\n    \"android.uid.phone\" to \"com.android.phone\",\n    \"android.uid.bluetooth\" to \"com.android.bluetooth\",\n    \"android.uid.nfc\" to \"com.android.nfc\",\n)\n\nfun pickPrimary(apps: List<SuperUserViewModel.AppInfo>): SuperUserViewModel.AppInfo {\n    if (apps.isEmpty()) throw IllegalArgumentException(\"apps must not be empty\")\n    val labeled = apps.filter { it.packageInfo.sharedUserLabel != 0 }\n    if (labeled.isNotEmpty()) {\n        return labeled.minWith(compareBy({ it.packageName.length }, { it.packageName }))\n    }\n    val bySuid = apps.groupBy { it.packageInfo.sharedUserId ?: \"\" }\n        .filterKeys { it.startsWith(\"android.uid.\") }\n    if (bySuid.isEmpty()) return apps.first()\n    val suid = bySuid.keys.minOf { it }\n    val group = bySuid[suid] ?: apps\n    val preferredPkg = PREFERRED_PKG_BY_SUID[suid]\n    preferredPkg?.let { pkg ->\n        group.firstOrNull { it.packageName == pkg }?.let { return it }\n    }\n    return group.minWith(compareBy({ it.packageName.length }, { it.packageName }))\n}\n\nval ownerNameCache = ConcurrentHashMap<Int, String>()\nfun ownerNameForUid(uid: Int, appSource: List<SuperUserViewModel.AppInfo>? = null): String {\n    ownerNameCache[uid]?.let { return it.ifEmpty { uid.toString() } }\n    val apps = (appSource ?: SuperUserViewModel.apps).filter { it.uid == uid }\n    val labeledApp = apps.firstOrNull { it.packageInfo.sharedUserLabel != 0 }\n    val name = if (labeledApp != null) {\n        val pm = ksuApp.packageManager\n        val resId = labeledApp.packageInfo.sharedUserLabel\n        val text = runCatching { pm.getText(labeledApp.packageName, resId, labeledApp.packageInfo.applicationInfo) }.getOrNull()\n        text?.toString() ?: \"\"\n    } else {\n        Natives.getUserName(uid) ?: \"\"\n    }\n    val appId = uid % 100000\n    val isAppRange = appId in 10000..19999\n    val isUA = name.matches(Regex(\"u\\\\d+_a\\\\d+\"))\n    val sharedUserId = apps.firstOrNull { !it.packageInfo.sharedUserId.isNullOrEmpty() }?.packageInfo?.sharedUserId\n    val finalName = if (isAppRange && isUA && !sharedUserId.isNullOrEmpty()) {\n        sharedUserId\n    } else {\n        name\n    }\n    ownerNameCache[uid] = finalName\n    return finalName.ifEmpty { uid.toString() }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/LatestVersionInfo.kt",
    "content": "package me.weishu.kernelsu.ui.util.module\n\ndata class LatestVersionInfo(\n    val versionCode: Int = 0,\n    val downloadUrl: String = \"\",\n    val changelog: String = \"\"\n)\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/ModuleRepoApi.kt",
    "content": "package me.weishu.kernelsu.ui.util.module\n\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.util.isNetworkAvailable\nimport okhttp3.Request\nimport org.json.JSONArray\nimport org.json.JSONObject\n\ndata class ModuleDetail(\n    val readme: String,\n    val readmeHtml: String,\n    val latestTag: String,\n    val latestTime: String,\n    val latestAssetName: String?,\n    val latestAssetUrl: String?,\n    val releases: List<ReleaseInfo>,\n    val homepageUrl: String,\n    val sourceUrl: String,\n    val url: String\n)\n\ndata class ReleaseInfo(\n    val name: String,\n    val tagName: String,\n    val publishedAt: String,\n    val descriptionHTML: String,\n    val assets: List<ReleaseAssetInfo>\n)\n\ndata class ReleaseAssetInfo(\n    val name: String,\n    val downloadUrl: String,\n    val size: Long,\n    val downloadCount: Int\n)\n\nfun sanitizeVersionString(version: String): String {\n    return version.replace(Regex(\"[^a-zA-Z0-9.\\\\-_]\"), \"_\")\n}\n\nfun stripTicks(s: String): String {\n    val t = s.trim()\n    return if (t.startsWith(\"`\") && t.endsWith(\"`\") && t.length >= 2) t.substring(1, t.length - 1) else t\n}\n\nfun fetchReleaseDescriptionHtml(moduleId: String, latestTag: String): String? {\n    if (!isNetworkAvailable(ksuApp)) return null\n    val url = \"https://modules.kernelsu.org/module/$moduleId.json\"\n    return runCatching {\n        ksuApp.okhttpClient.newCall(Request.Builder().url(url).build()).execute().use { resp ->\n            if (!resp.isSuccessful) null else {\n                val body = resp.body.string()\n                val obj = JSONObject(body)\n                val releasesArray = obj.optJSONArray(\"releases\") ?: return@use null\n                var fallbackHtml: String? = null\n                for (i in 0 until releasesArray.length()) {\n                    val r = releasesArray.optJSONObject(i) ?: continue\n                    val descHtml = r.optString(\"descriptionHTML\", \"\")\n                    if (fallbackHtml == null && descHtml.isNotBlank()) {\n                        fallbackHtml = descHtml\n                    }\n                    val rname = r.optString(\"name\", r.optString(\"tagName\", r.optString(\"version\", \"\")))\n                    if (rname == latestTag && descHtml.isNotBlank()) {\n                        return@use descHtml\n                    }\n                }\n                fallbackHtml\n            }\n        }\n    }.getOrNull()\n}\n\n\nfun fetchModuleDetail(moduleId: String): ModuleDetail? {\n    if (!isNetworkAvailable(ksuApp)) return null\n    val url = \"https://modules.kernelsu.org/module/$moduleId.json\"\n    return runCatching {\n        ksuApp.okhttpClient.newCall(Request.Builder().url(url).build()).execute().use { resp ->\n            if (!resp.isSuccessful) return@use null\n            val body = resp.body.string()\n            val obj = JSONObject(body)\n            val readme = obj.optString(\"readme\", \"\")\n            val readmeHtml = obj.optString(\"readmeHTML\", \"\")\n            val homepageUrl = stripTicks(obj.optString(\"homepageUrl\", \"\"))\n            val sourceUrl = stripTicks(obj.optString(\"sourceUrl\", \"\"))\n            val url = stripTicks(obj.optString(\"url\", \"\"))\n            val lr = obj.optJSONObject(\"latestRelease\")\n            var latestTag: String\n            var latestTime = \"\"\n            var latestAssetName: String? = null\n            var latestAssetUrl: String? = null\n            if (lr != null) {\n                latestTag = lr.optString(\"name\", lr.optString(\"version\", \"\"))\n                latestTime = lr.optString(\"time\", \"\")\n                var urlDl = lr.optString(\"downloadUrl\", \"\")\n                urlDl = stripTicks(urlDl)\n                if (urlDl.isNotEmpty()) {\n                    latestAssetName = urlDl.substringAfterLast('/')\n                    latestAssetUrl = urlDl\n                }\n            } else {\n                latestTag = obj.optString(\"latestRelease\", \"\")\n            }\n\n            val releasesArray = obj.optJSONArray(\"releases\")\n            val releases = if (releasesArray != null) {\n                (0 until releasesArray.length()).mapNotNull { rIdx ->\n                    val r = releasesArray.optJSONObject(rIdx) ?: return@mapNotNull null\n                    val rname = r.optString(\"name\", r.optString(\"tagName\", r.optString(\"version\", \"\")))\n                    val publishedAt = r.optString(\"publishedAt\", \"\")\n                    val descHtml = r.optString(\"descriptionHTML\", \"\")\n                    val assetsArray = r.optJSONArray(\"releaseAssets\") ?: JSONArray()\n                    val assets = (0 until assetsArray.length()).mapNotNull { aIdx ->\n                        val a = assetsArray.optJSONObject(aIdx) ?: return@mapNotNull null\n                        val aname = a.optString(\"name\", \"\")\n                        var adl = a.optString(\"downloadUrl\", \"\")\n                        adl = stripTicks(adl)\n                        val asz = a.optLong(\"size\", 0L)\n                        val dcnt = when (val dcAny = a.opt(\"downloadCount\")) {\n                            is Number -> dcAny.toInt()\n                            is String -> dcAny.toIntOrNull() ?: 0\n                            else -> 0\n                        }\n                        if (aname.isEmpty() || adl.isEmpty()) null else ReleaseAssetInfo(aname, adl, asz, dcnt)\n                    }\n                    ReleaseInfo(\n                        name = rname,\n                        tagName = r.optString(\"tagName\", rname),\n                        publishedAt = publishedAt,\n                        descriptionHTML = descHtml,\n                        assets = assets\n                    )\n                }\n            } else emptyList()\n\n            return@use ModuleDetail(\n                readme = readme,\n                readmeHtml = readmeHtml,\n                latestTag = latestTag,\n                latestTime = latestTime,\n                latestAssetName = latestAssetName,\n                latestAssetUrl = latestAssetUrl,\n                releases = releases,\n                homepageUrl = homepageUrl,\n                sourceUrl = sourceUrl,\n                url = url\n            )\n        }\n    }.getOrNull()\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/Shortcut.kt",
    "content": "package me.weishu.kernelsu.ui.util.module\n\nimport android.app.AppOpsManager\nimport android.content.Context\nimport android.content.Intent\nimport android.graphics.Bitmap\nimport android.graphics.BitmapFactory\nimport android.net.Uri\nimport android.provider.Settings\nimport android.util.Log\nimport android.widget.Toast\nimport androidx.core.content.pm.ShortcutInfoCompat\nimport androidx.core.content.pm.ShortcutManagerCompat\nimport androidx.core.graphics.drawable.IconCompat\nimport androidx.core.graphics.scale\nimport androidx.core.net.toUri\nimport com.topjohnwu.superuser.io.SuFile\nimport com.topjohnwu.superuser.io.SuFileInputStream\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.ui.MainActivity\nimport me.weishu.kernelsu.ui.util.getRootShell\nimport me.weishu.kernelsu.ui.util.isColorOS\nimport me.weishu.kernelsu.ui.util.isHyperOS\nimport me.weishu.kernelsu.ui.util.isMiui\nimport me.weishu.kernelsu.ui.webui.WebUIActivity\n\nobject Shortcut {\n\n    private const val TAG = \"ModuleShortcut\"\n\n    fun createModuleActionShortcut(\n        context: Context,\n        moduleId: String,\n        name: String,\n        iconUri: String?\n    ) {\n        val shortcutId = \"module_action_$moduleId\"\n        val shortcutIntent = Intent(context, MainActivity::class.java).apply {\n            action = Intent.ACTION_VIEW\n            putExtra(\"shortcut_type\", \"module_action\")\n            putExtra(\"module_id\", moduleId)\n            addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)\n        }\n        createModuleShortcut(\n            context = context,\n            moduleId = moduleId,\n            name = name,\n            iconUri = iconUri,\n            shortcutId = shortcutId,\n            shortcutIntent = shortcutIntent,\n            logPrefix = \"createModuleActionShortcut\"\n        )\n    }\n\n    fun createModuleWebUiShortcut(\n        context: Context,\n        moduleId: String,\n        name: String,\n        iconUri: String?\n    ) {\n        val shortcutId = \"module_webui_$moduleId\"\n        val shortcutIntent = Intent(context, WebUIActivity::class.java).apply {\n            action = Intent.ACTION_VIEW\n            data = \"kernelsu://webui/$moduleId\".toUri()\n            putExtra(\"id\", moduleId)\n            putExtra(\"from_webui_shortcut\", true)\n            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)\n        }\n        createModuleShortcut(\n            context = context,\n            moduleId = moduleId,\n            name = name,\n            iconUri = iconUri,\n            shortcutId = shortcutId,\n            shortcutIntent = shortcutIntent,\n            logPrefix = \"createModuleWebUiShortcut\"\n        )\n    }\n\n    private fun createModuleShortcut(\n        context: Context,\n        moduleId: String,\n        name: String,\n        iconUri: String?,\n        shortcutId: String,\n        shortcutIntent: Intent,\n        logPrefix: String\n    ) {\n        val hasPinned = hasPinnedShortcut(context, shortcutId)\n        Log.d(TAG, \"$logPrefix: shortcutId=$shortcutId, hasPinned=$hasPinned\")\n\n        val iconCompat = createShortcutIcon(context, iconUri)\n        val finalIcon = iconCompat ?: IconCompat.createWithResource(context, R.mipmap.ic_launcher)\n\n        val shortcut = ShortcutInfoCompat.Builder(context, shortcutId)\n            .setShortLabel(name)\n            .setIntent(shortcutIntent)\n            .setIcon(finalIcon)\n            .build()\n\n        try {\n            Log.d(TAG, \"$logPrefix: pushDynamicShortcut() called for moduleId=$moduleId\")\n            ShortcutManagerCompat.pushDynamicShortcut(context, shortcut)\n        } catch (t: Throwable) {\n            Log.w(TAG, \"$logPrefix: pushDynamicShortcut() threw exception for moduleId=$moduleId: ${t.message}\", t)\n        }\n\n        if (hasPinned) {\n            Log.d(TAG, \"$logPrefix: detected existing pinned shortcut, updating only\")\n            Toast.makeText(context, context.getString(R.string.module_shortcut_updated), Toast.LENGTH_SHORT).show()\n            return\n        }\n\n        val initialState = getShortcutPermissionState(context)\n        Log.d(TAG, \"$logPrefix: initial permission state=$initialState\")\n        if ((isMiui() || isHyperOS()) && initialState != ShortcutPermissionState.Granted) {\n            Log.d(TAG, \"$logPrefix: device is Xiaomi, trying to grant via root shell\")\n            val rootSuccess = tryGrantMiuiShortcutPermissionByRoot(context)\n            Log.d(TAG, \"$logPrefix: root grant attempt success=$rootSuccess\")\n            val afterState = getShortcutPermissionState(context)\n            Log.d(TAG, \"$logPrefix: state after root attempt=$afterState\")\n            if (afterState != ShortcutPermissionState.Granted) {\n                Log.d(TAG, \"$logPrefix: still not Granted after root, showing hint\")\n                showShortcutPermissionHint(context)\n                return\n            }\n        } else if (initialState == ShortcutPermissionState.Denied || initialState == ShortcutPermissionState.Ask) {\n            Log.d(TAG, \"$logPrefix: permission not granted (state=$initialState), showing hint first\")\n            showShortcutPermissionHint(context)\n            return\n        }\n        if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) {\n            Log.w(TAG, \"$logPrefix: requestPinShortcut not supported on this launcher\")\n            Toast.makeText(\n                context,\n                context.getString(R.string.module_shortcut_not_supported),\n                Toast.LENGTH_LONG\n            ).show()\n            return\n        }\n\n        val pinned = try {\n            Log.d(TAG, \"$logPrefix: requestPinShortcut() called for moduleId=$moduleId\")\n            val result = ShortcutManagerCompat.requestPinShortcut(context, shortcut, null)\n            Log.d(TAG, \"$logPrefix: requestPinShortcut() result=$result\")\n            result\n        } catch (t: Throwable) {\n            Log.w(TAG, \"$logPrefix: requestPinShortcut() threw exception for moduleId=$moduleId: ${t.message}\", t)\n            false\n        }\n\n        if (pinned) {\n            Log.d(TAG, \"$logPrefix: pinned shortcut created successfully for moduleId=$moduleId\")\n            Toast.makeText(\n                context,\n                context.getString(R.string.module_shortcut_created),\n                Toast.LENGTH_SHORT\n            ).show()\n        } else {\n            Log.w(TAG, \"$logPrefix: pinned shortcut not created, showing permission hint for moduleId=$moduleId\")\n            showShortcutPermissionHint(context)\n        }\n    }\n\n    fun hasModuleActionShortcut(context: Context, moduleId: String): Boolean {\n        val id = \"module_action_$moduleId\"\n        return hasPinnedShortcut(context, id)\n    }\n\n    fun hasModuleWebUiShortcut(context: Context, moduleId: String): Boolean {\n        val id = \"module_webui_$moduleId\"\n        return hasPinnedShortcut(context, id)\n    }\n\n    fun deleteModuleActionShortcut(context: Context, moduleId: String) {\n        deleteShortcut(context, \"module_action_$moduleId\")\n    }\n\n    fun deleteModuleWebUiShortcut(context: Context, moduleId: String) {\n        deleteShortcut(context, \"module_webui_$moduleId\")\n    }\n\n    fun loadShortcutBitmap(context: Context, iconUri: String?): Bitmap? {\n        if (iconUri.isNullOrBlank()) {\n            return null\n        }\n        return try {\n            val uri = iconUri.toUri()\n            Log.d(TAG, \"loadShortcutBitmap: loading bitmap from uri=$uri\")\n            val rawBitmap = if (uri.scheme.equals(\"su\", ignoreCase = true)) {\n                val path = uri.path ?: \"\"\n                if (path.isNotBlank()) {\n                    val shell = getRootShell(true)\n                    val suFile = SuFile(path)\n                    suFile.shell = shell\n                    SuFileInputStream.open(suFile).use { input ->\n                        BitmapFactory.decodeStream(input)\n                    }\n                } else null\n            } else {\n                context.contentResolver.openInputStream(uri)?.use { input ->\n                    BitmapFactory.decodeStream(input)\n                }\n            }\n            if (rawBitmap != null) {\n                Log.d(TAG, \"loadShortcutBitmap: decoded bitmap successfully\")\n                val w = rawBitmap.width\n                val h = rawBitmap.height\n                val side = minOf(w, h)\n                val x = (w - side) / 2\n                val y = (h - side) / 2\n                val square = try {\n                    Bitmap.createBitmap(rawBitmap, x, y, side, side)\n                } catch (_: Throwable) {\n                    rawBitmap\n                }\n                if (square !== rawBitmap && !rawBitmap.isRecycled) {\n                    rawBitmap.recycle()\n                }\n                if (side > 512) {\n                    try {\n                        val scaled = square.scale(512, 512)\n                        if (scaled !== square && !square.isRecycled) {\n                            square.recycle()\n                        }\n                        scaled\n                    } catch (_: Throwable) {\n                        square\n                    }\n                } else {\n                    square\n                }\n            } else {\n                Log.w(TAG, \"loadShortcutBitmap: bitmap decode returned null\")\n                null\n            }\n        } catch (t: Throwable) {\n            Log.w(TAG, \"loadShortcutBitmap: exception when loading icon from uri=$iconUri: ${t.message}\", t)\n            null\n        }\n    }\n\n    private fun createShortcutIcon(context: Context, iconUri: String?): IconCompat? {\n        val bitmap = loadShortcutBitmap(context, iconUri) ?: return null\n        return IconCompat.createWithBitmap(bitmap)\n    }\n\n    private fun hasPinnedShortcut(context: Context, id: String): Boolean {\n        return try {\n            val shortcuts = ShortcutManagerCompat.getShortcuts(\n                context,\n                ShortcutManagerCompat.FLAG_MATCH_PINNED\n            )\n            val exists = shortcuts.any { it.id == id && it.isEnabled }\n            Log.d(TAG, \"hasPinnedShortcut: id=$id, exists=$exists\")\n            exists\n        } catch (t: Throwable) {\n            Log.w(TAG, \"hasPinnedShortcut: exception for id=$id: ${t.message}\", t)\n            false\n        }\n    }\n\n    private fun deleteShortcut(context: Context, id: String) {\n        try {\n            ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(id))\n            Log.d(TAG, \"deleteShortcut: removed dynamic shortcut id=$id\")\n        } catch (t: Throwable) {\n            Log.w(TAG, \"deleteShortcut: removeDynamicShortcuts exception for id=$id: ${t.message}\", t)\n        }\n        try {\n            ShortcutManagerCompat.disableShortcuts(context, listOf(id), \"\")\n            Log.d(TAG, \"deleteShortcut: disabled shortcut id=$id\")\n        } catch (t: Throwable) {\n            Log.w(TAG, \"deleteShortcut: disableShortcuts exception for id=$id: ${t.message}\", t)\n        }\n    }\n\n    private enum class ShortcutPermissionState {\n        Granted,\n        Denied,\n        Ask,\n        Unknown\n    }\n\n    private fun checkMiuiShortcutPermission(context: Context): ShortcutPermissionState {\n        return try {\n            val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as? AppOpsManager\n                ?: return ShortcutPermissionState.Unknown\n            val pkg = context.applicationContext.packageName\n            val uid = context.applicationInfo.uid\n            Log.d(TAG, \"checkMiuiShortcutPermission: pkg=$pkg, uid=$uid\")\n            val appOpsClass = Class.forName(AppOpsManager::class.java.name)\n            val method = appOpsClass.getDeclaredMethod(\n                \"checkOpNoThrow\",\n                Integer.TYPE,\n                Integer.TYPE,\n                String::class.java\n            )\n            val result = method.invoke(appOps, 10017, uid, pkg)?.toString()\n            if (result == null) {\n                Log.w(TAG, \"checkMiuiShortcutPermission: checkOpNoThrow returned null\")\n                return ShortcutPermissionState.Unknown\n            }\n            Log.d(TAG, \"checkMiuiShortcutPermission: raw result=$result\")\n            val state = when (result) {\n                \"0\" -> ShortcutPermissionState.Granted\n                \"1\" -> ShortcutPermissionState.Denied\n                \"5\" -> ShortcutPermissionState.Ask\n                else -> ShortcutPermissionState.Unknown\n            }\n            Log.d(TAG, \"checkMiuiShortcutPermission: mapped state=$state\")\n            state\n        } catch (t: Throwable) {\n            Log.w(TAG, \"checkMiuiShortcutPermission: exception=${t.message}\", t)\n            ShortcutPermissionState.Unknown\n        }\n    }\n\n    private fun checkOppoShortcutPermission(context: Context): ShortcutPermissionState {\n        val resolver = context.contentResolver ?: run {\n            Log.w(TAG, \"checkOppoShortcutPermission: contentResolver is null\")\n            return ShortcutPermissionState.Unknown\n        }\n        val uri = \"content://settings/secure/launcher_shortcut_permission_settings\".toUri()\n        val cursor = resolver.query(uri, null, null, null, null) ?: run {\n            Log.w(TAG, \"checkOppoShortcutPermission: query returned null cursor, uri=$uri\")\n            return ShortcutPermissionState.Unknown\n        }\n        cursor.use { c ->\n            val pkg = context.applicationContext.packageName\n            val index = c.getColumnIndex(\"value\")\n            if (index == -1) {\n                Log.w(TAG, \"checkOppoShortcutPermission: 'value' column not found\")\n                return ShortcutPermissionState.Unknown\n            }\n            Log.d(TAG, \"checkOppoShortcutPermission: pkg=$pkg\")\n            while (c.moveToNext()) {\n                val value = c.getString(index)\n                if (!value.isNullOrEmpty()) {\n                    Log.d(TAG, \"checkOppoShortcutPermission: row value=$value\")\n                    if (value.contains(\"$pkg, 1\")) {\n                        Log.d(TAG, \"checkOppoShortcutPermission: detected Granted\")\n                        return ShortcutPermissionState.Granted\n                    }\n                    if (value.contains(\"$pkg, 0\")) {\n                        Log.d(TAG, \"checkOppoShortcutPermission: detected Denied\")\n                        return ShortcutPermissionState.Denied\n                    }\n                }\n            }\n        }\n        return ShortcutPermissionState.Unknown\n    }\n\n    private fun tryGrantMiuiShortcutPermissionByRoot(context: Context): Boolean {\n        val pkg = context.applicationContext.packageName\n        val cmd = \"appops set $pkg 10017 allow\"\n        return try {\n            val shell = getRootShell()\n            val result = shell.newJob().add(cmd).exec()\n            Log.d(TAG, \"tryGrantMiuiShortcutPermissionByRoot: cmd=$cmd, code=${result.code}, isSuccess=${result.isSuccess}\")\n            result.isSuccess\n        } catch (t: Throwable) {\n            Log.w(TAG, \"tryGrantMiuiShortcutPermissionByRoot: exception=${t.message}\", t)\n            false\n        }\n    }\n\n    private fun getShortcutPermissionState(context: Context): ShortcutPermissionState {\n        return when {\n            isMiui() || isHyperOS() -> checkMiuiShortcutPermission(context)\n            isColorOS() -> checkOppoShortcutPermission(context)\n            else -> ShortcutPermissionState.Unknown\n        }\n    }\n\n    private fun showShortcutPermissionHint(context: Context) {\n        val state = getShortcutPermissionState(context)\n        val messageRes = when {\n            isMiui() || isHyperOS() -> R.string.module_shortcut_permission_tip_xiaomi\n            isColorOS() -> R.string.module_shortcut_permission_tip_oppo\n            else -> R.string.module_shortcut_permission_tip_default\n        }\n        Log.d(TAG, \"showShortcutPermissionHint: state=$state, messageRes=$messageRes\")\n        Toast.makeText(context, context.getString(messageRes), Toast.LENGTH_LONG).show()\n        if (state != ShortcutPermissionState.Granted) {\n            Log.d(TAG, \"showShortcutPermissionHint: state is not Granted, opening app details settings\")\n            openAppDetailsSettings(context)\n        }\n    }\n\n    private fun openAppDetailsSettings(context: Context) {\n        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {\n            data = Uri.fromParts(\"package\", context.packageName, null)\n            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n        }\n        try {\n            Log.d(TAG, \"openAppDetailsSettings: launching settings for package=${context.packageName}\")\n            context.startActivity(intent)\n        } catch (t: Throwable) {\n            Log.w(TAG, \"openAppDetailsSettings: failed to launch settings: ${t.message}\", t)\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/HomeViewModel.kt",
    "content": "package me.weishu.kernelsu.ui.viewmodel\r\n\r\nimport android.content.Context\r\nimport android.os.Build\r\nimport android.system.Os\r\nimport androidx.lifecycle.ViewModel\r\nimport androidx.lifecycle.viewModelScope\r\nimport kotlinx.coroutines.Dispatchers\r\nimport kotlinx.coroutines.flow.MutableStateFlow\r\nimport kotlinx.coroutines.flow.StateFlow\r\nimport kotlinx.coroutines.flow.asStateFlow\r\nimport kotlinx.coroutines.flow.update\r\nimport kotlinx.coroutines.launch\r\nimport kotlinx.coroutines.withContext\r\nimport me.weishu.kernelsu.BuildConfig\r\nimport me.weishu.kernelsu.Natives\r\nimport me.weishu.kernelsu.getKernelVersion\r\nimport me.weishu.kernelsu.ksuApp\r\nimport me.weishu.kernelsu.ui.screen.home.HomeUiState\r\nimport me.weishu.kernelsu.ui.screen.home.SystemInfo\r\nimport me.weishu.kernelsu.ui.screen.home.getManagerVersion\r\nimport me.weishu.kernelsu.ui.util.checkNewVersion\r\nimport me.weishu.kernelsu.ui.util.getModuleCount\r\nimport me.weishu.kernelsu.ui.util.getSuperuserCount\r\nimport me.weishu.kernelsu.ui.util.isSELinuxPermissive\r\nimport me.weishu.kernelsu.ui.util.module.LatestVersionInfo\r\nimport me.weishu.kernelsu.ui.util.rootAvailable\r\n\r\nclass HomeViewModel : ViewModel() {\r\n\r\n    private val _uiState = MutableStateFlow(buildState())\r\n    val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()\r\n\r\n    fun refresh() {\n        viewModelScope.launch {\n            val baseState = withContext(Dispatchers.IO) { buildState() }\n            _uiState.update { current ->\n                baseState.copy(systemInfo = current.systemInfo)\n            }\n            if (baseState.checkUpdateEnabled) {\n                val latestVersionInfo = withContext(Dispatchers.IO) { checkNewVersion() }\n                _uiState.update { it.copy(latestVersionInfo = latestVersionInfo) }\n            }\n        }\n    }\r\n\r\n    fun updateSystemInfo(info: SystemInfo) = _uiState.update { it.copy(systemInfo = info) }\r\n\r\n    private fun buildState(): HomeUiState {\r\n        val kernelVersion = getKernelVersion()\r\n        val isManager = Natives.isManager\r\n        val ksuVersion = if (isManager) Natives.version else null\r\n        val lkmMode = ksuVersion?.let { if (kernelVersion.isGKI()) Natives.isLkmMode else null }\r\n        val isRootAvailable = rootAvailable()\r\n        val managerVersion = getManagerVersion(ksuApp)\r\n\r\n        return HomeUiState(\r\n            kernelVersion = kernelVersion,\r\n            ksuVersion = ksuVersion,\r\n            lkmMode = lkmMode,\r\n            isManager = isManager,\r\n            isManagerPrBuild = BuildConfig.IS_PR_BUILD,\r\n            isKernelPrBuild = Natives.isPrBuild,\r\n            requiresNewKernel = isManager && Natives.requireNewKernel(),\r\n            isRootAvailable = isRootAvailable,\r\n            isSafeMode = Natives.isSafeMode,\r\n            isLateLoadMode = Natives.isLateLoadMode,\r\n            isSELinuxPermissive = isSELinuxPermissive(),\r\n            checkUpdateEnabled = ksuApp.getSharedPreferences(\"settings\", Context.MODE_PRIVATE)\r\n                .getBoolean(\"check_update\", true),\r\n            latestVersionInfo = LatestVersionInfo(),\r\n            currentManagerVersionCode = managerVersion.versionCode,\r\n            superuserCount = getSuperuserCount(),\r\n            moduleCount = getModuleCount(),\r\n            systemInfo = SystemInfo(\r\n                kernelVersion = Os.uname().release,\r\n                managerVersion = \"${managerVersion.versionName} (${managerVersion.versionCode})\",\r\n                fingerprint = Build.FINGERPRINT,\r\n                selinuxStatus = \"Unknown\",\r\n            ),\r\n        )\r\n    }\r\n}\r\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/MainActivityUiState.kt",
    "content": "package me.weishu.kernelsu.ui.viewmodel\r\n\r\nimport me.weishu.kernelsu.ui.UiMode\r\nimport me.weishu.kernelsu.ui.theme.AppSettings\r\n\r\ndata class MainActivityUiState(\r\n    val appSettings: AppSettings,\r\n    val pageScale: Float,\r\n    val enableBlur: Boolean,\r\n    val enableFloatingBottomBar: Boolean,\r\n    val enableFloatingBottomBarBlur: Boolean,\r\n    val uiMode: UiMode,\r\n)\r\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/MainActivityViewModel.kt",
    "content": "package me.weishu.kernelsu.ui.viewmodel\r\n\r\nimport android.content.Context\r\nimport android.content.SharedPreferences\r\nimport androidx.lifecycle.ViewModel\r\nimport kotlinx.coroutines.flow.MutableStateFlow\r\nimport kotlinx.coroutines.flow.StateFlow\r\nimport kotlinx.coroutines.flow.asStateFlow\r\nimport me.weishu.kernelsu.data.repository.SettingsRepository\r\nimport me.weishu.kernelsu.data.repository.SettingsRepositoryImpl\r\nimport me.weishu.kernelsu.ksuApp\r\nimport me.weishu.kernelsu.ui.UiMode\r\nimport me.weishu.kernelsu.ui.theme.ThemeController\r\n\r\nclass MainActivityViewModel : ViewModel() {\r\n\r\n    private val prefs = ksuApp.getSharedPreferences(\"settings\", Context.MODE_PRIVATE)\r\n    private val settingRepo: SettingsRepository = SettingsRepositoryImpl()\r\n    private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->\r\n        if (key == null || key in observedKeys) {\r\n            _uiState.value = readUiState()\r\n        }\r\n    }\r\n\r\n    private val _uiState = MutableStateFlow(readUiState())\r\n    val uiState: StateFlow<MainActivityUiState> = _uiState.asStateFlow()\r\n\r\n    init {\r\n        prefs.registerOnSharedPreferenceChangeListener(listener)\r\n    }\r\n\r\n    override fun onCleared() {\r\n        prefs.unregisterOnSharedPreferenceChangeListener(listener)\r\n        super.onCleared()\r\n    }\r\n\r\n    private fun readUiState(): MainActivityUiState {\r\n        return MainActivityUiState(\r\n            appSettings = ThemeController.getAppSettings(ksuApp),\r\n            pageScale = settingRepo.pageScale,\r\n            enableBlur = settingRepo.enableBlur,\r\n            enableFloatingBottomBar = settingRepo.enableFloatingBottomBar,\r\n            enableFloatingBottomBarBlur = settingRepo.enableFloatingBottomBarBlur,\r\n            uiMode = UiMode.fromValue(settingRepo.uiMode),\r\n        )\r\n    }\r\n\r\n    private companion object {\r\n        val observedKeys = setOf(\r\n            \"color_mode\",\r\n            \"key_color\",\r\n            \"color_style\",\r\n            \"color_spec\",\r\n            \"page_scale\",\r\n            \"enable_blur\",\r\n            \"enable_floating_bottom_bar\",\r\n            \"enable_floating_bottom_bar_blur\",\r\n            \"ui_mode\",\r\n        )\r\n    }\r\n}\r\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleRepoViewModel.kt",
    "content": "package me.weishu.kernelsu.ui.viewmodel\n\nimport android.content.Context\nimport android.util.Log\nimport android.widget.Toast\nimport androidx.core.content.edit\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.data.repository.ModuleRepoRepository\nimport me.weishu.kernelsu.data.repository.ModuleRepoRepositoryImpl\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.component.SearchStatus\nimport me.weishu.kernelsu.ui.screen.modulerepo.ModuleRepoUiState\nimport me.weishu.kernelsu.ui.util.isNetworkAvailable\n\nclass ModuleRepoViewModel(\n    private val repo: ModuleRepoRepository = ModuleRepoRepositoryImpl()\n) : ViewModel() {\n\n    companion object {\n        private const val TAG = \"ModuleRepoViewModel\"\n    }\n\n    typealias RepoModule = me.weishu.kernelsu.data.model.RepoModule\n\n    private val _uiState = MutableStateFlow(ModuleRepoUiState())\n    val uiState: StateFlow<ModuleRepoUiState> = _uiState.asStateFlow()\n\n    private val prefs = ksuApp.getSharedPreferences(\"settings\", Context.MODE_PRIVATE)\n    private val searchQuery = MutableStateFlow(\"\")\n\n    init {\n        _uiState.update {\n            it.copy(\n                sortByName = prefs.getBoolean(\"module_repo_sort_name\", false),\n                offline = !isNetworkAvailable(ksuApp)\n            )\n        }\n\n        viewModelScope.launchSearchQueryCollector(searchQuery, ::applySearchText)\n    }\n\n    private fun filterModules(modules: List<RepoModule>, text: String): List<RepoModule> {\n        if (text.isEmpty()) return emptyList()\n\n        return modules.filter {\n            it.moduleId.contains(text, true) ||\n                    it.moduleName.contains(text, true) ||\n                    it.authors.contains(text, true) ||\n                    it.summary.contains(text, true) ||\n                    me.weishu.kernelsu.ui.util.HanziToPinyin.getInstance().toPinyinString(it.moduleName)\n                        .contains(text, true)\n        }\n    }\n\n    private suspend fun applySearchText(text: String) {\n        _uiState.update {\n            it.copy(\n                searchStatus = it.searchStatus.copy(\n                    resultStatus = searchLoadingStatusFor(text)\n                )\n            )\n        }\n\n        if (text.isEmpty()) {\n            _uiState.update { state ->\n                state.copy(\n                    searchResults = emptyList(),\n                    searchStatus = state.searchStatus.copy(resultStatus = SearchStatus.ResultStatus.DEFAULT)\n                )\n            }\n            return\n        }\n\n        val result = withContext(Dispatchers.IO) {\n            filterModules(_uiState.value.modules, text)\n        }\n\n        _uiState.update {\n            it.copy(\n                searchResults = result,\n                searchStatus = it.searchStatus.copy(resultStatus = searchResultStatusFor(text, result.isEmpty()))\n            )\n        }\n    }\n\n    private fun refreshSearchResults() {\n        val state = _uiState.value\n        val text = state.searchStatus.searchText\n        val results = filterModules(state.modules, text)\n        _uiState.update {\n            it.copy(\n                searchResults = results,\n                searchStatus = it.searchStatus.copy(resultStatus = searchResultStatusFor(text, results.isEmpty()))\n            )\n        }\n    }\n\n    fun refresh() {\n        viewModelScope.launch {\n            _uiState.update {\n                it.copy(\n                    isRefreshing = true,\n                    error = null,\n                    offline = !isNetworkAvailable(ksuApp)\n                )\n            }\n            val result = repo.fetchModules()\n\n            withContext(Dispatchers.Main) {\n                result.onSuccess { modules ->\n                    _uiState.update {\n                        it.copy(\n                            modules = modules,\n                            isRefreshing = false,\n                            offline = !isNetworkAvailable(ksuApp)\n                        )\n                    }\n                    refreshSearchResults()\n                }.onFailure { e ->\n                    Log.e(TAG, \"fetch modules failed\", e)\n                    Toast.makeText(\n                        ksuApp,\n                        ksuApp.getString(R.string.network_offline), Toast.LENGTH_SHORT\n                    ).show()\n                    _uiState.update {\n                        it.copy(\n                            isRefreshing = false,\n                            error = e,\n                            offline = !isNetworkAvailable(ksuApp)\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    fun toggleSortByName() {\n        val newValue = !_uiState.value.sortByName\n        prefs.edit { putBoolean(\"module_repo_sort_name\", newValue) }\n        _uiState.update { it.copy(sortByName = newValue) }\n    }\n\n    fun updateSearchStatus(status: SearchStatus) {\n        val previous = _uiState.value.searchStatus\n        _uiState.update { it.copy(searchStatus = status) }\n        if (previous.searchText != status.searchText) {\n            searchQuery.value = status.searchText\n        }\n    }\n\n    fun updateSearchText(text: String) {\n        updateSearchStatus(_uiState.value.searchStatus.copy(searchText = text))\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt",
    "content": "package me.weishu.kernelsu.ui.viewmodel\n\nimport android.os.SystemClock\nimport android.util.Log\nimport androidx.core.content.edit\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.async\nimport kotlinx.coroutines.awaitAll\nimport kotlinx.coroutines.coroutineScope\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.data.model.Module\nimport me.weishu.kernelsu.data.model.ModuleUpdateInfo\nimport me.weishu.kernelsu.data.repository.ModuleRepository\nimport me.weishu.kernelsu.data.repository.ModuleRepositoryImpl\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.component.SearchStatus\nimport me.weishu.kernelsu.ui.screen.module.ModuleUiState\nimport me.weishu.kernelsu.ui.util.hasMagisk\nimport java.text.Collator\nimport java.util.Locale\n\nclass ModuleViewModel(\n    private val repo: ModuleRepository = ModuleRepositoryImpl()\n) : ViewModel() {\n\n    companion object {\n        private const val TAG = \"ModuleViewModel\"\n    }\n\n    private data class ModuleUpdateSignature(\n        val updateJson: String,\n        val versionCode: Int,\n        val enabled: Boolean,\n        val update: Boolean,\n        val remove: Boolean\n    )\n\n    private data class ModuleUpdateCache(\n        val signature: ModuleUpdateSignature,\n        val info: ModuleUpdateInfo\n    )\n\n    private val _uiState = MutableStateFlow(ModuleUiState())\n    val uiState: StateFlow<ModuleUiState> = _uiState.asStateFlow()\n\n    private val updateInfoMutex = Mutex()\n    private var updateInfoCache: MutableMap<String, ModuleUpdateCache> = mutableMapOf()\n    private val updateInfoInFlight = mutableSetOf<String>()\n    private val searchQuery = MutableStateFlow(\"\")\n\n    var isNeedRefresh = false\n        private set\n\n    init {\n        viewModelScope.launchSearchQueryCollector(searchQuery, ::applySearchText)\n    }\n\n    fun markNeedRefresh() {\n        isNeedRefresh = true\n    }\n\n    fun initializePreferences() {\n        val prefs = ksuApp.getSharedPreferences(\"settings\", 0)\n        _uiState.update {\n            it.copy(\n                checkModuleUpdate = prefs.getBoolean(\"module_check_update\", true),\n                sortEnabledFirst = prefs.getBoolean(\"module_sort_enabled_first\", false),\n                sortActionFirst = prefs.getBoolean(\"module_sort_action_first\", false),\n            )\n        }\n        updateModuleList()\n    }\n\n    fun toggleSortActionFirst() {\n        val newValue = !_uiState.value.sortActionFirst\n        ksuApp.getSharedPreferences(\"settings\", 0).edit {\n            putBoolean(\"module_sort_action_first\", newValue)\n        }\n        _uiState.update { it.copy(sortActionFirst = newValue) }\n        updateModuleList()\n    }\n\n    fun toggleSortEnabledFirst() {\n        val newValue = !_uiState.value.sortEnabledFirst\n        ksuApp.getSharedPreferences(\"settings\", 0).edit {\n            putBoolean(\"module_sort_enabled_first\", newValue)\n        }\n        _uiState.update { it.copy(sortEnabledFirst = newValue) }\n        updateModuleList()\n    }\n\n    fun refreshEnvironmentState() {\n        viewModelScope.launch {\n            val magiskInstalled = withContext(Dispatchers.IO) { hasMagisk() }\n            val isSafeMode = Natives.isSafeMode\n            _uiState.update {\n                it.copy(\n                    magiskInstalled = magiskInstalled,\n                    isSafeMode = isSafeMode,\n                )\n            }\n        }\n    }\n\n    fun updateSearchStatus(status: SearchStatus) {\n        val previous = _uiState.value.searchStatus\n        _uiState.update { it.copy(searchStatus = status) }\n        if (previous.searchText != status.searchText) {\n            searchQuery.value = status.searchText\n        }\n    }\n\n    fun updateSearchText(text: String) {\n        updateSearchStatus(_uiState.value.searchStatus.copy(searchText = text))\n    }\n\n    private fun filterModules(modules: List<Module>, text: String): List<Module> {\n        if (text.isEmpty()) return emptyList()\n\n        return modules.filter {\n            it.id.contains(text, true) || it.name.contains(text, true) ||\n                    it.description.contains(text, true) || it.author.contains(text, true) ||\n                    me.weishu.kernelsu.ui.util.HanziToPinyin.getInstance().toPinyinString(it.name)\n                        .contains(text, true)\n        }\n    }\n\n    private suspend fun applySearchText(text: String) {\n        _uiState.update {\n            it.copy(\n                searchStatus = it.searchStatus.copy(\n                    resultStatus = searchLoadingStatusFor(text)\n                )\n            )\n        }\n\n        if (text.isEmpty()) {\n            updateModuleList()\n            return\n        }\n\n        val result = withContext(Dispatchers.IO) {\n            val state = _uiState.value\n            filterModules(state.modules, text).sortedWith(moduleComparator(state))\n        }\n\n        _uiState.update {\n            it.copy(\n                searchResults = result,\n                searchStatus = it.searchStatus.copy(\n                    resultStatus = searchResultStatusFor(text, result.isEmpty())\n                )\n            )\n        }\n    }\n\n    private fun updateModuleList() {\n        viewModelScope.launch(Dispatchers.IO) {\n            val state = _uiState.value\n            val searchText = state.searchStatus.searchText\n            val shorted = state.modules.sortedWith(moduleComparator(state))\n            val searchResults = filterModules(shorted, searchText)\n\n            _uiState.update {\n                it.copy(\n                    moduleList = shorted,\n                    searchResults = searchResults,\n                    searchStatus = it.searchStatus.copy(\n                        resultStatus = searchResultStatusFor(searchText, searchResults.isEmpty())\n                    )\n                )\n            }\n        }\n    }\n\n    private fun moduleComparator(state: ModuleUiState): Comparator<Module> {\n        return compareBy<Module>(\n            {\n                val executable = it.hasWebUi || it.hasActionScript\n                when {\n                    it.metamodule && it.enabled -> 0\n                    state.sortEnabledFirst && state.sortActionFirst -> when {\n                        it.enabled && executable -> 1\n                        it.enabled -> 2\n                        executable -> 3\n                        else -> 4\n                    }\n\n                    state.sortEnabledFirst && !state.sortActionFirst -> if (it.enabled) 1 else 2\n                    !state.sortEnabledFirst && state.sortActionFirst -> if (executable) 1 else 2\n                    else -> 1\n                }\n            },\n            { if (state.sortEnabledFirst) !it.enabled else 0 },\n            { if (state.sortActionFirst) !(it.hasWebUi || it.hasActionScript) else 0 },\n        ).thenBy(Collator.getInstance(Locale.getDefault()), Module::id)\n    }\n\n    suspend fun loadModuleList() {\n        val parsedModules = withContext(Dispatchers.IO) {\n            repo.getModules().getOrElse {\n                Log.e(TAG, \"fetchModuleList: \", it)\n                emptyList()\n            }\n        }\n\n        withContext(Dispatchers.Main) {\n            _uiState.update {\n                it.copy(\n                    modules = parsedModules,\n                )\n            }\n            // Trigger recalculation of moduleList\n            updateModuleList()\n            isNeedRefresh = false\n        }\n    }\n\n    fun fetchModuleList(checkUpdate: Boolean = false) {\n        viewModelScope.launch {\n            _uiState.update { it.copy(isRefreshing = true) }\n\n            val start = SystemClock.elapsedRealtime()\n\n            loadModuleList()\n\n            if (checkUpdate) syncModuleUpdateInfo(_uiState.value.modules)\n\n            _uiState.update { it.copy(isRefreshing = false) }\n\n            Log.i(TAG, \"load cost: ${SystemClock.elapsedRealtime() - start}, modules: ${_uiState.value.modules}\")\n        }\n    }\n\n    private fun Module.toSignature(): ModuleUpdateSignature {\n        return ModuleUpdateSignature(\n            updateJson = updateJson,\n            versionCode = versionCode,\n            enabled = enabled,\n            update = update,\n            remove = remove\n        )\n    }\n\n    suspend fun syncModuleUpdateInfo(modules: List<Module>) {\n        if (!_uiState.value.checkModuleUpdate) return\n\n        val modulesToFetch = mutableListOf<Triple<String, Module, ModuleUpdateSignature>>()\n        val removedIds = mutableSetOf<String>()\n\n        updateInfoMutex.withLock {\n            val ids = modules.map { it.id }.toSet()\n            updateInfoCache.keys.filter { it !in ids }.forEach { removedId ->\n                removedIds += removedId\n                updateInfoCache.remove(removedId)\n                updateInfoInFlight.remove(removedId)\n            }\n\n            modules.forEach { module ->\n                val signature = module.toSignature()\n                val cached = updateInfoCache[module.id]\n                if ((cached == null || cached.signature != signature) && updateInfoInFlight.add(module.id)) {\n                    modulesToFetch += Triple(module.id, module, signature)\n                }\n            }\n        }\n\n        val fetchedEntries = coroutineScope {\n            modulesToFetch.map { (id, module, signature) ->\n                async(Dispatchers.IO) {\n                    id to ModuleUpdateCache(signature, checkUpdate(module))\n                }\n            }.awaitAll()\n        }\n\n        val changedEntries = mutableListOf<Pair<String, ModuleUpdateInfo>>()\n        updateInfoMutex.withLock {\n            fetchedEntries.forEach { (id, entry) ->\n                val existing = updateInfoCache[id]\n                if (existing == null || existing.signature != entry.signature || existing.info != entry.info) {\n                    updateInfoCache[id] = entry\n                    changedEntries += id to entry.info\n                }\n                updateInfoInFlight.remove(id)\n            }\n        }\n\n        if (removedIds.isEmpty() && changedEntries.isEmpty()) {\n            return\n        }\n\n        withContext(Dispatchers.Main) {\n            _uiState.update { state ->\n                val newMap = state.updateInfo.toMutableMap()\n                removedIds.forEach { newMap.remove(it) }\n                changedEntries.forEach { (id, info) ->\n                    newMap[id] = info\n                }\n                state.copy(updateInfo = newMap)\n            }\n        }\n    }\n\n    private suspend fun checkUpdate(m: Module): ModuleUpdateInfo {\n        return repo.checkUpdate(m).getOrDefault(ModuleUpdateInfo.Empty)\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SearchViewModelHelper.kt",
    "content": "package me.weishu.kernelsu.ui.viewmodel\n\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.FlowPreview\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.collectLatest\nimport kotlinx.coroutines.flow.debounce\nimport kotlinx.coroutines.flow.distinctUntilChanged\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.ui.component.SearchStatus\n\nprivate const val SEARCH_DEBOUNCE_MILLIS = 150L\n\n@OptIn(FlowPreview::class)\nfun CoroutineScope.launchSearchQueryCollector(\n    searchQuery: StateFlow<String>,\n    onQuery: suspend (String) -> Unit,\n): Job {\n    return launch {\n        searchQuery\n            .debounce(SEARCH_DEBOUNCE_MILLIS)\n            .distinctUntilChanged()\n            .collectLatest(onQuery)\n    }\n}\n\nfun searchLoadingStatusFor(text: String): SearchStatus.ResultStatus {\n    return if (text.isEmpty()) {\n        SearchStatus.ResultStatus.DEFAULT\n    } else {\n        SearchStatus.ResultStatus.LOAD\n    }\n}\n\nfun searchResultStatusFor(text: String, isEmpty: Boolean): SearchStatus.ResultStatus {\n    return when {\n        text.isEmpty() -> SearchStatus.ResultStatus.DEFAULT\n        isEmpty -> SearchStatus.ResultStatus.EMPTY\n        else -> SearchStatus.ResultStatus.SHOW\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SettingsViewModel.kt",
    "content": "package me.weishu.kernelsu.ui.viewmodel\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.data.repository.SettingsRepository\nimport me.weishu.kernelsu.data.repository.SettingsRepositoryImpl\nimport me.weishu.kernelsu.ui.screen.settings.SettingsUiState\nimport me.weishu.kernelsu.ui.theme.ColorMode\n\nclass SettingsViewModel(\n    private val repo: SettingsRepository = SettingsRepositoryImpl()\n) : ViewModel() {\n\n    private val _uiState = MutableStateFlow(SettingsUiState())\n    val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()\n\n    init {\n        refresh()\n    }\n\n    fun refresh() {\n        viewModelScope.launch {\n            val checkUpdate = repo.checkUpdate\n            val checkModuleUpdate = repo.checkModuleUpdate\n            val themeMode = repo.themeMode\n            val miuixMonet = repo.miuixMonet\n            val keyColor = repo.keyColor\n            val enablePredictiveBack = repo.enablePredictiveBack\n            val enableBlur = repo.enableBlur\n            val enableFloatingBottomBar = repo.enableFloatingBottomBar\n            val enableFloatingBottomBarBlur = repo.enableFloatingBottomBarBlur\n            val pageScale = repo.pageScale\n            val enableWebDebugging = repo.enableWebDebugging\n            val colorStyle = repo.colorStyle\n            val colorSpec = repo.colorSpec\n            val isLkmMode = repo.isLkmMode()\n\n            // Async loading for natives/features\n            val suCompatStatus = repo.getSuCompatStatus()\n            val suCompatPersistValue = repo.getSuCompatPersistValue()\n            val isSuEnabled = repo.isSuEnabled()\n\n            val suCompatMode = if (suCompatPersistValue == 0L) 2 else if (!isSuEnabled) 1 else 0\n\n            val kernelUmountStatus = repo.getKernelUmountStatus()\n            val isKernelUmountEnabled = repo.isKernelUmountEnabled()\n            val isDefaultUmountModules = repo.isDefaultUmountModules()\n            val uiMode = repo.uiMode\n            val autoJailbreak = repo.autoJailbreak\n            val isLateLoadMode = Natives.isLateLoadMode\n\n            _uiState.update {\n                it.copy(\n                    uiMode = uiMode,\n                    checkUpdate = checkUpdate,\n                    checkModuleUpdate = checkModuleUpdate,\n                    themeMode = themeMode,\n                    miuixMonet = miuixMonet,\n                    keyColor = keyColor,\n                    enablePredictiveBack = enablePredictiveBack,\n                    enableBlur = enableBlur,\n                    enableFloatingBottomBar = enableFloatingBottomBar,\n                    enableFloatingBottomBarBlur = enableFloatingBottomBarBlur,\n                    pageScale = pageScale,\n                    enableWebDebugging = enableWebDebugging,\n                    colorStyle = colorStyle,\n                    colorSpec = colorSpec,\n                    suCompatStatus = suCompatStatus,\n                    suCompatMode = suCompatMode,\n                    isSuEnabled = isSuEnabled,\n                    kernelUmountStatus = kernelUmountStatus,\n                    isKernelUmountEnabled = isKernelUmountEnabled,\n                    isDefaultUmountModules = isDefaultUmountModules,\n                    isLkmMode = isLkmMode,\n                    autoJailbreak = autoJailbreak,\n                    isLateLoadMode = isLateLoadMode,\n                )\n            }\n        }\n    }\n\n    fun setCheckUpdate(enabled: Boolean) {\n        repo.checkUpdate = enabled\n        _uiState.update { it.copy(checkUpdate = enabled) }\n    }\n\n    fun setUiMode(mode: String) {\n        val oldMode = repo.uiMode\n        val currentThemeMode = repo.themeMode\n\n        val newThemeMode = when (oldMode) {\n            \"material\" if mode == \"miuix\" -> {\n                val colorMode = ColorMode.fromValue(currentThemeMode)\n                val baseMode = if (colorMode == ColorMode.DARK_AMOLED) 2 else currentThemeMode\n                if (repo.miuixMonet && !colorMode.isMonet) {\n                    ColorMode.fromValue(baseMode).toMonetMode()\n                } else if (!repo.miuixMonet && colorMode.isMonet) {\n                    ColorMode.fromValue(baseMode).toNonMonetMode()\n                } else baseMode\n            }\n\n            \"miuix\" if mode == \"material\" -> {\n                val colorMode = ColorMode.fromValue(currentThemeMode)\n                if (colorMode.isMonet) {\n                    colorMode.toNonMonetMode()\n                } else currentThemeMode\n            }\n\n            else -> currentThemeMode\n        }\n\n        repo.uiMode = mode\n        repo.themeMode = newThemeMode\n        _uiState.update { it.copy(uiMode = mode, themeMode = newThemeMode) }\n    }\n\n    fun setCheckModuleUpdate(enabled: Boolean) {\n        repo.checkModuleUpdate = enabled\n        _uiState.update { it.copy(checkModuleUpdate = enabled) }\n    }\n\n    fun setThemeMode(mode: Int) {\n        val currentUiMode = repo.uiMode\n        val effectiveMode = if (currentUiMode == \"miuix\" && _uiState.value.miuixMonet) {\n            mode + 3\n        } else {\n            mode\n        }\n        repo.themeMode = effectiveMode\n        _uiState.update { it.copy(themeMode = effectiveMode) }\n    }\n\n    fun setColorMode(mode: ColorMode) {\n        repo.themeMode = mode.value\n        _uiState.update { it.copy(themeMode = mode.value) }\n    }\n\n    fun setMiuixMonet(enabled: Boolean) {\n        val currentThemeMode = repo.themeMode\n        val colorMode = ColorMode.fromValue(currentThemeMode)\n        val newThemeMode = if (enabled) {\n            if (!colorMode.isMonet) colorMode.toMonetMode() else currentThemeMode\n        } else {\n            if (colorMode.isMonet) colorMode.toNonMonetMode() else currentThemeMode\n        }\n        repo.miuixMonet = enabled\n        repo.themeMode = newThemeMode\n        _uiState.update { it.copy(miuixMonet = enabled, themeMode = newThemeMode) }\n    }\n\n    fun setKeyColor(color: Int) {\n        repo.keyColor = color\n        _uiState.update { it.copy(keyColor = color) }\n    }\n\n    fun setColorStyle(style: String) {\n        repo.colorStyle = style\n        _uiState.update { it.copy(colorStyle = style) }\n    }\n\n    fun setColorSpec(spec: String) {\n        repo.colorSpec = spec\n        _uiState.update { it.copy(colorSpec = spec) }\n    }\n\n    fun setEnablePredictiveBack(enabled: Boolean) {\n        repo.enablePredictiveBack = enabled\n        _uiState.update { it.copy(enablePredictiveBack = enabled) }\n    }\n\n    fun setEnableBlur(enabled: Boolean) {\n        repo.enableBlur = enabled\n        _uiState.update { it.copy(enableBlur = enabled) }\n    }\n\n    fun setEnableFloatingBottomBar(enabled: Boolean) {\n        repo.enableFloatingBottomBar = enabled\n        _uiState.update { it.copy(enableFloatingBottomBar = enabled) }\n    }\n\n    fun setEnableFloatingBottomBarBlur(enabled: Boolean) {\n        repo.enableFloatingBottomBarBlur = enabled\n        _uiState.update { it.copy(enableFloatingBottomBarBlur = enabled) }\n    }\n\n    fun setPageScale(scale: Float) {\n        repo.pageScale = scale\n        _uiState.update { it.copy(pageScale = scale) }\n    }\n\n    fun setEnableWebDebugging(enabled: Boolean) {\n        repo.enableWebDebugging = enabled\n        _uiState.update { it.copy(enableWebDebugging = enabled) }\n    }\n\n    fun setSuCompatMode(mode: Int) {\n        viewModelScope.launch(Dispatchers.IO) {\n            when (mode) {\n                0 -> if (repo.setSuEnabled(true)) {\n                    repo.execKsudFeatureSave()\n                    repo.setSuCompatModePref(0)\n                    _uiState.update { it.copy(suCompatMode = 0, isSuEnabled = true) }\n                }\n\n                1 -> if (repo.setSuEnabled(true)) {\n                    repo.execKsudFeatureSave()\n                    if (repo.setSuEnabled(false)) {\n                        // \"Disable until reboot\" implies it should be enabled on next boot.\n                        // We set the preference to 0 (Enabled) to match the persistent state.\n                        repo.setSuCompatModePref(0)\n                        _uiState.update { it.copy(suCompatMode = 1, isSuEnabled = false) }\n                    }\n                }\n\n                2 -> if (repo.setSuEnabled(false)) {\n                    repo.execKsudFeatureSave()\n                    repo.setSuCompatModePref(2)\n                    _uiState.update { it.copy(suCompatMode = 2, isSuEnabled = false) }\n                }\n            }\n        }\n    }\n\n    fun setKernelUmountEnabled(enabled: Boolean) {\n        viewModelScope.launch(Dispatchers.IO) {\n            if (repo.setKernelUmountEnabled(enabled)) {\n                repo.execKsudFeatureSave()\n                _uiState.update { it.copy(isKernelUmountEnabled = enabled) }\n            }\n        }\n    }\n\n    fun setAutoJailbreak(enabled: Boolean) {\n        repo.autoJailbreak = enabled\n        _uiState.update { it.copy(autoJailbreak = enabled) }\n    }\n\n    fun setDefaultUmountModules(enabled: Boolean) {\n        viewModelScope.launch(Dispatchers.IO) {\n            if (repo.setDefaultUmountModules(enabled)) {\n                _uiState.update { it.copy(isDefaultUmountModules = enabled) }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt",
    "content": "package me.weishu.kernelsu.ui.viewmodel\n\nimport android.content.Context\nimport android.content.pm.ApplicationInfo\nimport android.graphics.drawable.Drawable\nimport android.util.Log\nimport androidx.core.content.edit\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.Job\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.data.repository.SuperUserRepository\nimport me.weishu.kernelsu.data.repository.SuperUserRepositoryImpl\nimport me.weishu.kernelsu.ksuApp\nimport me.weishu.kernelsu.ui.component.SearchStatus\nimport me.weishu.kernelsu.ui.screen.superuser.GroupedApps\nimport me.weishu.kernelsu.ui.screen.superuser.SuperUserUiState\nimport me.weishu.kernelsu.ui.util.HanziToPinyin\nimport me.weishu.kernelsu.ui.util.ownerNameForUid\nimport me.weishu.kernelsu.ui.util.pickPrimary\nimport java.text.Collator\nimport java.util.Locale\n\nclass SuperUserViewModel(\n    private val repo: SuperUserRepository = SuperUserRepositoryImpl()\n) : ViewModel() {\n\n    companion object {\n        private const val TAG = \"SuperUserViewModel\"\n\n        // Cache to support getAppIconDrawable static method\n        private val appsLock = Any()\n        private var cachedApps: List<AppInfo> = emptyList()\n        private val groupedAppsLock = Any()\n        private var cachedGroupedApps: List<GroupedApps> = emptyList()\n\n        val apps: List<AppInfo>\n            get() = synchronized(appsLock) { cachedApps }\n\n        @JvmStatic\n        fun getGroupedApp(uid: Int): GroupedApps? {\n            return synchronized(groupedAppsLock) { cachedGroupedApps.find { it.uid == uid } }\n        }\n\n        @JvmStatic\n        fun getAppIconDrawable(context: Context, packageName: String): Drawable? {\n            val appList = synchronized(appsLock) { cachedApps }\n            val appDetail = appList.find { it.packageName == packageName }\n            return appDetail?.packageInfo?.applicationInfo?.loadIcon(context.packageManager)\n        }\n    }\n\n    typealias AppInfo = me.weishu.kernelsu.data.model.AppInfo\n\n    private val _uiState = MutableStateFlow(SuperUserUiState())\n    val uiState: StateFlow<SuperUserUiState> = _uiState.asStateFlow()\n    private val prefs = ksuApp.getSharedPreferences(\"settings\", Context.MODE_PRIVATE)\n\n    private val refreshMutex = Mutex()\n    private val searchQuery = MutableStateFlow(\"\")\n    var isNeedRefresh = false\n        private set\n\n    init {\n        viewModelScope.launchSearchQueryCollector(searchQuery, ::applySearchText)\n    }\n\n    fun markNeedRefresh() {\n        isNeedRefresh = true\n    }\n\n    fun initializePreferences() {\n        val showSystemApps = prefs.getBoolean(\"show_system_apps\", false)\n        val showOnlyPrimaryUserApps = prefs.getBoolean(\"show_only_primary_user_apps\", false)\n        _uiState.update {\n            it.copy(\n                showSystemApps = showSystemApps,\n                showOnlyPrimaryUserApps = showOnlyPrimaryUserApps,\n            )\n        }\n    }\n\n    fun toggleShowSystemApps(): Job {\n        val newValue = !_uiState.value.showSystemApps\n        prefs.edit { putBoolean(\"show_system_apps\", newValue) }\n        _uiState.update { it.copy(showSystemApps = newValue) }\n        return viewModelScope.launch {\n            // Re-filter when setting changes\n            val grouped = withContext(Dispatchers.IO) {\n                buildGroups(filterAndSort(apps))\n            }\n            updateVisibleApps(grouped)\n        }\n    }\n\n    fun toggleShowOnlyPrimaryUserApps(): Job {\n        val newValue = !_uiState.value.showOnlyPrimaryUserApps\n        prefs.edit { putBoolean(\"show_only_primary_user_apps\", newValue) }\n        _uiState.update { it.copy(showOnlyPrimaryUserApps = newValue) }\n        return viewModelScope.launch {\n            // Re-filter when setting changes\n            val grouped = withContext(Dispatchers.IO) {\n                buildGroups(filterAndSort(apps))\n            }\n            updateVisibleApps(grouped)\n        }\n    }\n\n    fun updateSearchStatus(status: SearchStatus) {\n        val previous = _uiState.value.searchStatus\n        _uiState.update { it.copy(searchStatus = status) }\n        if (previous.searchText != status.searchText) {\n            searchQuery.value = status.searchText\n        }\n    }\n\n    fun updateSearchText(text: String) {\n        updateSearchStatus(_uiState.value.searchStatus.copy(searchText = text))\n    }\n\n    private fun filterSearchResults(groups: List<GroupedApps>, text: String): List<GroupedApps> {\n        if (text.isEmpty()) return emptyList()\n\n        return groups.mapNotNull { group ->\n            val matchedPackageNames = group.apps.filter {\n                it.label.contains(text, true) ||\n                        it.packageName.contains(text, true) ||\n                        HanziToPinyin.getInstance().toPinyinString(it.label).contains(text, true)\n            }.mapTo(linkedSetOf()) { it.packageName }\n\n            if (matchedPackageNames.isEmpty()) {\n                null\n            } else {\n                val sortedApps = group.apps.sortedWith(\n                    compareByDescending { it.packageName in matchedPackageNames }\n                )\n                group.copy(\n                    apps = sortedApps,\n                    matchedPackageNames = matchedPackageNames,\n                )\n            }\n        }\n    }\n\n    private suspend fun applySearchText(text: String) {\n        _uiState.update {\n            it.copy(\n                searchStatus = it.searchStatus.copy(\n                    resultStatus = searchLoadingStatusFor(text)\n                )\n            )\n        }\n\n        if (text.isEmpty()) {\n            _uiState.update { state ->\n                state.copy(\n                    searchResults = emptyList(),\n                    searchStatus = state.searchStatus.copy(resultStatus = SearchStatus.ResultStatus.DEFAULT)\n                )\n            }\n            return\n        }\n\n        val result = withContext(Dispatchers.IO) {\n            filterSearchResults(_uiState.value.groupedApps, text)\n        }\n\n        _uiState.update {\n            it.copy(\n                searchResults = result,\n                searchStatus = it.searchStatus.copy(resultStatus = searchResultStatusFor(text, result.isEmpty()))\n            )\n        }\n    }\n\n    private fun updateCachedGroupedApps(grouped: List<GroupedApps>) {\n        synchronized(groupedAppsLock) {\n            cachedGroupedApps = grouped.map { it.copy(matchedPackageNames = emptySet()) }\n        }\n    }\n\n    private fun updateVisibleApps(grouped: List<GroupedApps>) {\n        val searchText = _uiState.value.searchStatus.searchText\n        val searchResults = filterSearchResults(grouped, searchText)\n        _uiState.update {\n            it.copy(\n                groupedApps = grouped.map { group -> group.copy(matchedPackageNames = emptySet()) },\n                searchResults = searchResults,\n                searchStatus = it.searchStatus.copy(\n                    resultStatus = searchResultStatusFor(searchText, searchResults.isEmpty())\n                )\n            )\n        }\n    }\n\n    private fun filterAndSort(list: List<AppInfo>): List<AppInfo> {\n        val comparator = compareBy<AppInfo> {\n            when {\n                it.allowSu -> 0\n                it.hasCustomProfile -> 1\n                else -> 2\n            }\n        }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label))\n\n        val currentState = _uiState.value\n\n        return list.filter {\n            if (it.packageName == ksuApp.packageName) return@filter false\n            if (it.allowSu || it.hasCustomProfile) {\n                return@filter true\n            }\n            val userFilter = !currentState.showOnlyPrimaryUserApps || it.uid / 100000 == 0\n            val isSystemApp = it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) != 0\n            val typeFilter = it.uid == 2000\n                    || currentState.showSystemApps\n                    || !isSystemApp\n            userFilter && typeFilter\n        }.sortedWith(comparator)\n    }\n\n    private fun buildCachedGroups(apps: List<AppInfo>): List<GroupedApps> {\n        return buildGroups(apps.filter { it.packageName != ksuApp.packageName })\n    }\n\n    private fun buildGroups(apps: List<AppInfo>): List<GroupedApps> {\n        val comparator = compareBy<AppInfo> {\n            when {\n                it.allowSu -> 0\n                it.hasCustomProfile -> 1\n                else -> 2\n            }\n        }.thenBy { it.label.lowercase() }\n        val groups = apps.groupBy { it.uid }.map { (uid, list) ->\n            val sorted = list.sortedWith(comparator)\n            val primary = pickPrimary(sorted)\n            val shouldUmount = Natives.uidShouldUmount(uid)\n            val ownerName = if (sorted.size > 1) ownerNameForUid(uid, sorted) else null\n\n            GroupedApps(\n                uid = uid,\n                apps = sorted,\n                primary = primary,\n                anyAllowSu = sorted.any { it.allowSu },\n                anyCustom = sorted.any { it.hasCustomProfile },\n                shouldUmount = shouldUmount,\n                ownerName = ownerName\n            )\n        }\n        return groups.sortedWith(Comparator { a, b ->\n            fun rank(g: GroupedApps): Int = when {\n                g.anyAllowSu -> 0\n                g.anyCustom -> 1\n                g.apps.size > 1 -> 2\n                g.shouldUmount -> 4\n                else -> 3\n            }\n\n            val ra = rank(a)\n            val rb = rank(b)\n            if (ra != rb) return@Comparator ra - rb\n            return@Comparator when (ra) {\n                2 -> a.uid.compareTo(b.uid)\n                else -> a.primary.label.lowercase().compareTo(b.primary.label.lowercase())\n            }\n        })\n    }\n\n    suspend fun fetchAppList() {\n        refreshMutex.withLock {\n            _uiState.update { it.copy(isRefreshing = true, error = null) }\n\n            repo.getAppList().onSuccess { (newApps, ids) ->\n                val (cachedGroups, grouped) = withContext(Dispatchers.IO) {\n                    buildCachedGroups(newApps) to buildGroups(filterAndSort(newApps))\n                }\n\n                // Update cache for static method\n                synchronized(appsLock) { cachedApps = newApps }\n                updateCachedGroupedApps(cachedGroups)\n                updateVisibleApps(grouped)\n                _uiState.update { it.copy(userIds = ids, isRefreshing = false) }\n            }.onFailure { e ->\n                Log.e(TAG, \"fetchAppList failed\", e)\n                _uiState.update {\n                    it.copy(\n                        isRefreshing = false,\n                        error = e\n                    )\n                }\n            }\n\n            isNeedRefresh = false\n        }\n    }\n\n    private suspend fun refreshAppList(resort: Boolean = true) {\n        refreshMutex.withLock {\n            val currentApps = synchronized(appsLock) { cachedApps }\n            if (currentApps.isEmpty()) return\n\n            repo.refreshProfiles(currentApps).onSuccess { updatedApps ->\n                // Update cache for static method\n                synchronized(appsLock) { cachedApps = updatedApps }\n\n                val cachedGroups = withContext(Dispatchers.IO) {\n                    buildCachedGroups(updatedApps)\n                }\n                updateCachedGroupedApps(cachedGroups)\n\n                val grouped = if (resort) {\n                    withContext(Dispatchers.IO) {\n                        buildGroups(filterAndSort(updatedApps))\n                    }\n                } else {\n                    val updatedGroups = buildGroups(filterAndSort(updatedApps)).associateBy { it.uid }\n                    _uiState.value.groupedApps.map { group ->\n                        val newApps = updatedGroups[group.uid]?.apps ?: group.apps\n                        val primary = pickPrimary(newApps)\n                        val shouldUmount = Natives.uidShouldUmount(group.uid)\n                        val ownerName = if (newApps.size > 1) ownerNameForUid(group.uid, newApps) else null\n                        group.copy(\n                            apps = newApps,\n                            primary = primary,\n                            anyAllowSu = newApps.any { it.allowSu },\n                            anyCustom = newApps.any { it.hasCustomProfile },\n                            shouldUmount = shouldUmount,\n                            ownerName = ownerName\n                        )\n                    }\n                }\n\n                updateVisibleApps(grouped)\n                _uiState.update { it.copy(isRefreshing = false) }\n                isNeedRefresh = false\n            }\n        }\n    }\n\n    fun loadAppList(force: Boolean = false, resort: Boolean = true): Job {\n        return viewModelScope.launch {\n            if (force || _uiState.value.groupedApps.isEmpty()) {\n                fetchAppList()\n            } else {\n                refreshAppList(resort)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/TemplateViewModel.kt",
    "content": "package me.weishu.kernelsu.ui.viewmodel\n\nimport android.util.Log\nimport androidx.lifecycle.ViewModel\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.update\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.Natives\nimport me.weishu.kernelsu.data.repository.TemplateRepository\nimport me.weishu.kernelsu.data.repository.TemplateRepositoryImpl\nimport me.weishu.kernelsu.profile.Capabilities\nimport me.weishu.kernelsu.profile.Groups\nimport me.weishu.kernelsu.ui.screen.template.TemplateUiState\nimport me.weishu.kernelsu.ui.util.getAppProfileTemplate\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.text.Collator\nimport java.util.Locale\n\nconst val TAG = \"TemplateViewModel\"\n\nclass TemplateViewModel(\n    private val repo: TemplateRepository = TemplateRepositoryImpl()\n) : ViewModel() {\n\n    typealias TemplateInfo = me.weishu.kernelsu.data.model.TemplateInfo\n\n    private val _uiState = MutableStateFlow(TemplateUiState())\n    val uiState: StateFlow<TemplateUiState> = _uiState.asStateFlow()\n\n    suspend fun fetchTemplates(sync: Boolean = false) {\n        _uiState.update { it.copy(isRefreshing = true, error = null) }\n\n        val result = repo.getTemplates(sync)\n\n        withContext(Dispatchers.Main) {\n            result.onSuccess { templates ->\n                val comparator = compareBy(TemplateInfo::local).reversed().then(\n                    compareBy(\n                        Collator.getInstance(Locale.getDefault()), TemplateInfo::id\n                    )\n                )\n                val sorted = templates.sortedWith(comparator)\n\n                _uiState.update {\n                    it.copy(\n                        templates = templates,\n                        templateList = sorted,\n                        isRefreshing = false\n                    )\n                }\n            }.onFailure { e ->\n                _uiState.update {\n                    it.copy(\n                        isRefreshing = false,\n                        error = e\n                    )\n                }\n            }\n        }\n    }\n\n    suspend fun importTemplates(\n        templates: String,\n        onSuccess: suspend () -> Unit,\n        onFailure: suspend (String) -> Unit\n    ) {\n        repo.importTemplates(templates)\n            .onSuccess { onSuccess() }\n            .onFailure { onFailure(it.message ?: \"Unknown error\") }\n    }\n\n    suspend fun exportTemplates(onTemplateEmpty: suspend () -> Unit, callback: suspend (String) -> Unit) {\n        repo.exportTemplates()\n            .onSuccess { callback(it) }\n            .onFailure { onTemplateEmpty() }\n    }\n}\n\nfun getTemplateInfoById(id: String): me.weishu.kernelsu.data.model.TemplateInfo? {\n    return runCatching {\n        me.weishu.kernelsu.data.model.TemplateInfo.fromJSON(JSONObject(getAppProfileTemplate(id)))\n    }.onFailure {\n        Log.e(TAG, \"ignore invalid template: $it\", it)\n    }.getOrNull()\n}\n\n@Suppress(\"unused\")\nfun generateTemplates() {\n    val templateJson = JSONObject()\n    templateJson.put(\"id\", \"com.example\")\n    templateJson.put(\"name\", \"Example\")\n    templateJson.put(\"description\", \"This is an example template\")\n    templateJson.put(\"local\", true)\n    templateJson.put(\"namespace\", Natives.Profile.Namespace.INHERITED.name)\n    templateJson.put(\"uid\", 0)\n    templateJson.put(\"gid\", 0)\n\n    templateJson.put(\"groups\", JSONArray().apply { put(Groups.INET.name) })\n    templateJson.put(\"capabilities\", JSONArray().apply { put(Capabilities.CAP_NET_RAW.name) })\n    templateJson.put(\"context\", \"u:r:su:s0\")\n    Log.i(TAG, \"$templateJson\")\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/AppIconUtil.kt",
    "content": "package me.weishu.kernelsu.ui.webui\n\nimport android.content.Context\nimport android.graphics.Bitmap\nimport android.graphics.Canvas\nimport android.graphics.drawable.BitmapDrawable\nimport android.graphics.drawable.Drawable\nimport android.util.LruCache\nimport androidx.core.graphics.createBitmap\nimport androidx.core.graphics.scale\nimport me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel.Companion.getAppIconDrawable\n\nobject AppIconUtil {\n    // Limit cache size to 200 icons\n    private const val CACHE_SIZE = 200\n    private val iconCache = LruCache<String?, Bitmap?>(CACHE_SIZE)\n\n    @Synchronized\n    fun loadAppIconSync(context: Context, packageName: String, sizePx: Int): Bitmap? {\n        val cached = iconCache.get(packageName)\n        if (cached != null) return cached\n\n        try {\n            val drawable = getAppIconDrawable(context, packageName) ?: return null\n            val raw = drawableToBitmap(drawable, sizePx)\n            val icon = raw.scale(sizePx, sizePx)\n            iconCache.put(packageName, icon)\n            return icon\n        } catch (_: Exception) {\n            return null\n        }\n    }\n\n    private fun drawableToBitmap(drawable: Drawable, size: Int): Bitmap {\n        if (drawable is BitmapDrawable) return drawable.bitmap\n\n        val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else size\n        val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else size\n\n        val bmp = createBitmap(width, height)\n        val canvas = Canvas(bmp)\n        drawable.setBounds(0, 0, canvas.width, canvas.height)\n        drawable.draw(canvas)\n        return bmp\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/Insets.kt",
    "content": "package me.weishu.kernelsu.ui.webui\n\n/**\n * Insets data class from GitHub@MMRLApp/WebUI-X-Portable\n *\n * Data class representing insets (top, bottom, left, right) for a view.\n *\n * This class provides methods to generate CSS code that can be injected into a WebView\n * to apply these insets as CSS variables. This is useful for adapting web content\n * to the safe areas of a device screen, considering notches, status bars, and navigation bars.\n *\n * @property top The top inset value in pixels.\n * @property bottom The bottom inset value in pixels.\n * @property left The left inset value in pixels.\n * @property right The right inset value in pixels.\n */\ndata class Insets(\n    val top: Int,\n    val bottom: Int,\n    val left: Int,\n    val right: Int,\n) {\n    val css\n        get() = buildString {\n            appendLine(\":root {\")\n            appendLine(\"\\t--safe-area-inset-top: ${top}px;\")\n            appendLine(\"\\t--safe-area-inset-right: ${right}px;\")\n            appendLine(\"\\t--safe-area-inset-bottom: ${bottom}px;\")\n            appendLine(\"\\t--safe-area-inset-left: ${left}px;\")\n            appendLine(\"\\t--window-inset-top: var(--safe-area-inset-top, 0px);\")\n            appendLine(\"\\t--window-inset-bottom: var(--safe-area-inset-bottom, 0px);\")\n            appendLine(\"\\t--window-inset-left: var(--safe-area-inset-left, 0px);\")\n            appendLine(\"\\t--window-inset-right: var(--safe-area-inset-right, 0px);\")\n            appendLine(\"\\t--f7-safe-area-top: var(--window-inset-top, 0px) !important;\")\n            appendLine(\"\\t--f7-safe-area-bottom: var(--window-inset-bottom, 0px) !important;\")\n            appendLine(\"\\t--f7-safe-area-left: var(--window-inset-left, 0px) !important;\")\n            appendLine(\"\\t--f7-safe-area-right: var(--window-inset-right, 0px) !important;\")\n            append(\"}\")\n        }\n    val js\n        get() = buildString {\n            append(\"(function() {\")\n            append(\" var s = document.documentElement.style;\")\n            append(\" s.setProperty('--safe-area-inset-top', '${top}px');\")\n            append(\" s.setProperty('--safe-area-inset-right', '${right}px');\")\n            append(\" s.setProperty('--safe-area-inset-bottom', '${bottom}px');\")\n            append(\" s.setProperty('--safe-area-inset-left', '${left}px');\")\n            append(\"})();\")\n        }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/MimeUtil.java",
    "content": "/*\n * Copyright 2023 The Android Open Source Project\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *      http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\npackage me.weishu.kernelsu.ui.webui;\n\nimport java.net.URLConnection;\n\nclass MimeUtil {\n\n    public static String getMimeFromFileName(String fileName) {\n        if (fileName == null) {\n            return null;\n        }\n\n        // Copying the logic and mapping that Chromium follows.\n        // First we check against the OS (this is a limited list by default)\n        // but app developers can extend this.\n        // We then check against a list of hardcoded mime types above if the\n        // OS didn't provide a result.\n        String mimeType = URLConnection.guessContentTypeFromName(fileName);\n\n        if (mimeType != null) {\n            return mimeType;\n        }\n\n        return guessHardcodedMime(fileName);\n    }\n\n    // We should keep this map in sync with the lists under\n    // //net/base/mime_util.cc in Chromium.\n    // A bunch of the mime types don't really apply to Android land\n    // like word docs so feel free to filter out where necessary.\n    private static String guessHardcodedMime(String fileName) {\n        int finalFullStop = fileName.lastIndexOf('.');\n        if (finalFullStop == -1) {\n            return null;\n        }\n\n        final String extension = fileName.substring(finalFullStop + 1).toLowerCase();\n\n        return switch (extension) {\n            case \"webm\" -> \"video/webm\";\n            case \"mpeg\", \"mpg\" -> \"video/mpeg\";\n            case \"mp3\" -> \"audio/mpeg\";\n            case \"wasm\" -> \"application/wasm\";\n            case \"xhtml\", \"xht\", \"xhtm\" -> \"application/xhtml+xml\";\n            case \"flac\" -> \"audio/flac\";\n            case \"ogg\", \"oga\", \"opus\" -> \"audio/ogg\";\n            case \"wav\" -> \"audio/wav\";\n            case \"m4a\" -> \"audio/x-m4a\";\n            case \"gif\" -> \"image/gif\";\n            case \"jpeg\", \"jpg\", \"jfif\", \"pjpeg\", \"pjp\" -> \"image/jpeg\";\n            case \"png\" -> \"image/png\";\n            case \"apng\" -> \"image/apng\";\n            case \"svg\", \"svgz\" -> \"image/svg+xml\";\n            case \"webp\" -> \"image/webp\";\n            case \"mht\", \"mhtml\" -> \"multipart/related\";\n            case \"css\" -> \"text/css\";\n            case \"html\", \"htm\", \"shtml\", \"shtm\", \"ehtml\" -> \"text/html\";\n            case \"js\", \"mjs\" -> \"application/javascript\";\n            case \"xml\" -> \"text/xml\";\n            case \"mp4\", \"m4v\" -> \"video/mp4\";\n            case \"ogv\", \"ogm\" -> \"video/ogg\";\n            case \"ico\" -> \"image/x-icon\";\n            case \"woff\" -> \"application/font-woff\";\n            case \"gz\", \"tgz\" -> \"application/gzip\";\n            case \"json\" -> \"application/json\";\n            case \"pdf\" -> \"application/pdf\";\n            case \"zip\" -> \"application/zip\";\n            case \"bmp\" -> \"image/bmp\";\n            case \"tiff\", \"tif\" -> \"image/tiff\";\n            default -> null;\n        };\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/MonetColorsProvider.kt",
    "content": "package me.weishu.kernelsu.ui.webui\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.surfaceColorAtElevation\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport top.yukonga.miuix.kmp.theme.MiuixTheme\nimport java.util.concurrent.atomic.AtomicReference\n\n/**\n * @author rifsxd\n * @date 2025/6/2.\n */\nobject MonetColorsProvider {\n\n    private val colorsCss: AtomicReference<String?> = AtomicReference(null)\n\n    fun getColorsCss(): String {\n        return colorsCss.get() ?: \"\"\n    }\n\n    @Composable\n    fun UpdateCss() {\n        when (LocalUiMode.current) {\n            UiMode.Miuix -> UpdateCssMiuix()\n            UiMode.Material -> UpdateCssMaterial()\n        }\n    }\n\n    @Composable\n    private fun UpdateCssMiuix() {\n        val colorScheme = MiuixTheme.colorScheme\n\n        LaunchedEffect(colorScheme) {\n            val monetColors = mapOf(\n                \"primary\" to colorScheme.primary.toCssValue(),\n                \"onPrimary\" to colorScheme.onPrimary.toCssValue(),\n                \"primaryContainer\" to colorScheme.primaryContainer.toCssValue(),\n                \"onPrimaryContainer\" to colorScheme.onPrimaryContainer.toCssValue(),\n                \"inversePrimary\" to colorScheme.primaryVariant.toCssValue(),\n                \"secondary\" to colorScheme.secondary.toCssValue(),\n                \"onSecondary\" to colorScheme.onSecondary.toCssValue(),\n                \"secondaryContainer\" to colorScheme.secondaryContainer.toCssValue(),\n                \"onSecondaryContainer\" to colorScheme.onSecondaryContainer.toCssValue(),\n                \"tertiary\" to colorScheme.tertiaryContainerVariant.toCssValue(),\n                \"onTertiary\" to colorScheme.tertiaryContainer.toCssValue(),\n                \"tertiaryContainer\" to colorScheme.tertiaryContainer.toCssValue(),\n                \"onTertiaryContainer\" to colorScheme.onTertiaryContainer.toCssValue(),\n                \"background\" to colorScheme.background.toCssValue(),\n                \"onBackground\" to colorScheme.onBackground.toCssValue(),\n                \"surface\" to colorScheme.surface.toCssValue(),\n                \"tonalSurface\" to colorScheme.surfaceContainer.toCssValue(),\n                \"onSurface\" to colorScheme.onSurface.toCssValue(),\n                \"surfaceVariant\" to colorScheme.surfaceVariant.toCssValue(),\n                \"onSurfaceVariant\" to colorScheme.onSurfaceVariantSummary.toCssValue(),\n                \"surfaceTint\" to colorScheme.primary.toCssValue(),\n                \"inverseSurface\" to colorScheme.disabledOnSurface.toCssValue(),\n                \"inverseOnSurface\" to colorScheme.surfaceContainer.toCssValue(),\n                \"error\" to colorScheme.error.toCssValue(),\n                \"onError\" to colorScheme.onError.toCssValue(),\n                \"errorContainer\" to colorScheme.errorContainer.toCssValue(),\n                \"onErrorContainer\" to colorScheme.onErrorContainer.toCssValue(),\n                \"outline\" to colorScheme.outline.toCssValue(),\n                \"outlineVariant\" to colorScheme.dividerLine.toCssValue(),\n                \"scrim\" to colorScheme.windowDimming.toCssValue(),\n                \"surfaceBright\" to colorScheme.surface.toCssValue(),\n                \"surfaceDim\" to colorScheme.surface.toCssValue(),\n                \"surfaceContainer\" to colorScheme.surfaceContainer.toCssValue(),\n                \"surfaceContainerHigh\" to colorScheme.surfaceContainerHigh.toCssValue(),\n                \"surfaceContainerHighest\" to colorScheme.surfaceContainerHighest.toCssValue(),\n                \"surfaceContainerLow\" to colorScheme.surfaceContainer.toCssValue(),\n                \"surfaceContainerLowest\" to colorScheme.surfaceContainer.toCssValue(),\n                \"filledTonalButtonContentColor\" to colorScheme.onPrimaryContainer.toCssValue(),\n                \"filledTonalButtonContainerColor\" to colorScheme.secondaryContainer.toCssValue(),\n                \"filledTonalButtonDisabledContentColor\" to colorScheme.onSurfaceVariantSummary.toCssValue(),\n                \"filledTonalButtonDisabledContainerColor\" to colorScheme.surfaceVariant.toCssValue(),\n                \"filledCardContentColor\" to colorScheme.onPrimaryContainer.toCssValue(),\n                \"filledCardContainerColor\" to colorScheme.primaryContainer.toCssValue(),\n                \"filledCardDisabledContentColor\" to colorScheme.onSurfaceVariantSummary.toCssValue(),\n                \"filledCardDisabledContainerColor\" to colorScheme.surfaceVariant.toCssValue()\n            )\n\n            colorsCss.set(monetColors.toCssVars())\n        }\n    }\n\n    @Composable\n    private fun UpdateCssMaterial() {\n        val colorScheme = MaterialTheme.colorScheme\n\n        LaunchedEffect(colorScheme) {\n            val monetColors = mapOf(\n                \"primary\" to colorScheme.primary.toCssValue(),\n                \"onPrimary\" to colorScheme.onPrimary.toCssValue(),\n                \"primaryContainer\" to colorScheme.primaryContainer.toCssValue(),\n                \"onPrimaryContainer\" to colorScheme.onPrimaryContainer.toCssValue(),\n                \"inversePrimary\" to colorScheme.inversePrimary.toCssValue(),\n                \"secondary\" to colorScheme.secondary.toCssValue(),\n                \"onSecondary\" to colorScheme.onSecondary.toCssValue(),\n                \"secondaryContainer\" to colorScheme.secondaryContainer.toCssValue(),\n                \"onSecondaryContainer\" to colorScheme.onSecondaryContainer.toCssValue(),\n                \"tertiary\" to colorScheme.tertiary.toCssValue(),\n                \"onTertiary\" to colorScheme.onTertiary.toCssValue(),\n                \"tertiaryContainer\" to colorScheme.tertiaryContainer.toCssValue(),\n                \"onTertiaryContainer\" to colorScheme.onTertiaryContainer.toCssValue(),\n                \"background\" to colorScheme.background.toCssValue(),\n                \"onBackground\" to colorScheme.onBackground.toCssValue(),\n                \"surface\" to colorScheme.surface.toCssValue(),\n                \"tonalSurface\" to colorScheme.surfaceColorAtElevation(1.dp).toCssValue(),\n                \"onSurface\" to colorScheme.onSurface.toCssValue(),\n                \"surfaceVariant\" to colorScheme.surfaceVariant.toCssValue(),\n                \"onSurfaceVariant\" to colorScheme.onSurfaceVariant.toCssValue(),\n                \"surfaceTint\" to colorScheme.surfaceTint.toCssValue(),\n                \"inverseSurface\" to colorScheme.inverseSurface.toCssValue(),\n                \"inverseOnSurface\" to colorScheme.inverseOnSurface.toCssValue(),\n                \"error\" to colorScheme.error.toCssValue(),\n                \"onError\" to colorScheme.onError.toCssValue(),\n                \"errorContainer\" to colorScheme.errorContainer.toCssValue(),\n                \"onErrorContainer\" to colorScheme.onErrorContainer.toCssValue(),\n                \"outline\" to colorScheme.outline.toCssValue(),\n                \"outlineVariant\" to colorScheme.outlineVariant.toCssValue(),\n                \"scrim\" to colorScheme.scrim.toCssValue(),\n                \"surfaceBright\" to colorScheme.surfaceBright.toCssValue(),\n                \"surfaceDim\" to colorScheme.surfaceDim.toCssValue(),\n                \"surfaceContainer\" to colorScheme.surfaceContainer.toCssValue(),\n                \"surfaceContainerHigh\" to colorScheme.surfaceContainerHigh.toCssValue(),\n                \"surfaceContainerHighest\" to colorScheme.surfaceContainerHighest.toCssValue(),\n                \"surfaceContainerLow\" to colorScheme.surfaceContainerLow.toCssValue(),\n                \"surfaceContainerLowest\" to colorScheme.surfaceContainerLowest.toCssValue(),\n                \"filledTonalButtonContentColor\" to colorScheme.onPrimaryContainer.toCssValue(),\n                \"filledTonalButtonContainerColor\" to colorScheme.secondaryContainer.toCssValue(),\n                \"filledTonalButtonDisabledContentColor\" to colorScheme.onSurfaceVariant.toCssValue(),\n                \"filledTonalButtonDisabledContainerColor\" to colorScheme.surfaceVariant.toCssValue(),\n                \"filledCardContentColor\" to colorScheme.onPrimaryContainer.toCssValue(),\n                \"filledCardContainerColor\" to colorScheme.primaryContainer.toCssValue(),\n                \"filledCardDisabledContentColor\" to colorScheme.onSurfaceVariant.toCssValue(),\n                \"filledCardDisabledContainerColor\" to colorScheme.surfaceVariant.toCssValue()\n            )\n\n            colorsCss.set(monetColors.toCssVars())\n        }\n    }\n\n    private fun Map<String, String>.toCssVars(): String {\n        return buildString {\n            append(\":root {\\n\")\n            for ((k, v) in this@toCssVars) {\n                append(\"  --$k: $v;\\n\")\n            }\n            append(\"}\\n\")\n        }\n    }\n\n    private fun Color.toCssValue(): String {\n        fun Float.toHex(): String {\n            return (this * 255).toInt().coerceIn(0, 255).toString(16).padStart(2, '0')\n        }\n        return if (alpha == 1f) {\n            \"#${red.toHex()}${green.toHex()}${blue.toHex()}\"\n        } else {\n            \"#${red.toHex()}${green.toHex()}${blue.toHex()}${alpha.toHex()}\"\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/SuFilePathHandler.java",
    "content": "package me.weishu.kernelsu.ui.webui;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.util.Log;\nimport android.webkit.WebResourceResponse;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.WorkerThread;\nimport androidx.webkit.WebViewAssetLoader;\n\nimport com.topjohnwu.superuser.Shell;\nimport com.topjohnwu.superuser.io.SuFile;\nimport com.topjohnwu.superuser.io.SuFileInputStream;\n\nimport java.io.ByteArrayInputStream;\nimport java.io.File;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.nio.charset.StandardCharsets;\nimport java.util.zip.GZIPInputStream;\n\n/**\n * Handler class to open files from file system by root access\n * For more information about android storage please refer to\n * <a href=\"https://developer.android.com/guide/topics/data/data-storage\">Android Developers\n * Docs: Data and file storage overview</a>.\n * <p class=\"note\">\n * To avoid leaking user or app data to the web, make sure to choose {@code directory}\n * carefully, and assume any file under this directory could be accessed by any web page subject\n * to same-origin rules.\n * <p>\n * A typical usage would be like:\n * <pre class=\"prettyprint\">\n * File publicDir = new File(context.getFilesDir(), \"public\");\n * // Host \"files/public/\" in app's data directory under:\n * // http://appassets.androidplatform.net/public/...\n * WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()\n *          .addPathHandler(\"/public/\", new InternalStoragePathHandler(context, publicDir))\n *          .build();\n * </pre>\n */\npublic final class SuFilePathHandler implements WebViewAssetLoader.PathHandler {\n    /**\n     * Default value to be used as MIME type if guessing MIME type failed.\n     */\n    public static final String DEFAULT_MIME_TYPE = \"text/plain\";\n    private static final String TAG = \"SuFilePathHandler\";\n    /**\n     * Forbidden subdirectories of {@link Context#getDataDir} that cannot be exposed by this\n     * handler. They are forbidden as they often contain sensitive information.\n     * <p class=\"note\">\n     * Note: Any future addition to this list will be considered breaking changes to the API.\n     */\n    private static final String[] FORBIDDEN_DATA_DIRS =\n            new String[]{\"/data/data\", \"/data/system\"};\n\n    @NonNull\n    private final File mDirectory;\n\n    private final Shell mShell;\n    private final InsetsSupplier mInsetsSupplier;\n    private final OnInsetsRequestedListener mOnInsetsRequestedListener;\n    private final Context mContext;\n\n    /**\n     * Creates PathHandler for app's internal storage.\n     * The directory to be exposed must be inside either the application's internal data\n     * directory {@link Context#getDataDir} or cache directory {@link Context#getCacheDir}.\n     * External storage is not supported for security reasons, as other apps with\n     * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} may be able to modify the\n     * files.\n     * <p>\n     * Exposing the entire data or cache directory is not permitted, to avoid accidentally\n     * exposing sensitive application files to the web. Certain existing subdirectories of\n     * {@link Context#getDataDir} are also not permitted as they are often sensitive.\n     * These files are ({@code \"app_webview/\"}, {@code \"databases/\"}, {@code \"lib/\"},\n     * {@code \"shared_prefs/\"} and {@code \"code_cache/\"}).\n     * <p>\n     * The application should typically use a dedicated subdirectory for the files it intends to\n     * expose and keep them separate from other files.\n     *\n     * @param context                   {@link Context} that is used to access app's internal storage.\n     * @param directory                 the absolute path of the exposed app internal storage directory from\n     *                                  which files can be loaded.\n     * @param rootShell                 {@link Shell} instance with root access to read files.\n     * @param insetsSupplier            {@link InsetsSupplier} to provide window insets for styling web content.\n     * @param onInsetsRequestedListener {@link OnInsetsRequestedListener} to notify when insets are requested.\n     * @throws IllegalArgumentException if the directory is not allowed.\n     */\n    public SuFilePathHandler(@NonNull Context context, @NonNull File directory, Shell rootShell, @NonNull InsetsSupplier insetsSupplier, OnInsetsRequestedListener onInsetsRequestedListener) {\n        try {\n            mContext = context;\n            mInsetsSupplier = insetsSupplier;\n            mOnInsetsRequestedListener = onInsetsRequestedListener;\n            mDirectory = new File(getCanonicalDirPath(directory));\n            if (!isAllowedInternalStorageDir(context)) {\n                throw new IllegalArgumentException(\"The given directory \\\"\" + directory\n                        + \"\\\" doesn't exist under an allowed app internal storage directory\");\n            }\n            mShell = rootShell;\n        } catch (IOException e) {\n            throw new IllegalArgumentException(\n                    \"Failed to resolve the canonical path for the given directory: \"\n                            + directory.getPath(), e);\n        }\n    }\n\n    public static String getCanonicalDirPath(@NonNull File file) throws IOException {\n        String canonicalPath = file.getCanonicalPath();\n        if (!canonicalPath.endsWith(\"/\")) canonicalPath += \"/\";\n        return canonicalPath;\n    }\n\n    public static File getCanonicalFileIfChild(@NonNull File parent, @NonNull String child)\n            throws IOException {\n        String parentCanonicalPath = getCanonicalDirPath(parent);\n        String childCanonicalPath = new File(parent, child).getCanonicalPath();\n        if (childCanonicalPath.startsWith(parentCanonicalPath)) {\n            return new File(childCanonicalPath);\n        }\n        return null;\n    }\n\n    @NonNull\n    private static InputStream handleSvgzStream(@NonNull String path,\n                                                @NonNull InputStream stream) throws IOException {\n        return path.endsWith(\".svgz\") ? new GZIPInputStream(stream) : stream;\n    }\n\n    public static InputStream openFile(@NonNull File file, @NonNull Shell shell) throws IOException {\n        SuFile suFile = new SuFile(file.getAbsolutePath());\n        suFile.setShell(shell);\n        InputStream fis = SuFileInputStream.open(suFile);\n        return handleSvgzStream(file.getPath(), fis);\n    }\n\n    /**\n     * Use {@link MimeUtil#getMimeFromFileName} to guess MIME type or return the\n     * {@link #DEFAULT_MIME_TYPE} if it can't guess.\n     *\n     * @param filePath path of the file to guess its MIME type.\n     * @return MIME type guessed from file extension or {@link #DEFAULT_MIME_TYPE}.\n     */\n    @NonNull\n    public static String guessMimeType(@NonNull String filePath) {\n        String mimeType = MimeUtil.getMimeFromFileName(filePath);\n        return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;\n    }\n\n    private boolean isAllowedInternalStorageDir(@NonNull Context context) throws IOException {\n        String dir = getCanonicalDirPath(mDirectory);\n\n        for (String forbiddenPath : FORBIDDEN_DATA_DIRS) {\n            if (dir.startsWith(forbiddenPath)) {\n                return false;\n            }\n        }\n        return true;\n    }\n\n    /**\n     * Opens the requested file from the exposed data directory.\n     * <p>\n     * The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the\n     * requested file cannot be found or is outside the mounted directory a\n     * {@link WebResourceResponse} object with a {@code null} {@link InputStream} will be\n     * returned instead of {@code null}. This saves the time of falling back to network and\n     * trying to resolve a path that doesn't exist. A {@link WebResourceResponse} with\n     * {@code null} {@link InputStream} will be received as an HTTP response with status code\n     * {@code 404} and no body.\n     * <p class=\"note\">\n     * The MIME type for the file will be determined from the file's extension using\n     * {@link java.net.URLConnection#guessContentTypeFromName}. Developers should ensure that\n     * files are named using standard file extensions. If the file does not have a\n     * recognised extension, {@code \"text/plain\"} will be used by default.\n     *\n     * @param path the suffix path to be handled.\n     * @return {@link WebResourceResponse} for the requested file.\n     */\n    @Override\n    @WorkerThread\n    @NonNull\n    public WebResourceResponse handle(@NonNull String path) {\n        if (\"internal/insets.css\".equals(path)) {\n            if (mOnInsetsRequestedListener != null) {\n                mOnInsetsRequestedListener.onInsetsRequested(true);\n            }\n            String css = mInsetsSupplier.get().getCss();\n            return new WebResourceResponse(\n                    \"text/css\",\n                    \"utf-8\",\n                    new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8))\n            );\n        }\n        if (\"internal/colors.css\".equals(path)) {\n            SharedPreferences prefs = mContext.getSharedPreferences(\"settings\", Context.MODE_PRIVATE);\n            int colorMode = prefs.getInt(\"color_mode\", 0);\n            String uiMode = prefs.getString(\"ui_mode\", \"miuix\");\n            String css = \"\";\n            if ((colorMode >= 3 && colorMode <= 6) || \"material\".equals(uiMode)) {\n                css = MonetColorsProvider.INSTANCE.getColorsCss();\n            }\n            return new WebResourceResponse(\n                    \"text/css\",\n                    \"utf-8\",\n                    new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8))\n            );\n        }\n        try {\n            File file = getCanonicalFileIfChild(mDirectory, path);\n            if (file != null) {\n                InputStream is = openFile(file, mShell);\n                String mimeType = guessMimeType(path);\n                return new WebResourceResponse(mimeType, null, is);\n            } else {\n                Log.e(TAG, String.format(\n                        \"The requested file: %s is outside the mounted directory: %s\", path,\n                        mDirectory));\n            }\n        } catch (IOException e) {\n            Log.e(TAG, \"Error opening the requested path: \" + path, e);\n        }\n        return new WebResourceResponse(null, null, null);\n    }\n\n    public interface InsetsSupplier {\n        @NonNull\n        Insets get();\n    }\n\n    public interface OnInsetsRequestedListener {\n        void onInsetsRequested(boolean enable);\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIActivity.kt",
    "content": "package me.weishu.kernelsu.ui.webui\n\nimport android.annotation.SuppressLint\nimport android.content.SharedPreferences\nimport android.os.Bundle\nimport android.widget.Toast\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.compose.animation.Crossfade\nimport androidx.compose.animation.core.tween\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.material3.ExperimentalMaterial3ExpressiveApi\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.CompositionLocalProvider\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.setValue\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\nimport me.weishu.kernelsu.ui.theme.KernelSUTheme\nimport me.weishu.kernelsu.ui.theme.ThemeController\nimport top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator\n\n@SuppressLint(\"SetJavaScriptEnabled\")\nclass WebUIActivity : ComponentActivity() {\n    override fun onCreate(savedInstanceState: Bundle?) {\n\n        enableEdgeToEdge()\n        window.isNavigationBarContrastEnforced = false\n\n        super.onCreate(savedInstanceState)\n\n        setContent {\n            val context = LocalContext.current\n            val prefs = context.getSharedPreferences(\"settings\", MODE_PRIVATE)\n            var appSettings by remember { mutableStateOf(ThemeController.getAppSettings(context)) }\n            var uiModeValue by remember { mutableStateOf(prefs.getString(\"ui_mode\", UiMode.DEFAULT_VALUE) ?: UiMode.DEFAULT_VALUE) }\n            val uiMode = remember(uiModeValue) {\n                UiMode.fromValue(uiModeValue)\n            }\n\n            DisposableEffect(prefs) {\n                val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->\n                    if (key in listOf(\"color_mode\", \"key_color\", \"color_style\", \"color_spec\")) {\n                        appSettings = ThemeController.getAppSettings(context)\n                    } else if (key == \"ui_mode\") {\n                        uiModeValue = prefs.getString(\"ui_mode\", UiMode.DEFAULT_VALUE) ?: UiMode.DEFAULT_VALUE\n                    }\n                }\n                prefs.registerOnSharedPreferenceChangeListener(listener)\n                onDispose { prefs.unregisterOnSharedPreferenceChangeListener(listener) }\n            }\n\n            CompositionLocalProvider(LocalUiMode provides uiMode) {\n                KernelSUTheme(appSettings = appSettings, uiMode = uiMode) {\n                    MainContent(activity = this, onFinish = { finish() })\n                }\n            }\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun MainContent(activity: ComponentActivity, onFinish: () -> Unit) {\n    val moduleId = remember { activity.intent.getStringExtra(\"id\") }\n    val webUIState = remember { WebUIState() }\n\n    LaunchedEffect(moduleId) {\n        if (moduleId == null) {\n            onFinish()\n            return@LaunchedEffect\n        }\n        prepareWebView(activity, moduleId, webUIState)\n    }\n\n    DisposableEffect(Unit) {\n        onDispose { webUIState.dispose(activity) }\n    }\n\n    when (val event = webUIState.uiEvent) {\n        is WebUIEvent.Error -> {\n            LaunchedEffect(event) {\n                Toast.makeText(activity, event.message, Toast.LENGTH_SHORT).show()\n                onFinish()\n            }\n        }\n\n        is WebUIEvent.Close -> {\n            LaunchedEffect(event) { onFinish() }\n        }\n\n        else -> {}\n    }\n    val isLoading = webUIState.uiEvent is WebUIEvent.Loading\n\n    Crossfade(targetState = isLoading, animationSpec = tween(300)) { loading ->\n        if (loading) {\n            LoadingContent()\n        } else {\n            WebUIScreen(webUIState = webUIState)\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3ExpressiveApi::class)\n@Composable\nprivate fun LoadingContent() {\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> {\n            Box(\n                modifier = Modifier.fillMaxSize(),\n                contentAlignment = Alignment.Center\n            ) {\n                InfiniteProgressIndicator()\n            }\n        }\n\n        UiMode.Material -> {\n            Box(\n                modifier = Modifier\n                    .fillMaxSize()\n                    .background(MaterialTheme.colorScheme.background),\n                contentAlignment = Alignment.Center\n            ) {\n                androidx.compose.material3.LoadingIndicator()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIMaterial.kt",
    "content": "package me.weishu.kernelsu.ui.webui\n\nimport android.content.Intent\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.material3.AlertDialog\nimport androidx.compose.material3.OutlinedTextField\nimport androidx.compose.material3.Text\nimport androidx.compose.material3.TextButton\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport me.weishu.kernelsu.R\n\n@Composable\nfun HandleWebUIEventMaterial(\n    webUIState: WebUIState,\n    fileLauncher: ActivityResultLauncher<Intent>\n) {\n    when (val event = webUIState.uiEvent) {\n        is WebUIEvent.ShowAlert -> {\n            val showDialog = remember(event) { mutableStateOf(true) }\n            if (showDialog.value) {\n                AlertDialog(\n                    onDismissRequest = {\n                        webUIState.onAlertResult()\n                        showDialog.value = false\n                    },\n                    confirmButton = {\n                        TextButton(\n                            onClick = {\n                                webUIState.onAlertResult()\n                                showDialog.value = false\n                            },\n                        ) {\n                            Text(stringResource(R.string.confirm))\n                        }\n                    },\n                    text = {\n                        Text(event.message)\n                    }\n                )\n            }\n        }\n\n        is WebUIEvent.ShowConfirm -> {\n            val showDialog = remember(event) { mutableStateOf(true) }\n            if (showDialog.value) {\n                AlertDialog(\n                    onDismissRequest = {\n                        webUIState.onConfirmResult(false)\n                        showDialog.value = false\n                    },\n                    confirmButton = {\n                        TextButton(\n                            onClick = {\n                                webUIState.onConfirmResult(true)\n                                showDialog.value = false\n                            },\n                        ) {\n                            Text(stringResource(R.string.confirm))\n                        }\n                    },\n                    dismissButton = {\n                        TextButton(\n                            onClick = {\n                                webUIState.onConfirmResult(false)\n                                showDialog.value = false\n                            },\n                        ) {\n                            Text(stringResource(android.R.string.cancel))\n                        }\n                    },\n                    text = {\n                        Text(event.message)\n                    }\n                )\n            }\n        }\n\n        is WebUIEvent.ShowPrompt -> {\n            val showDialog = remember(event) { mutableStateOf(true) }\n            val state = remember(event) { mutableStateOf(event.defaultValue) }\n            if (showDialog.value) {\n                AlertDialog(\n                    onDismissRequest = {\n                        webUIState.onPromptResult(null)\n                        showDialog.value = false\n                    },\n                    confirmButton = {\n                        TextButton(\n                            onClick = {\n                                webUIState.onPromptResult(state.value)\n                                showDialog.value = false\n                            },\n                        ) {\n                            Text(stringResource(R.string.confirm))\n                        }\n                    },\n                    dismissButton = {\n                        TextButton(\n                            onClick = {\n                                webUIState.onPromptResult(null)\n                                showDialog.value = false\n                            },\n                        ) {\n                            Text(stringResource(android.R.string.cancel))\n                        }\n                    },\n                    text = {\n                        Column {\n                            OutlinedTextField(\n                                label = { Text(event.message) },\n                                value = state.value,\n                                onValueChange = { state.value = it },\n                                modifier = Modifier.fillMaxWidth()\n                            )\n                        }\n                    }\n                )\n            }\n        }\n\n        is WebUIEvent.ShowFileChooser -> {\n            LaunchedEffect(event) {\n                try {\n                    fileLauncher.launch(event.intent)\n                } catch (_: Exception) {\n                    webUIState.onFileChooserResult(null)\n                }\n            }\n        }\n\n        else -> {}\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIMiuix.kt",
    "content": "package me.weishu.kernelsu.ui.webui\n\nimport android.content.Intent\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.compose.foundation.layout.Arrangement\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.height\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.width\nimport androidx.compose.foundation.text.input.rememberTextFieldState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.unit.dp\nimport me.weishu.kernelsu.R\nimport top.yukonga.miuix.kmp.basic.ButtonDefaults\nimport top.yukonga.miuix.kmp.basic.Text\nimport top.yukonga.miuix.kmp.basic.TextButton\nimport top.yukonga.miuix.kmp.basic.TextField\nimport top.yukonga.miuix.kmp.extra.WindowDialog\n\n@Composable\nfun HandleWebUIEventMiuix(\n    webUIState: WebUIState,\n    fileLauncher: ActivityResultLauncher<Intent>\n) {\n    when (val event = webUIState.uiEvent) {\n        is WebUIEvent.ShowAlert -> {\n            val showDialog = remember(event) { mutableStateOf(true) }\n            WindowDialog(\n                show = showDialog.value,\n                content = {\n                    Column {\n                        Text(event.message)\n                        Spacer(Modifier.height(12.dp))\n                        TextButton(\n                            modifier = Modifier.fillMaxWidth(),\n                            onClick = {\n                                webUIState.onAlertResult()\n                                showDialog.value = false\n                            },\n                            text = stringResource(R.string.confirm),\n                            colors = ButtonDefaults.textButtonColorsPrimary()\n                        )\n                    }\n                }\n            )\n        }\n\n        is WebUIEvent.ShowConfirm -> {\n            val showDialog = remember(event) { mutableStateOf(true) }\n            WindowDialog(\n                show = showDialog.value,\n                onDismissRequest = { webUIState.onConfirmResult(false) },\n                content = {\n                    Column {\n                        Text(event.message)\n                        Spacer(Modifier.height(12.dp))\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            horizontalArrangement = Arrangement.SpaceBetween\n                        ) {\n                            TextButton(\n                                onClick = {\n                                    webUIState.onConfirmResult(false)\n                                    showDialog.value = false\n                                },\n                                text = stringResource(android.R.string.cancel),\n                                modifier = Modifier.weight(1f),\n                            )\n                            Spacer(modifier = Modifier.width(20.dp))\n                            TextButton(\n                                onClick = {\n                                    webUIState.onConfirmResult(true)\n                                    showDialog.value = false\n                                },\n                                text = stringResource(R.string.confirm),\n                                modifier = Modifier.weight(1f),\n                                colors = ButtonDefaults.textButtonColorsPrimary()\n                            )\n                        }\n                    }\n                }\n            )\n        }\n\n        is WebUIEvent.ShowPrompt -> {\n            val showDialog = remember(event) { mutableStateOf(true) }\n            val state = rememberTextFieldState(event.defaultValue)\n            WindowDialog(\n                show = showDialog.value,\n                onDismissRequest = { webUIState.onPromptResult(null) },\n                content = {\n                    Column {\n                        Text(event.message)\n                        Spacer(Modifier.height(12.dp))\n                        TextField(\n                            modifier = Modifier.padding(bottom = 16.dp),\n                            state = state\n                        )\n                        Row(\n                            modifier = Modifier.fillMaxWidth(),\n                            horizontalArrangement = Arrangement.SpaceBetween\n                        ) {\n                            TextButton(\n                                onClick = {\n                                    webUIState.onPromptResult(null)\n                                    showDialog.value = false\n                                },\n                                text = stringResource(android.R.string.cancel),\n                                modifier = Modifier.weight(1f),\n                            )\n                            Spacer(modifier = Modifier.width(20.dp))\n                            TextButton(\n                                onClick = {\n                                    webUIState.onPromptResult(state.text.toString())\n                                    showDialog.value = false\n                                },\n                                text = stringResource(R.string.confirm),\n                                modifier = Modifier.weight(1f),\n                                colors = ButtonDefaults.textButtonColorsPrimary()\n                            )\n                        }\n                    }\n                }\n            )\n        }\n\n        is WebUIEvent.ShowFileChooser -> {\n            LaunchedEffect(event) {\n                try {\n                    fileLauncher.launch(event.intent)\n                } catch (_: Exception) {\n                    webUIState.onFileChooserResult(null)\n                }\n            }\n        }\n\n        else -> {}\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIScreen.kt",
    "content": "package me.weishu.kernelsu.ui.webui\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.net.Uri\nimport android.view.View\nimport android.view.ViewGroup\nimport android.webkit.WebView\nimport androidx.activity.compose.BackHandler\nimport androidx.activity.compose.rememberLauncherForActivityResult\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.PaddingValues\nimport androidx.compose.foundation.layout.WindowInsets\nimport androidx.compose.foundation.layout.asPaddingValues\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.layout.safeDrawing\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.DisposableEffect\nimport androidx.compose.runtime.LaunchedEffect\nimport androidx.compose.runtime.snapshotFlow\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalConfiguration\nimport androidx.compose.ui.platform.LocalDensity\nimport androidx.compose.ui.platform.LocalLayoutDirection\nimport androidx.compose.ui.unit.dp\nimport androidx.compose.ui.viewinterop.AndroidView\nimport androidx.lifecycle.Lifecycle\nimport androidx.lifecycle.LifecycleEventObserver\nimport androidx.lifecycle.compose.LocalLifecycleOwner\nimport me.weishu.kernelsu.ui.LocalUiMode\nimport me.weishu.kernelsu.ui.UiMode\n\n@Composable\nfun rememberFileLauncher(webUIState: WebUIState): ActivityResultLauncher<Intent> {\n    return rememberLauncherForActivityResult(\n        contract = ActivityResultContracts.StartActivityForResult()\n    ) { result ->\n        val uris: Array<Uri>? = if (result.resultCode == Activity.RESULT_OK) {\n            result.data?.let { data ->\n                data.clipData?.let { clipData ->\n                    Array(clipData.itemCount) { i -> clipData.getItemAt(i).uri }\n                } ?: data.data?.let { arrayOf(it) }\n            }\n        } else null\n        webUIState.onFileChooserResult(uris)\n    }\n}\n\n@Composable\nfun WebUIScreen(webUIState: WebUIState) {\n    val density = LocalDensity.current\n    val layoutDirection = LocalLayoutDirection.current\n    val windowInsets = WindowInsets.safeDrawing\n    val innerPadding = if (webUIState.isInsetsEnabled) PaddingValues(0.dp) else windowInsets.asPaddingValues()\n    val fileLauncher = rememberFileLauncher(webUIState)\n\n    LaunchedEffect(density, layoutDirection, windowInsets, webUIState.isInsetsEnabled) {\n        if (!webUIState.isInsetsEnabled) {\n            return@LaunchedEffect\n        }\n        snapshotFlow {\n            val top = (windowInsets.getTop(density) / density.density).toInt()\n            val bottom = (windowInsets.getBottom(density) / density.density).toInt()\n            val left = (windowInsets.getLeft(density, layoutDirection) / density.density).toInt()\n            val right = (windowInsets.getRight(density, layoutDirection) / density.density).toInt()\n            Insets(top, bottom, left, right)\n        }.collect { newInsets ->\n            if (webUIState.currentInsets != newInsets) {\n                webUIState.currentInsets = newInsets\n                webUIState.webView?.evaluateJavascript(newInsets.js, null)\n            }\n        }\n    }\n\n    BackHandler(enabled = webUIState.webCanGoBack) {\n        webUIState.webView?.goBack()\n    }\n\n    Box(\n        modifier = Modifier\n            .fillMaxSize()\n            .padding(innerPadding)\n    ) {\n        webUIState.webView?.let { webView ->\n            AndroidView(\n                modifier = Modifier.fillMaxSize(),\n                factory = { _ ->\n                    webView.apply {\n                        layoutParams = ViewGroup.LayoutParams(\n                            ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT\n                        )\n                        if (!webUIState.isUrlLoaded) {\n                            val homePage = \"https://mui.kernelsu.org/index.html\"\n                            if (width > 0 && height > 0) {\n                                loadUrl(homePage)\n                                webUIState.isUrlLoaded = true\n                            } else {\n                                val listener = object : View.OnLayoutChangeListener {\n                                    override fun onLayoutChange(\n                                        v: View, left: Int, top: Int, right: Int, bottom: Int,\n                                        oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int\n                                    ) {\n                                        if (v.width > 0 && v.height > 0) {\n                                            (v as WebView).loadUrl(homePage)\n                                            webUIState.isUrlLoaded = true\n                                            v.removeOnLayoutChangeListener(this)\n                                        }\n                                    }\n                                }\n                                addOnLayoutChangeListener(listener)\n                            }\n                        }\n                    }\n                }\n            )\n        }\n    }\n\n    when (LocalUiMode.current) {\n        UiMode.Miuix -> HandleWebUIEventMiuix(webUIState, fileLauncher)\n        UiMode.Material -> HandleWebUIEventMaterial(webUIState, fileLauncher)\n    }\n\n    HandleWebViewLifecycle(webUIState)\n    HandleConfigurationChanges(webUIState)\n}\n\n@Composable\nprivate fun HandleWebViewLifecycle(webUIState: WebUIState) {\n    val lifecycleOwner = LocalLifecycleOwner.current\n\n    DisposableEffect(lifecycleOwner, webUIState) {\n        val observer = LifecycleEventObserver { _, event ->\n            when (event) {\n                Lifecycle.Event.ON_RESUME -> webUIState.webView?.onResume()\n                Lifecycle.Event.ON_PAUSE -> webUIState.webView?.onPause()\n                else -> {}\n            }\n        }\n        lifecycleOwner.lifecycle.addObserver(observer)\n\n        onDispose {\n            lifecycleOwner.lifecycle.removeObserver(observer)\n        }\n    }\n}\n\n@Composable\nprivate fun HandleConfigurationChanges(webUIState: WebUIState) {\n    val configuration = LocalConfiguration.current\n    LaunchedEffect(configuration.fontScale, webUIState.webView) {\n        webUIState.webView?.settings?.textZoom = (configuration.fontScale * 100).toInt()\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIState.kt",
    "content": "package me.weishu.kernelsu.ui.webui\n\nimport android.app.Activity\nimport android.content.Intent\nimport android.net.Uri\nimport android.webkit.JsPromptResult\nimport android.webkit.JsResult\nimport android.webkit.WebView\nimport androidx.compose.runtime.getValue\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.setValue\nimport com.topjohnwu.superuser.Shell\nimport me.weishu.kernelsu.R\n\nsealed class WebUIEvent {\n    data object Loading : WebUIEvent()\n    data object WebViewReady : WebUIEvent()\n    data class Error(val message: String) : WebUIEvent()\n    data object Close : WebUIEvent()\n    data class ShowAlert(val message: String, val result: JsResult) : WebUIEvent()\n    data class ShowConfirm(val message: String, val result: JsResult) : WebUIEvent()\n    data class ShowPrompt(val message: String, val defaultValue: String, val result: JsPromptResult) : WebUIEvent()\n    data class ShowFileChooser(val intent: Intent) : WebUIEvent()\n}\n\nclass WebUIState {\n    var webView: WebView? = null\n    var rootShell: Shell? = null\n    lateinit var modDir: String\n    var moduleName: String = \"\"\n\n    var uiEvent by mutableStateOf<WebUIEvent>(WebUIEvent.Loading)\n    var isUrlLoaded = false\n    var currentInsets: Insets = Insets(0, 0, 0, 0)\n    var isInsetsEnabled by mutableStateOf(false)\n    var webCanGoBack by mutableStateOf(false)\n    var filePathCallback: android.webkit.ValueCallback<Array<Uri>>? = null\n\n    fun onAlertResult() {\n        val event = uiEvent\n        if (event is WebUIEvent.ShowAlert) {\n            event.result.confirm()\n            uiEvent = WebUIEvent.WebViewReady\n        }\n    }\n\n    fun onConfirmResult(confirmed: Boolean) {\n        val event = uiEvent\n        if (event is WebUIEvent.ShowConfirm) {\n            if (confirmed) event.result.confirm() else event.result.cancel()\n            uiEvent = WebUIEvent.WebViewReady\n        }\n    }\n\n    fun onPromptResult(result: String?) {\n        val event = uiEvent\n        if (event is WebUIEvent.ShowPrompt) {\n            if (result != null) event.result.confirm(result) else event.result.cancel()\n            uiEvent = WebUIEvent.WebViewReady\n        }\n    }\n\n    fun onFileChooserResult(uris: Array<Uri>?) {\n        filePathCallback?.onReceiveValue(uris)\n        filePathCallback = null\n        uiEvent = WebUIEvent.WebViewReady\n    }\n\n    fun requestExit() {\n        uiEvent = WebUIEvent.Close\n    }\n\n    fun dispose(activity: Activity) {\n        activity.setTaskDescription(activity.getString(R.string.app_name))\n        webView?.let { view ->\n            (view.parent as? android.view.ViewGroup)?.removeView(view)\n            view.destroy()\n        }\n        webView = null\n        rootShell?.close()\n    }\n}\n"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebViewHelper.kt",
    "content": "package me.weishu.kernelsu.ui.webui\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.app.ActivityManager\nimport android.content.Context\nimport android.content.Intent\nimport android.graphics.Color\nimport android.net.Uri\nimport android.os.Build\nimport android.webkit.JsPromptResult\nimport android.webkit.JsResult\nimport android.webkit.ValueCallback\nimport android.webkit.WebChromeClient\nimport android.webkit.WebResourceRequest\nimport android.webkit.WebResourceResponse\nimport android.webkit.WebView\nimport android.webkit.WebViewClient\nimport androidx.webkit.WebViewAssetLoader\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.withContext\nimport me.weishu.kernelsu.R\nimport me.weishu.kernelsu.data.repository.ModuleRepositoryImpl\nimport me.weishu.kernelsu.ui.util.createRootShell\nimport me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel\nimport java.io.File\n\nfun Activity.setTaskDescription(label: String) {\n    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {\n        @Suppress(\"DEPRECATION\")\n        setTaskDescription(ActivityManager.TaskDescription(label))\n    } else {\n        val taskDescription = ActivityManager.TaskDescription.Builder()\n            .setLabel(label)\n            .build()\n        setTaskDescription(taskDescription)\n    }\n}\n\n@SuppressLint(\"SetJavaScriptEnabled\")\ninternal suspend fun prepareWebView(\n    activity: Activity,\n    moduleId: String,\n    webUIState: WebUIState,\n) {\n    withContext(Dispatchers.IO) {\n        val repo = ModuleRepositoryImpl()\n        val modules = repo.getModules().getOrDefault(emptyList())\n        val moduleInfo = modules.find { info -> info.id == moduleId }\n\n        if (moduleInfo == null) {\n            withContext(Dispatchers.Main) {\n                webUIState.uiEvent = WebUIEvent.Error(activity.getString(R.string.no_such_module, moduleId))\n            }\n            return@withContext\n        }\n\n        if (!moduleInfo.hasWebUi || !moduleInfo.enabled || moduleInfo.update || moduleInfo.remove) {\n            withContext(Dispatchers.Main) {\n                webUIState.uiEvent = WebUIEvent.Error(activity.getString(R.string.module_unavailable, moduleInfo.name))\n            }\n            return@withContext\n        }\n\n        webUIState.moduleName = moduleInfo.name\n        webUIState.modDir = \"/data/adb/modules/${moduleId}\"\n\n        if (SuperUserViewModel.apps.isEmpty()) {\n            SuperUserViewModel().fetchAppList()\n        }\n        val shell = createRootShell(true)\n        webUIState.rootShell = shell\n\n        withContext(Dispatchers.Main) {\n            activity.setTaskDescription(activity.getString(R.string.app_name) + \" - ${moduleInfo.name}\")\n\n            val webView = WebView(activity)\n            webView.setBackgroundColor(Color.TRANSPARENT)\n\n            val prefs = activity.getSharedPreferences(\"settings\", Context.MODE_PRIVATE)\n            WebView.setWebContentsDebuggingEnabled(prefs.getBoolean(\"enable_web_debugging\", false))\n\n            webView.settings.apply {\n                javaScriptEnabled = true\n                domStorageEnabled = true\n                allowFileAccess = false\n            }\n\n            val webRoot = File(\"${webUIState.modDir}/webroot\")\n            val webViewAssetLoader = WebViewAssetLoader.Builder()\n                .setDomain(\"mui.kernelsu.org\")\n                .addPathHandler(\n                    \"/\",\n                    SuFilePathHandler(\n                        activity,\n                        webRoot,\n                        shell,\n                        { webUIState.currentInsets },\n                        { enable -> webUIState.isInsetsEnabled = enable })\n                )\n                .build()\n\n            // WebViewClient\n            webView.webViewClient = object : WebViewClient() {\n                override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? {\n                    val url = request.url\n                    if (url.scheme.equals(\"ksu\", ignoreCase = true) && url.host.equals(\"icon\", ignoreCase = true)) {\n                        val packageName = url.path?.substring(1)\n                        if (!packageName.isNullOrEmpty()) {\n                            val icon = AppIconUtil.loadAppIconSync(activity, packageName, 512)\n                            if (icon != null) {\n                                val stream = java.io.ByteArrayOutputStream()\n                                icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, stream)\n                                return WebResourceResponse(\"image/png\", null, java.io.ByteArrayInputStream(stream.toByteArray()))\n                            }\n                        }\n                    }\n                    return webViewAssetLoader.shouldInterceptRequest(url)\n                }\n\n                override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) {\n                    webUIState.webCanGoBack = view?.canGoBack() ?: false\n                    if (webUIState.isInsetsEnabled) webUIState.webView?.evaluateJavascript(webUIState.currentInsets.js, null)\n                    super.doUpdateVisitedHistory(view, url, isReload)\n                }\n            }\n\n            // WebChromeClient\n            webView.webChromeClient = object : WebChromeClient() {\n                override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {\n                    if (message == null || result == null) return false\n                    webUIState.uiEvent = WebUIEvent.ShowAlert(message, result)\n                    return true\n                }\n\n                override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {\n                    if (message == null || result == null) return false\n                    webUIState.uiEvent = WebUIEvent.ShowConfirm(message, result)\n                    return true\n                }\n\n                override fun onJsPrompt(\n                    view: WebView?,\n                    url: String?,\n                    message: String?,\n                    defaultValue: String?,\n                    result: JsPromptResult?\n                ): Boolean {\n                    if (message == null || result == null || defaultValue == null) return false\n                    webUIState.uiEvent = WebUIEvent.ShowPrompt(message, defaultValue, result)\n                    return true\n                }\n\n                override fun onShowFileChooser(\n                    webView: WebView?, filePathCallback: ValueCallback<Array<Uri>>?, fileChooserParams: FileChooserParams?\n                ): Boolean {\n                    webUIState.filePathCallback?.onReceiveValue(null)\n                    webUIState.filePathCallback = filePathCallback\n\n                    val intent = fileChooserParams?.createIntent() ?: Intent(Intent.ACTION_GET_CONTENT).apply { type = \"*/*\" }\n                    if (fileChooserParams?.mode == FileChooserParams.MODE_OPEN_MULTIPLE) {\n                        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)\n                    }\n                    webUIState.uiEvent = WebUIEvent.ShowFileChooser(intent)\n                    return true\n                }\n            }\n\n            // JS Interface\n            val webviewInterface = WebViewInterface(webUIState)\n            webUIState.webView = webView\n            webView.addJavascriptInterface(webviewInterface, \"ksu\")\n            webUIState.uiEvent = WebUIEvent.WebViewReady\n        }\n    }\n}"
  },
  {
    "path": "manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebViewInterface.kt",
    "content": "package me.weishu.kernelsu.ui.webui\n\nimport android.app.Activity\nimport android.content.pm.ApplicationInfo\nimport android.os.Handler\nimport android.os.Looper\nimport android.text.TextUtils\nimport android.view.Window\nimport android.webkit.JavascriptInterface\nimport android.widget.Toast\nimport androidx.core.content.pm.PackageInfoCompat\nimport androidx.core.view.WindowInsetsCompat\nimport androidx.core.view.WindowInsetsControllerCompat\nimport com.topjohnwu.superuser.CallbackList\nimport com.topjohnwu.superuser.ShellUtils\nimport com.topjohnwu.superuser.internal.UiThreadHandler\nimport me.weishu.kernelsu.ui.util.createRootShell\nimport me.weishu.kernelsu.ui.util.listModules\nimport me.weishu.kernelsu.ui.util.withNewRootShell\nimport me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel\nimport org.json.JSONArray\nimport org.json.JSONObject\nimport java.io.File\nimport java.util.concurrent.CompletableFuture\n\nclass WebViewInterface(private val state: WebUIState) {\n    private val webView get() = state.webView\n    private val modDir get() = state.modDir\n\n    @JavascriptInterface\n    fun exec(cmd: String): String {\n        return withNewRootShell(true) { ShellUtils.fastCmd(this, cmd) }\n    }\n\n    @JavascriptInterface\n    fun exec(cmd: String, callbackFunc: String) {\n        exec(cmd, null, callbackFunc)\n    }\n\n    private fun processOptions(sb: StringBuilder, options: String?) {\n        val opts = if (options == null) JSONObject() else {\n            JSONObject(options)\n        }\n\n        val cwd = opts.optString(\"cwd\")\n        if (!TextUtils.isEmpty(cwd)) {\n            sb.append(\"cd ${cwd};\")\n        }\n\n        opts.optJSONObject(\"env\")?.let { env ->\n            env.keys().forEach { key ->\n                sb.append(\"export ${key}=${env.getString(key)};\")\n            }\n        }\n    }\n\n    @JavascriptInterface\n    fun exec(\n        cmd: String,\n        options: String?,\n        callbackFunc: String\n    ) {\n        val finalCommand = StringBuilder()\n        processOptions(finalCommand, options)\n        finalCommand.append(cmd)\n\n        val result = withNewRootShell(true) {\n            newJob().add(finalCommand.toString()).to(ArrayList(), ArrayList()).exec()\n        }\n        val stdout = result.out.joinToString(separator = \"\\n\")\n        val stderr = result.err.joinToString(separator = \"\\n\")\n\n        val jsCode =\n            \"javascript: (function() { try { ${callbackFunc}(${result.code}, ${\n                JSONObject.quote(\n                    stdout\n                )\n            }, ${JSONObject.quote(stderr)}); } catch(e) { console.error(e); } })();\"\n        webView?.post {\n            webView?.loadUrl(jsCode)\n        }\n    }\n\n    @JavascriptInterface\n    fun spawn(command: String, args: String, options: String?, callbackFunc: String) {\n        val finalCommand = StringBuilder()\n\n        processOptions(finalCommand, options)\n\n        if (!TextUtils.isEmpty(args)) {\n            finalCommand.append(command).append(\" \")\n            JSONArray(args).let { argsArray ->\n                for (i in 0 until argsArray.length()) {\n                    finalCommand.append(argsArray.getString(i))\n                    finalCommand.append(\" \")\n                }\n            }\n        } else {\n            finalCommand.append(command)\n        }\n\n        val shell = createRootShell(true)\n\n        val emitData = fun(name: String, data: String) {\n            val jsCode =\n                \"javascript: (function() { try { ${callbackFunc}.${name}.emit('data', ${\n                    JSONObject.quote(\n                        data\n                    )\n                }); } catch(e) { console.error('emitData', e); } })();\"\n            webView?.post {\n                webView?.loadUrl(jsCode)\n            }\n        }\n\n        val stdout = object : CallbackList<String>(UiThreadHandler::runAndWait) {\n            override fun onAddElement(s: String) {\n                emitData(\"stdout\", s)\n            }\n        }\n\n        val stderr = object : CallbackList<String>(UiThreadHandler::runAndWait) {\n            override fun onAddElement(s: String) {\n                emitData(\"stderr\", s)\n            }\n        }\n\n        val future = shell.newJob().add(finalCommand.toString()).to(stdout, stderr).enqueue()\n        val completableFuture = CompletableFuture.supplyAsync {\n            future.get()\n        }\n\n        completableFuture.thenAccept { result ->\n            val emitExitCode =\n                \"javascript: (function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \\${e}`); } })();\"\n            webView?.post {\n                webView?.loadUrl(emitExitCode)\n            }\n\n            if (result.code != 0) {\n                val emitErrCode =\n                    \"javascript: (function() { try { var err = new Error(); err.exitCode = ${result.code}; err.message = ${\n                        JSONObject.quote(\n                            result.err.joinToString(\n                                \"\\n\"\n                            )\n                        )\n                    };${callbackFunc}.emit('error', err); } catch(e) { console.error('emitErr', e); } })();\"\n                webView?.post {\n                    webView?.loadUrl(emitErrCode)\n                }\n            }\n        }.whenComplete { _, _ ->\n            runCatching { shell.close() }\n        }\n    }\n\n    @JavascriptInterface\n    fun toast(msg: String) {\n        webView?.post {\n            webView?.let { Toast.makeText(it.context, msg, Toast.LENGTH_SHORT).show() }\n        }\n    }\n\n    @JavascriptInterface\n    fun fullScreen(enable: Boolean) {\n        val context = webView?.context\n        if (context is Activity) {\n            Handler(Looper.getMainLooper()).post {\n                if (enable) {\n                    hideSystemUI(context.window)\n                } else {\n                    showSystemUI(context.window)\n                }\n            }\n        }\n        enableEdgeToEdge(enable)\n    }\n\n    @JavascriptInterface\n    fun enableEdgeToEdge(enable: Boolean = true) {\n        state.isInsetsEnabled = enable\n    }\n\n    @JavascriptInterface\n    fun moduleInfo(): String {\n        val moduleInfos = JSONArray(listModules())\n        val currentModuleInfo = JSONObject()\n        currentModuleInfo.put(\"moduleDir\", modDir)\n        val moduleId = File(modDir).name\n        for (i in 0 until moduleInfos.length()) {\n            val currentInfo = moduleInfos.getJSONObject(i)\n\n            if (currentInfo.getString(\"id\") != moduleId) {\n                continue\n            }\n\n            val keys = currentInfo.keys()\n            for (key in keys) {\n                currentModuleInfo.put(key, currentInfo.get(key))\n            }\n            break\n        }\n        return currentModuleInfo.toString()\n    }\n\n    @JavascriptInterface\n    fun listPackages(type: String): String {\n        val packageNames = SuperUserViewModel.apps\n            .filter { appInfo ->\n                val flags = appInfo.packageInfo.applicationInfo?.flags ?: 0\n                when (type.lowercase()) {\n                    \"system\" -> (flags and ApplicationInfo.FLAG_SYSTEM) != 0\n                    \"user\" -> (flags and ApplicationInfo.FLAG_SYSTEM) == 0\n                    else -> true\n                }\n            }\n            .map { it.packageName }\n            .sorted()\n\n        val jsonArray = JSONArray()\n        for (pkgName in packageNames) {\n            jsonArray.put(pkgName)\n        }\n        return jsonArray.toString()\n    }\n\n    @JavascriptInterface\n    fun getPackagesInfo(packageNamesJson: String): String {\n        val packageNames = JSONArray(packageNamesJson)\n        val jsonArray = JSONArray()\n        val appMap = SuperUserViewModel.apps.associateBy { it.packageName }\n        for (i in 0 until packageNames.length()) {\n            val pkgName = packageNames.getString(i)\n            val appInfo = appMap[pkgName]\n            if (appInfo != null) {\n                val pkg = appInfo.packageInfo\n                val app = pkg.applicationInfo\n                val obj = JSONObject()\n                obj.put(\"packageName\", pkg.packageName)\n                obj.put(\"versionName\", pkg.versionName ?: \"\")\n                obj.put(\"versionCode\", PackageInfoCompat.getLongVersionCode(pkg))\n                obj.put(\"appLabel\", appInfo.label)\n                obj.put(\"isSystem\", if (app != null) ((app.flags and ApplicationInfo.FLAG_SYSTEM) != 0) else JSONObject.NULL)\n                obj.put(\"uid\", app?.uid ?: JSONObject.NULL)\n                jsonArray.put(obj)\n            } else {\n                val obj = JSONObject()\n                obj.put(\"packageName\", pkgName)\n                obj.put(\"error\", \"Package not found or inaccessible\")\n                jsonArray.put(obj)\n            }\n        }\n        return jsonArray.toString()\n    }\n\n    @JavascriptInterface\n    fun exit() {\n        state.requestExit()\n    }\n}\n\nfun hideSystemUI(window: Window) =\n    WindowInsetsControllerCompat(window, window.decorView).let { controller ->\n        controller.hide(WindowInsetsCompat.Type.systemBars())\n        controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE\n    }\n\nfun showSystemUI(window: Window) =\n    WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars())\n"
  },
  {
    "path": "manager/app/src/main/jniLibs/.gitignore",
    "content": "libksud.so"
  },
  {
    "path": "manager/app/src/main/res/drawable/ic_launcher_foreground.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n\n    <group\n        android:scaleX=\"0.135\"\n        android:scaleY=\"0.135\">\n        <path\n            android:pathData=\"M 259 259 H 541 V 541 H 259 V 259 Z\"\n            android:strokeWidth=\"18\"\n            android:strokeColor=\"#1e110d\" />\n        <path\n            android:fillColor=\"#1e110d\"\n            android:pathData=\"M 257 257 H 407 V 407 H 257 V 257 Z\" />\n        <path\n            android:fillColor=\"#1e110d\"\n            android:pathData=\"M 393 393 H 543 V 543 H 393 V 393 Z\" />\n    </group>\n</vector>"
  },
  {
    "path": "manager/app/src/main/res/drawable/ic_launcher_monochrome.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n\n    <group\n        android:scaleX=\"0.135\"\n        android:scaleY=\"0.135\">\n        <path\n            android:pathData=\"M 259 259 H 541 V 541 H 259 V 259 Z\"\n            android:strokeWidth=\"18\"\n            android:strokeColor=\"#000000\" />\n        <path\n            android:fillColor=\"#000000\"\n            android:pathData=\"M 257 257 H 407 V 407 H 257 V 257 Z\" />\n        <path\n            android:fillColor=\"#000000\"\n            android:pathData=\"M 393 393 H 543 V 543 H 393 V 393 Z\" />\n    </group>\n</vector>"
  },
  {
    "path": "manager/app/src/main/res/mipmap-anydpi/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n    <monochrome android:drawable=\"@drawable/ic_launcher_monochrome\" />\n</adaptive-icon>"
  },
  {
    "path": "manager/app/src/main/res/resources.properties",
    "content": "unqualifiedResLocale=en-US"
  },
  {
    "path": "manager/app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#FFFFFFFF</color>\n</resources>"
  },
  {
    "path": "manager/app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\" translatable=\"false\">KernelSU</string>\n    <string name=\"home\">Home</string>\n    <string name=\"home_not_installed\">Not installed</string>\n    <string name=\"home_click_to_install\">Tap to install</string>\n    <string name=\"home_working\">Working</string>\n    <string name=\"home_working_version\">Version: %d</string>\n    <string name=\"home_unsupported\">Unsupported</string>\n    <string name=\"home_unsupported_reason\">KernelSU only supports GKI kernels now, but you can patch the image for GKI devices.</string>\n    <string name=\"home_version_mismatch\">Manager version (%1$d) and KernelSU driver version (%2$d) mismatch.</string>\n    <string name=\"home_kernel\">Kernel version</string>\n    <string name=\"home_manager_version\">Manager version</string>\n    <string name=\"home_fingerprint\">Fingerprint</string>\n    <string name=\"home_selinux_status\">SELinux status</string>\n    <string name=\"selinux_status_disabled\">Disabled</string>\n    <string name=\"selinux_status_enforcing\">Enforcing</string>\n    <string name=\"selinux_status_permissive\">Permissive</string>\n    <string name=\"selinux_status_unknown\">Unknown</string>\n    <string name=\"superuser\">Superuser</string>\n    <string name=\"module_failed_to_enable\">Failed to enable module: %s</string>\n    <string name=\"module_failed_to_disable\">Failed to disable module: %s</string>\n    <string name=\"module_empty\">No module installed</string>\n    <string name=\"module\">Module</string>\n    <string name=\"module_install_prompt_with_name\">The following modules will be installed: %1$s</string>\n    <string name=\"module_sort_action_first\">Action first</string>\n    <string name=\"module_sort_enabled_first\">Enabled first</string>\n    <string name=\"module_repos\">Repos</string>\n    <string name=\"module_repos_sort_name\">Name (A → Z)</string>\n    <string name=\"module_repos_source_code\">Source code</string>\n    <string name=\"confirm\">Confirm</string>\n    <string name=\"uninstall\">Uninstall</string>\n    <string name=\"module_install\">Install</string>\n    <string name=\"install\">Install</string>\n    <string name=\"reboot\">Reboot</string>\n    <string name=\"settings\">Settings</string>\n    <string name=\"reboot_soft\">Soft restart</string>\n    <string name=\"reboot_userspace\">Userspace reboot</string>\n    <string name=\"reboot_recovery\">Reboot to Recovery</string>\n    <string name=\"reboot_bootloader\">Reboot to Bootloader</string>\n    <string name=\"reboot_download\">Reboot to Download</string>\n    <string name=\"reboot_edl\">Reboot to EDL</string>\n    <string name=\"about\">About</string>\n    <string name=\"module_uninstall_confirm\">Are you sure you want to uninstall module %s?</string>\n    <string name=\"metamodule_uninstall_confirm\">Are you sure you want to uninstall module %s? This action will affect all modules, and certain features provided by the metamodule (such as mounting) will no longer work.</string>\n    <string name=\"module_uninstall_success\">%s uninstalled</string>\n    <string name=\"module_uninstall_failed\">Failed to uninstall: %s</string>\n    <string name=\"module_version\">Version</string>\n    <string name=\"module_author\">Author</string>\n    <string name=\"show_system_apps\">Show system apps</string>\n    <string name=\"send_log\">Send logs</string>\n    <string name=\"safe_mode\">Safe mode</string>\n    <string name=\"jailbreak_mode\">Jailbreak mode</string>\n    <string name=\"home_jailbreak\">Jailbreak</string>\n    <string name=\"jailbreak_timeout\">Jailbreak may have failed, please check logs</string>\n    <string name=\"safe_mode_module_disabled\">Module installation is disabled in safe mode</string>\n    <string name=\"reboot_to_apply\">Reboot to take effect</string>\n    <string name=\"module_magisk_conflict\">Modules are unavailable due to a conflict with Magisk!</string>\n    <string name=\"home_learn_kernelsu\">Learn KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Learn how to install KernelSU and use modules.</string>\n    <string name=\"home_support_title\">Support Us</string>\n    <string name=\"home_support_content\">KernelSU is, and always will be, free, and open source. However, you can show us that you care by making a donation.</string>\n    <string name=\"home_gki_warning\">Starting from v3.0.0, the GKI work mode will be used only in testing environments. We do not recommend it for daily use, and image files will no longer be provided.</string>\n    <string name=\"about_source_code\"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string>\n    <string name=\"profile\" translatable=\"false\">App Profile</string>\n    <string name=\"profile_default\">Default</string>\n    <string name=\"profile_template\">Template</string>\n    <string name=\"profile_custom\">Custom</string>\n    <string name=\"profile_name\">Profile name</string>\n    <string name=\"profile_namespace\">Mount namespace</string>\n    <string name=\"profile_namespace_inherited\">Inherited</string>\n    <string name=\"profile_namespace_global\">Global</string>\n    <string name=\"profile_namespace_individual\">Individual</string>\n    <string name=\"profile_groups\">Groups</string>\n    <string name=\"profile_capabilities\">Capabilities</string>\n    <string name=\"profile_selinux_context\">SELinux context</string>\n    <string name=\"profile_umount_modules\">Umount modules</string>\n    <string name=\"failed_to_update_app_profile\">Failed to update App Profile for %s</string>\n    <string name=\"require_kernel_version\">The current KernelSU version %1$d is too low for the manager to work properly. Please upgrade to version %2$d or higher!</string>\n    <string name=\"settings_umount_modules_default\">Umount modules by default</string>\n    <string name=\"settings_umount_modules_default_summary\">The global default value for \\\"Umount modules\\\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\\'t have a profile set.</string>\n    <string name=\"profile_umount_modules_summary\">Enabling this option will allow KernelSU to restore any modified files by the modules for this app.</string>\n    <string name=\"profile_selinux_domain\">Domain</string>\n    <string name=\"profile_selinux_rules\">Rules</string>\n    <string name=\"module_update\">Update</string>\n    <string name=\"module_downloading\">Downloading module: %s</string>\n    <string name=\"module_start_downloading\">Start downloading: %s</string>\n    <string name=\"module_action_success\">Module action executed successfully.</string>\n    <string name=\"new_version_available\">New version %s is available, click to upgrade!</string>\n    <string name=\"launch_app\">Launch</string>\n    <string name=\"force_stop_app\">Force stop</string>\n    <string name=\"restart_app\">Restart</string>\n    <string name=\"failed_to_update_sepolicy\">Failed to update SELinux rules for %s</string>\n    <string name=\"su_not_allowed\">Couldn\\'t grant Superuser access to %s</string>\n    <string name=\"module_changelog\">Changelog</string>\n    <string name=\"settings_profile_template\">App Profile template</string>\n    <string name=\"settings_profile_template_summary\">Manage local and online template of App Profile.</string>\n    <string name=\"app_profile_template_create\">Create template</string>\n    <string name=\"app_profile_template_edit\">Edit template</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"app_profile_template_id_invalid\">Invalid template ID</string>\n    <string name=\"app_profile_template_name\">Name</string>\n    <string name=\"app_profile_template_description\">Description</string>\n    <string name=\"app_profile_template_save\">Save</string>\n    <string name=\"app_profile_template_delete\">Delete</string>\n    <string name=\"app_profile_template_view\">View template</string>\n    <string name=\"app_profile_template_id_exist\">Template ID already exists!</string>\n    <string name=\"app_profile_import_export\">Import/Export</string>\n    <string name=\"app_profile_import_from_clipboard\">Import from clipboard</string>\n    <string name=\"app_profile_export_to_clipboard\">Export to clipboard</string>\n    <string name=\"app_profile_template_export_empty\">Cannot find local template to export!</string>\n    <string name=\"app_profile_template_import_success\">Imported successfully</string>\n    <string name=\"app_profile_template_save_failed\">Failed to save template</string>\n    <string name=\"app_profile_template_import_empty\">Clipboard is empty!</string>\n    <string name=\"app_profile_affects_following_apps\">Affects the following apps</string>\n    <string name=\"settings_check_update\">Check for updates</string>\n    <string name=\"settings_check_update_summary\">Automatically check for updates when opening the app.</string>\n    <string name=\"settings_enable_predictive_back\">Predictive back gesture</string>\n    <string name=\"settings_enable_predictive_back_summary\">Enable predictive back gesture support.</string>\n    <string name=\"settings_module_check_update\">Check for module updates</string>\n    <string name=\"grant_root_failed\">Failed to grant root!</string>\n    <string name=\"home_pr_build_warning\">This is a PR debug build. Do NOT use it in production!</string>\n    <string name=\"home_pr_kernel_warning\">The kernel was built with PR signature support. This is not a production kernel!</string>\n    <string name=\"action\">Action</string>\n    <string name=\"open\">Open</string>\n    <string name=\"enable_web_debugging\">WebView debugging</string>\n    <string name=\"enable_web_debugging_summary\">Can be used to debug WebUI. Please enable only when needed.</string>\n    <string name=\"direct_install\">Direct install (Recommended)</string>\n    <string name=\"select_file\">Select a file</string>\n    <string name=\"install_inactive_slot\">Install to inactive slot (After OTA)</string>\n    <string name=\"install_inactive_slot_warning\">Your device will be **FORCED** to boot to the current inactive slot after a reboot!\\nOnly use this option after OTA is done.\\nContinue?</string>\n    <string name=\"jailbreak_flash_warning\">You are in **Jailbreak mode**. Flashing a partition on a device with a **locked bootloader** will break AVB (Android Verified Boot) and may cause the device to **fail to boot**.\\n\\nMake sure your bootloader is unlocked before proceeding!</string>\n    <string name=\"jailbreak_flash_warning_countdown\">Continue (%1$d)</string>\n    <string name=\"install_next\">Next</string>\n    <string name=\"install_select_partition\">Select partition</string>\n    <string name=\"install_upload_lkm_file\">Use local LKM file</string>\n    <string name=\"install_only_support_ko_file\">Only .ko files are supported</string>\n    <string name=\"select_file_tip\">%1$s partition image is recommended</string>\n    <string name=\"select_kmi\">Select KMI</string>\n    <string name=\"settings_uninstall\">Uninstall</string>\n    <string name=\"settings_uninstall_temporary\">Uninstall temporarily</string>\n    <string name=\"settings_uninstall_permanent\">Uninstall permanently</string>\n    <string name=\"settings_restore_stock_image\">Restore stock image</string>\n    <string name=\"settings_uninstall_temporary_message\">Temporarily uninstall KernelSU, restore to original state after next reboot.</string>\n    <string name=\"settings_uninstall_permanent_message\">Uninstalling KernelSU (root and all modules) completely and permanently.</string>\n    <string name=\"settings_restore_stock_image_message\">Restore the stock factory image (if a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \\\"Uninstall permanently\\\".</string>\n    <string name=\"flashing\">Flashing</string>\n    <string name=\"flash_success\">Flash success</string>\n    <string name=\"flash_failed\">Flash failed</string>\n    <string name=\"selected_lkm\">Selected LKM: %s</string>\n    <string name=\"save_log\">Save logs</string>\n    <string name=\"log_saved\">Logs saved</string>\n    <string name=\"settings_sucompat\">Classic SU command</string>\n    <string name=\"settings_sucompat_summary\">Allow root access via /system/bin/su, in new processes.</string>\n    <string name=\"settings_kernel_umount\">Umount modules (kernel-level)</string>\n    <string name=\"settings_kernel_umount_summary\">Unmount modules from kernel in App Profile.</string>\n    <string name=\"feature_status_unsupported_summary\">Kernel does not support this feature</string>\n    <string name=\"feature_status_managed_summary\">This feature is managed by a module</string>\n    <string name=\"settings_mode_enable_by_default\">Enable (Default)</string>\n    <string name=\"settings_mode_disable_until_reboot\">Disable until Reboot</string>\n    <string name=\"settings_mode_disable_always\">Always disable</string>\n    <string name=\"processing\">Processing…</string>\n    <string name=\"refresh_pulling\">Pull down to refresh</string>\n    <string name=\"refresh_release\">Release to refresh</string>\n    <string name=\"refresh_refresh\">Refreshing…</string>\n    <string name=\"refresh_complete\">Refreshed successfully</string>\n    <string name=\"undo\">Undo</string>\n    <string name=\"module_undo_uninstall_success\">Successfully canceled uninstall of %s</string>\n    <string name=\"module_undo_uninstall_failed\">Failed to undo uninstall: %s</string>\n    <string name=\"group_contains_apps\">Contains %d apps</string>\n    <string name=\"settings_theme\">Theme</string>\n    <string name=\"settings_theme_summary\">Choose the app theme mode.</string>\n    <string name=\"settings_theme_mode_system\">Follow system</string>\n    <string name=\"settings_theme_mode_light\">Light</string>\n    <string name=\"settings_theme_mode_dark\">Dark</string>\n    <string name=\"settings_monet\">Enable Monet</string>\n    <string name=\"settings_key_color\">Key color</string>\n    <string name=\"settings_key_color_default\">Default</string>\n    <string name=\"settings_ui_mode\">UI Style</string>\n    <string name=\"settings_ui_mode_summary\">Choose the interface style.</string>\n    <string name=\"settings_page_scale\">Page Scale</string>\n    <string name=\"settings_page_scale_summary\">Adjust the global display scale.</string>\n    <string name=\"settings_color_spec\">Color Spec</string>\n    <string name=\"settings_color_style\">Color Style</string>\n    <string name=\"color_blue\">Blue</string>\n    <string name=\"color_red\">Red</string>\n    <string name=\"color_green\">Green</string>\n    <string name=\"color_purple\">Purple</string>\n    <string name=\"color_orange\">Orange</string>\n    <string name=\"color_teal\">Teal</string>\n    <string name=\"color_pink\">Pink</string>\n    <string name=\"color_brown\">Brown</string>\n    <string name=\"color_deep_purple\">Deep Purple</string>\n    <string name=\"color_indigo\">Indigo</string>\n    <string name=\"color_cyan\">Cyan</string>\n    <string name=\"color_yellow\">Yellow</string>\n    <string name=\"color_amber\">Amber</string>\n    <string name=\"color_blue_grey\">Blue Grey</string>\n    <string name=\"color_sakura\">Sakura</string>\n    <string name=\"network_offline\">Not connected to network</string>\n    <string name=\"network_retry\">Retry</string>\n    <string name=\"tab_readme\">README</string>\n    <string name=\"tab_releases\">Releases</string>\n    <string name=\"tab_info\">Info</string>\n    <string name=\"module_shortcut_title\">Create shortcut</string>\n    <string name=\"module_shortcut_name_label\">Shortcut name</string>\n    <string name=\"module_shortcut_icon_pick\">Choose custom icon</string>\n    <string name=\"module_shortcut_not_supported\">Launcher does not support desktop shortcuts.</string>\n    <string name=\"module_shortcut_created\">Shortcut created on desktop.</string>\n    <string name=\"module_shortcut_updated\">Shortcut updated.</string>\n    <string name=\"module_shortcut_delete\">Delete shortcut</string>\n    <string name=\"module_shortcut_permission_tip_xiaomi\">Please enable \\\"Home screen shortcuts\\\" permission for this app in Xiaomi settings.</string>\n    <string name=\"module_shortcut_permission_tip_oppo\">Please enable \\\"Create Home screen shortcuts\\\" permission for this app in OPPO settings.</string>\n    <string name=\"module_shortcut_permission_tip_default\">If shortcut creation fails, please enable desktop shortcut permission for this app in system settings.</string>\n    <string name=\"no_such_module\">Module %s does not exist</string>\n    <string name=\"module_unavailable\">Module %s is disabled, updating, or pending removal</string>\n    <string name=\"select_file_tip_nogki\">Please select the GKI device image file you want to patch</string>\n    <string name=\"current_kmi\">KMI version of this device: %s</string>\n    <string name=\"current_device_kmi\">This device KMI</string>\n    <string name=\"settings_enable_blur\">Blur</string>\n    <string name=\"settings_enable_blur_summary\">Enable blur effect for top and bottom bars</string>\n    <string name=\"settings_floating_bottom_bar\">Floating bottom bar</string>\n    <string name=\"settings_floating_bottom_bar_summary\">Use Apple style floating bottom bar</string>\n    <string name=\"settings_enable_glass\">Liquid glass</string>\n    <string name=\"settings_enable_glass_summary\">Enable liquid glass effect for floating bottom bar</string>\n    <string name=\"show_only_primary_user_apps\">Show only primary user apps</string>\n    <string name=\"settings_auto_jailbreak\">Auto jailbreak</string>\n    <string name=\"settings_auto_jailbreak_summary\">Automatically use Magica to escalate privileges when Permissive SELinux is detected at boot. Requires granting autostart permission to this app.</string>\n    <string name=\"allow_shell\">Always grant root to Shell</string>\n    <string name=\"allow_shell_summary\">Always allow adb shell to call su. Do not enable this unless absolutely necessary.</string>\n    <string name=\"enable_adb\">Force enable adb on boot</string>\n    <string name=\"enable_adb_summary\">Force enable USB debugging and disable adb authentication. Do not enable this unless absolutely necessary.</string>\n    <string name=\"advanced_options\">Advanced Options</string>\n    <string name=\"expand\">Expand</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values/themes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Theme.KernelSU\" parent=\"android:Theme.Material.Light.NoActionBar\">\n        <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n    </style>\n\n    <style name=\"Theme.KernelSU.WebUI\" parent=\"Theme.KernelSU\">\n        <item name=\"android:windowLightStatusBar\">true</item>\n        <item name=\"android:windowLightNavigationBar\">true</item>\n    </style>\n\n</resources>"
  },
  {
    "path": "manager/app/src/main/res/values-ar/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">الرئيسية</string>\n    <string name=\"home_not_installed\">غير مثبت</string>\n    <string name=\"home_click_to_install\">إضغط للتثبيت</string>\n    <string name=\"home_working\">يعمل</string>\n    <string name=\"home_working_version\">الإصدار: %d</string>\n    <string name=\"home_unsupported\">غير مدعوم</string>\n    <string name=\"home_unsupported_reason\">KernelSU يدعم GKI kernels فقط</string>\n    <string name=\"home_kernel\">إصدار النواة</string>\n    <string name=\"home_manager_version\">إصدار المدير</string>\n    <string name=\"home_fingerprint\">البصمة</string>\n    <string name=\"home_selinux_status\">وضع SELinux</string>\n    <string name=\"selinux_status_disabled\">معطل</string>\n    <string name=\"selinux_status_enforcing\">مفروض</string>\n    <string name=\"selinux_status_permissive\">متساهل</string>\n    <string name=\"selinux_status_unknown\">مجهول</string>\n    <string name=\"superuser\">مستخدم خارق</string>\n    <string name=\"module_failed_to_enable\">فشل في تمكين الوحدة %s</string>\n    <string name=\"module_failed_to_disable\">فشل تعطيل الإضافة : %s</string>\n    <string name=\"module_empty\">لا توجد إضافات مثبتة</string>\n    <string name=\"module\">الإضافات</string>\n    <string name=\"uninstall\">إلغاء التثبيت</string>\n    <string name=\"module_install\">تثبيت الوحدة</string>\n    <string name=\"install\">تثبيت</string>\n    <string name=\"reboot\">إعادة تشغيل</string>\n    <string name=\"settings\">الإعدادات</string>\n    <string name=\"reboot_userspace\">إعادة تشغيل سريعة</string>\n    <string name=\"reboot_recovery\">إعادة تشغيل إلى وضع Recovery</string>\n    <string name=\"reboot_bootloader\">إعادة تشغيل إلى وضع Bootloader</string>\n    <string name=\"reboot_download\">إعادة تشغيل إلى وضع Download</string>\n    <string name=\"reboot_edl\">إعادة تشغيل إلى وضع EDL</string>\n    <string name=\"about\">من نحن</string>\n    <string name=\"module_uninstall_confirm\">هل أنت متأكد أنك تريد إلغاء تثبيت الإضافة %s ?</string>\n    <string name=\"module_uninstall_success\">تم إلغاء تثبيتها %s</string>\n    <string name=\"module_uninstall_failed\">فشل إلغاء تثبيت %s</string>\n    <string name=\"module_version\">الإصدار</string>\n    <string name=\"module_author\">المطور</string>\n    <string name=\"show_system_apps\">إظهار تطبيقات النظام</string>\n    <string name=\"send_log\">إرسال السجلات</string>\n    <string name=\"safe_mode\">الوضع الآمن</string>\n    <string name=\"reboot_to_apply\">إعادة التشغيل لتطبيق التغييرات</string>\n    <string name=\"module_magisk_conflict\">الوحدات غير متاحة بسبب تعارضها مع Magisk!</string>\n    <string name=\"home_learn_kernelsu\">تعلم KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">تعرف على كيفية تثبيت KernelSU واستخدام الإضافات</string>\n    <string name=\"home_support_title\">إدعمنا</string>\n    <string name=\"home_support_content\">KernelSU سيظل دائماً مجانياً ومفتوح المصدر. مع ذلك، يمكنك متى ما استطعت أن تظهر لنا أنك تهتم بالتبرع.</string>\n    <string name=\"about_source_code\"><![CDATA[أنظر إلى مصدر البرمجة في %1$s<br/>إنضم إلى قناتنا في %2$s ]]></string>\n    <string name=\"profile_capabilities\">القدرات</string>\n    <string name=\"module_update\">تحديث</string>\n    <string name=\"module_downloading\">تحميل الإضافة: %s</string>\n    <string name=\"module_start_downloading\">ابدأ التنزيل: %s</string>\n    <string name=\"new_version_available\">الإصدار الجديد: %s متاح ، انقر للتحديث.</string>\n    <string name=\"launch_app\">تشغيل</string>\n    <string name=\"profile_default\">الإفتراضي</string>\n    <string name=\"profile_template\">نموذج</string>\n    <string name=\"profile_namespace_inherited\">موروث</string>\n    <string name=\"profile_namespace_global\">عالمي</string>\n    <string name=\"profile_namespace_individual\">فردي</string>\n    <string name=\"profile_groups\">مجموعات</string>\n    <string name=\"profile_custom\">مُخصّص</string>\n    <string name=\"profile_namespace\">تركيب مساحة الاسم</string>\n    <string name=\"profile_umount_modules\">الغاء تحميل الإضافات</string>\n    <string name=\"failed_to_update_app_profile\">فشل تحديث ملف تعريف التطبيق لـ %s</string>\n    <string name=\"profile_selinux_context\">سياق SELinux</string>\n    <string name=\"force_stop_app\">ايقاف إجباري</string>\n    <string name=\"settings_umount_modules_default\">الغاء تحميل الإضافات بشكل افتراضي</string>\n    <string name=\"settings_umount_modules_default_summary\">القيمة الافتراضية العامة لـ\\\"إلغاء تحميل الإضافات\\\" في ملفات تعريف التطبيقات. إذا تم تمكينه، إزالة جميع تعديلات الإضافات على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف.</string>\n    <string name=\"profile_umount_modules_summary\">سيسمح تمكين هذا الخيار لـKernelSU باستعادة أي ملفات معدلة بواسطة الإضافات لهذا التطبيق.</string>\n    <string name=\"profile_selinux_domain\">المجال</string>\n    <string name=\"profile_selinux_rules\">القواعد</string>\n    <string name=\"restart_app\">إعادة تشغيل التطبيق</string>\n    <string name=\"failed_to_update_sepolicy\">فشل تحديث قواعد SELinux لـ %s</string>\n    <string name=\"profile_name\">اسم الملف الشخصي</string>\n    <string name=\"require_kernel_version\">إصدار KernelSU الحالي %1$d منخفض جدًا بحيث لا يعمل المدير بشكل صحيح. الرجاء الترقية إلى الإصدار %2$d أو أعلى!</string>\n    <string name=\"module_changelog\">سجل التغييرات</string>\n    <string name=\"app_profile_template_import_success\">تم الاستيراد بنجاح</string>\n    <string name=\"app_profile_export_to_clipboard\">تصدير إلى الحافظة</string>\n    <string name=\"app_profile_template_export_empty\">لا يمكن العثور على القالب المحلي للتصدير!</string>\n    <string name=\"app_profile_template_id_exist\">معرف القالب موجود بالفعل!</string>\n    <string name=\"app_profile_import_from_clipboard\">استيراد من الحافظة</string>\n    <string name=\"app_profile_template_name\">الاسم</string>\n    <string name=\"app_profile_template_id_invalid\">معرف القالب غير صالح</string>\n    <string name=\"app_profile_template_create\">إنشاء قالب</string>\n    <string name=\"app_profile_import_export\">استيراد / تصدير</string>\n    <string name=\"app_profile_template_save_failed\">فشل في حفظ القالب</string>\n    <string name=\"app_profile_template_edit\">تحرير القالب</string>\n    <string name=\"app_profile_template_id\">المعرف</string>\n    <string name=\"settings_profile_template\">قالب ملف تعريف التطبيق</string>\n    <string name=\"app_profile_template_description\">الوصف</string>\n    <string name=\"app_profile_template_save\">حفظ</string>\n    <string name=\"settings_profile_template_summary\">إدارة القالب المحلي وعبر الإنترنت لملف تعريف التطبيق</string>\n    <string name=\"app_profile_template_delete\">حذف</string>\n    <string name=\"app_profile_template_import_empty\">الحافظة فارغة!</string>\n    <string name=\"app_profile_template_view\">عرض القالب</string>\n    <string name=\"grant_root_failed\">فشل في منح صلاحية الجذر!</string>\n    <string name=\"open\">فتح</string>\n    <string name=\"settings_check_update_summary\">التحقق تلقائيًا من وجود تحديثات عند فتح التطبيق</string>\n    <string name=\"settings_check_update\">التحقق من التحديث</string>\n    <string name=\"enable_web_debugging\">تمكين تصحيح أخطاء WebView</string>\n    <string name=\"enable_web_debugging_summary\">يمكن استخدامه لتصحيح أخطاء WebUI، يرجى تمكينه فقط عند الحاجة.</string>\n    <string name=\"install_next\">التالي</string>\n    <string name=\"select_file\">اختيار ملف</string>\n    <string name=\"direct_install\">تثبيت مباشر (موصى به)</string>\n    <string name=\"install_inactive_slot\">التثبيت على فتحة غير نشطة (بعد OTA)</string>\n    <string name=\"install_inactive_slot_warning\">سيتم **إجبار** جهازك على التمهيد إلى الفتحة غير النشطة الحالية بعد إعادة التشغيل!\n\\nاستخدم هذا الخيار فقط بعد انتهاء التحديث.\n\\nأستمرار؟</string>\n    <string name=\"select_kmi\">اختر KMI</string>\n    <string name=\"select_file_tip\">يوصى باستخدام صورة القسم %1$s</string>\n    <string name=\"settings_uninstall\">إلغاء التثبيت</string>\n    <string name=\"settings_uninstall_temporary\">إلغاء التثبيت مؤقتًا</string>\n    <string name=\"settings_uninstall_permanent\">إلغاء التثبيت بشكل دائم</string>\n    <string name=\"settings_restore_stock_image\">استعادة الصورة الاصلية</string>\n    <string name=\"settings_uninstall_permanent_message\">‬إلغاء تثبيت KernelSU .(الجذر وجميع الوحدات) بشكل كامل ودائم.</string>\n    <string name=\"flashing\">تركيب</string>\n    <string name=\"flash_success\">نجح التركيب</string>\n    <string name=\"flash_failed\">فشل التركيب</string>\n    <string name=\"selected_lkm\">LKM المحددة: %s</string>\n    <string name=\"settings_restore_stock_image_message\">استعادة صورة المصنع المخزنة (في حالة وجود نسخة احتياطية)، والتي تُستخدم عادة قبل OTA؛ إذا كنت بحاجة إلى إلغاء تثبيت KernelSU، فيرجى استخدام \\\"إلغاء التثبيت الدائم\\\".</string>\n    <string name=\"settings_uninstall_temporary_message\">قم بإلغاء تثبيت KernelSU مؤقتًا، واستعد إلى حالته الأصلية بعد إعادة التشغيل التالية.</string>\n    <string name=\"save_log\">حفظ السجلات</string>\n    <string name=\"action\">إجراء</string>\n    <string name=\"log_saved\">السجلات محفوظة</string>\n    <string name=\"module_sort_enabled_first\">فرز (الممكن أولاً)</string>\n    <string name=\"module_sort_action_first\">فرز (الإجراء أولاً)</string>\n    <string name=\"module_install_prompt_with_name\">الحزم الاتيه سيتم تثبيتها %1$s</string>\n    <string name=\"confirm\">تأكيد</string>\n    <string name=\"su_not_allowed\">من الغير ممكن اعطاء صلاحيات (المسخدم الخارق) لـ %s</string>\n    <string name=\"settings_sucompat\">إعادة توجيه ثنائي su</string>\n    <string name=\"settings_sucompat_summary\">يسمح للتطبيقات التي تم منحها صلاحيات Superuser في ملف تعريف التطبيق بالحصول على shell superuser من خلال تنفيذ /system/bin/su؛ فعال فقط للعمليات الجديدة.</string>\n    <string name=\"settings_kernel_umount\">إلغاء تحميل النواة</string>\n    <string name=\"settings_kernel_umount_summary\">سلوك إلغاء تحميل مستوى النواة الذي يسيطر عليه KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-az/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Ana səhifə</string>\n    <string name=\"home_kernel\">Nüvə</string>\n    <string name=\"home_not_installed\">Yüklənmədi</string>\n    <string name=\"home_click_to_install\">Yükləmək üçün toxunun</string>\n    <string name=\"home_working\">İşləyir</string>\n    <string name=\"home_working_version\">Versiya: %d</string>\n    <string name=\"home_unsupported_reason\">Hal-hazırda KernelSU yalnız GKI nüvələrini dəstəkləyir</string>\n    <string name=\"home_unsupported\">Dəstəklənmir</string>\n    <string name=\"module_install\">Yüklə</string>\n    <string name=\"install\">Yüklə</string>\n    <string name=\"selinux_status_unknown\">Naməlum</string>\n    <string name=\"home_fingerprint\">Barmaq izi</string>\n    <string name=\"home_manager_version\">Menecer versiyası</string>\n    <string name=\"selinux_status_disabled\">Qeyri-aktiv</string>\n    <string name=\"home_selinux_status\">SELinux vəziyyəti</string>\n    <string name=\"selinux_status_permissive\">Sərbəst</string>\n    <string name=\"selinux_status_enforcing\">Məcburi</string>\n    <string name=\"superuser\">Super istifadəçi</string>\n    <string name=\"uninstall\">Sil</string>\n    <string name=\"module_failed_to_enable\">Modulu aktiv etmək mümkün olmadı: %s</string>\n    <string name=\"module_failed_to_disable\">Modulu deaktiv etmək mümkün olmadı: %s</string>\n    <string name=\"module_empty\">Heç bir modul quraşdırılmayıb</string>\n    <string name=\"module\">Modul</string>\n    <string name=\"reboot\">Yenidən başlat</string>\n    <string name=\"settings\">Parametrlər</string>\n    <string name=\"reboot_recovery\">Bərpa rejimində yenidən başlat</string>\n    <string name=\"reboot_userspace\">Yüngül vəziyyətdə yenodən başlat</string>\n    <string name=\"reboot_bootloader\">Bootloader rejimində yenidən başlat</string>\n    <string name=\"reboot_download\">Yükləmə rejimində yenidən başlat</string>\n    <string name=\"module_version\">Versiya</string>\n    <string name=\"module_author\">Sahib</string>\n    <string name=\"module_uninstall_confirm\">Modulu silmək istədiyinizdən əminsiniz %s\\?</string>\n    <string name=\"show_system_apps\">Sistem proqramlarını göstər</string>\n    <string name=\"about\">Haqqında</string>\n    <string name=\"reboot_edl\">EDL rejimində yenidən başlat</string>\n    <string name=\"module_uninstall_failed\">Silmək mümkün olmadı: %s</string>\n    <string name=\"module_uninstall_success\">%s silindi</string>\n    <string name=\"send_log\">Log-u göndər</string>\n    <string name=\"safe_mode\">Təhlükəsiz rejimi</string>\n    <string name=\"reboot_to_apply\">Qüvvəyə minməsi üçün yenidən başlat</string>\n    <string name=\"module_magisk_conflict\">Modular deaktiv edilir,çünki o Magisk-in modulları ilə toqquşur!</string>\n    <string name=\"home_learn_kernelsu\">KernelSU-yu öyrən</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_support_title\">Bizi dəstəkləyin</string>\n    <string name=\"home_click_to_learn_kernelsu\">KernelSU-yu necə quraşdırılacağını və modulların necə istifadə ediləcəyini öyrən</string>\n    <string name=\"profile_template\">Şablon</string>\n    <string name=\"profile_default\">Defolt</string>\n    <string name=\"profile_custom\">Özəl</string>\n    <string name=\"home_support_content\">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.</string>\n    <string name=\"about_source_code\">Mənbə kodlarımıza baxın %1$s<br/>\nKanalımıza %2$s qoşulun</string>\n    <string name=\"profile_name\">Profil adı</string>\n    <string name=\"profile_capabilities\">Bacarıqlar</string>\n    <string name=\"profile_umount_modules\">Modulları umount et</string>\n    <string name=\"profile_namespace_inherited\">Miras qalmış</string>\n    <string name=\"profile_namespace_global\">Qlobal</string>\n    <string name=\"profile_namespace\">Bölmənin ad sahəsi</string>\n    <string name=\"profile_namespace_individual\">Fərdi</string>\n    <string name=\"profile_groups\">Qruplar</string>\n    <string name=\"settings_umount_modules_default\">Defolt olaraq modulları umount et</string>\n    <string name=\"profile_selinux_context\">SELinux konteksi</string>\n    <string name=\"failed_to_update_app_profile\">%s görə tətbiq profillərini güncəlləmək mümkün olmadı</string>\n    <string name=\"settings_umount_modules_default_summary\">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.</string>\n    <string name=\"profile_selinux_domain\">Domen</string>\n    <string name=\"profile_selinux_rules\">Qaydalar</string>\n    <string name=\"module_update\">Güncəllə</string>\n    <string name=\"module_start_downloading\">Endirməni başlat: %s</string>\n    <string name=\"new_version_available\">Yeni versiya: %s əlçatandır, endirmək üçün toxunun</string>\n    <string name=\"module_downloading\">Modul yüklənir: %s</string>\n    <string name=\"profile_umount_modules_summary\">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.</string>\n    <string name=\"launch_app\">Aç</string>\n    <string name=\"force_stop_app\">Məcburi dayandır</string>\n    <string name=\"restart_app\">Təkrar başlat</string>\n    <string name=\"failed_to_update_sepolicy\">%s görə SELinux qaydalarını güncəlləmək mümkün olmadı</string>\n    <string name=\"save_log\">Girişləri Saxla</string>\n    <string name=\"require_kernel_version\">Cari KernelSU versiyası %1$d menecerin düzgün işləməsi üçün çox aşağıdır. Lütfən, %2$d və ya daha yüksək versiyaya təkmilləşdirin!</string>\n    <string name=\"module_install_prompt_with_name\">Aşağıdakı modullar quraşdırılacaq: %1$s</string>\n    <string name=\"confirm\">Təsdiq edin</string>\n    <string name=\"module_sort_action_first\">Sıralama (ilk hərəkət)</string>\n    <string name=\"module_sort_enabled_first\">Sıralama (Əvvəlcə aktivdir)</string>\n    <string name=\"module_changelog\">Dəyişikliklər jurnalı</string>\n    <string name=\"settings_profile_template\">Tətbiq Profil Şablonu</string>\n    <string name=\"settings_profile_template_summary\">Tətbiq Profilinə aid yerli və onlayn şablonların idarə olunması</string>\n    <string name=\"app_profile_template_create\">Şablon yarat</string>\n    <string name=\"app_profile_template_edit\">Şablonu redaktə et</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"app_profile_template_id_invalid\">Etibarsız şablon ID-si</string>\n    <string name=\"app_profile_template_name\">Ad</string>\n    <string name=\"app_profile_template_description\">Açıqlama</string>\n    <string name=\"su_not_allowed\">%s üçün Superistifadəçi girişi vermək mümkün olmadı.</string>\n    <string name=\"app_profile_template_save\">Yadda saxla</string>\n    <string name=\"app_profile_template_delete\">Sil</string>\n    <string name=\"app_profile_template_view\">Şablonu göstər</string>\n    <string name=\"app_profile_template_id_exist\">Şablon ID-si artıq mövcuddur!</string>\n    <string name=\"settings_sucompat\">su binary yönləndirmə</string>\n    <string name=\"settings_sucompat_summary\">Tətbiq Profilinə Superuser icazəsi verilmiş proqramları /system/bin/su vasitəsilə superuser shell əldə etməyə icazə verir; yalnız yeni proseslər üçün effektivdir.</string>\n    <string name=\"settings_kernel_umount\">Nüvə umount</string>\n    <string name=\"settings_kernel_umount_summary\">KernelSU tərəfindən idarə olunan nüvə səviyyəsində umount davranışı</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-bg/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Начало</string>\n    <string name=\"home_not_installed\">Не е инсталирано</string>\n    <string name=\"home_click_to_install\">Натиснете да инсталирате</string>\n    <string name=\"home_working\">Работи</string>\n    <string name=\"home_working_version\">Версия: %d</string>\n    <string name=\"home_unsupported\">Неподдържано</string>\n    <string name=\"home_unsupported_reason\">KernelSU само поддържа GKI кернели за сега.</string>\n    <string name=\"home_kernel\">Версия на кернела</string>\n    <string name=\"home_manager_version\">Версия на мениджъра</string>\n    <string name=\"home_fingerprint\">Пръстов отпечатък</string>\n    <string name=\"home_selinux_status\">SELinux статус</string>\n    <string name=\"selinux_status_disabled\">Деактивиран</string>\n    <string name=\"selinux_status_enforcing\">Налагащ</string>\n    <string name=\"selinux_status_permissive\">Разрешителен</string>\n    <string name=\"selinux_status_unknown\">Неизвестен</string>\n    <string name=\"superuser\">Суперпотребител</string>\n    <string name=\"module_failed_to_enable\">Неуспешно включване на модула: %s</string>\n    <string name=\"module_failed_to_disable\">Неуспешно изключване на модула: %s</string>\n    <string name=\"module_empty\">Няма инсталирани модули</string>\n    <string name=\"module\">Модул</string>\n    <string name=\"module_install_prompt_with_name\">Следните модули ще бъдат инсталирани: %1$s</string>\n    <string name=\"module_sort_action_first\">Първо действие</string>\n    <string name=\"module_sort_enabled_first\">Първо включено</string>\n    <string name=\"confirm\">Потвърди</string>\n    <string name=\"uninstall\">Деинсталиране</string>\n    <string name=\"module_install\">Инсалирай</string>\n    <string name=\"install\">Инсталирай</string>\n    <string name=\"reboot\">Рестартирай</string>\n    <string name=\"settings\">Настройки</string>\n    <string name=\"reboot_userspace\">Меко рестартиране</string>\n    <string name=\"reboot_recovery\">Рестартирай във възстановяване</string>\n    <string name=\"reboot_bootloader\">Рестартирай в Booloader</string>\n    <string name=\"reboot_download\">Рестартирай в Download</string>\n    <string name=\"reboot_edl\">Рестартирай в EDL</string>\n    <string name=\"about\">Относно</string>\n    <string name=\"module_uninstall_confirm\">Сигурни ли сте че искате да премахнете модула %s?</string>\n    <string name=\"metamodule_uninstall_confirm\">Сигурни ли сте, че искате да деинсталирате модула %s? Това действие ще засегне всички модули и някои функции, предоставяни от метамодула (като например \\\"mounting\\\"), вече няма да работят.</string>\n    <string name=\"module_uninstall_success\">%s премахнат</string>\n    <string name=\"module_uninstall_failed\">Неуспешно премахване на: %s</string>\n    <string name=\"module_version\">Версия</string>\n    <string name=\"module_author\">Автор</string>\n    <string name=\"show_system_apps\">Покажи системно приложения</string>\n    <string name=\"send_log\">Прати логове</string>\n    <string name=\"safe_mode\">Безопасен режим</string>\n    <string name=\"reboot_to_apply\">Рестартирай за да приеме ефект</string>\n    <string name=\"module_magisk_conflict\">Модулите не са налични поради конфликт с Magisk!</string>\n    <string name=\"home_learn_kernelsu\">Научи KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Научи как да изтеглите KernelSU и да използвате модули.</string>\n    <string name=\"home_support_title\">Помогнете Ни</string>\n    <string name=\"home_support_content\">KernelSU си е, и винаги ще е безплатен, и с отворен код. Обаче, можете да ни покажете че се интересувате като ни направите дарение.</string>\n    <string name=\"about_source_code\"><![CDATA[View source code at %1$s<br/>Присъединете се към нашия%2$s канал]]></string>\n    <string name=\"profile_default\">По подразбиране</string>\n    <string name=\"profile_template\">Шаблон</string>\n    <string name=\"profile_custom\">По избор</string>\n    <string name=\"profile_name\">Име на профил</string>\n    <string name=\"profile_namespace\">Прикачи Namespace</string>\n    <string name=\"profile_namespace_inherited\">Наследени</string>\n    <string name=\"profile_namespace_global\">Глобален</string>\n    <string name=\"profile_namespace_individual\">Индивид</string>\n    <string name=\"profile_groups\">Групи</string>\n    <string name=\"profile_capabilities\">Възможности</string>\n    <string name=\"profile_selinux_context\">SELinux контекст</string>\n    <string name=\"profile_umount_modules\">Разкачи модули по подразбиране</string>\n    <string name=\"failed_to_update_app_profile\">Неуспешно актуализиране на App Profile за %s</string>\n    <string name=\"require_kernel_version\">Текущата версия %1$d на KernelSU е твърде ниска, за да работи правилно мениджърът. Моля, актуализирайте към версия %2$d или по-висока!</string>\n    <string name=\"settings_umount_modules_default\">Разкачи модули</string>\n    <string name=\"settings_umount_modules_default_summary\">Глобалната стойност по подразбиране за „Разкачи модули“ в профила на приложението. Ако е активирана, ще премахне всички модификации на модулите в системата за приложения, които нямат зададен профил.</string>\n    <string name=\"profile_umount_modules_summary\">Активирането на тази опция ще позволи на KernelSU да възстанови всички модифицирани файлове от модулите за това приложение.</string>\n    <string name=\"profile_selinux_domain\">Домейн</string>\n    <string name=\"profile_selinux_rules\">Правила</string>\n    <string name=\"module_update\">Актуализиране</string>\n    <string name=\"module_downloading\">Изтегляне на модул: %s</string>\n    <string name=\"module_start_downloading\">Започни изтегляне: %s</string>\n    <string name=\"new_version_available\">Нова версия %s е налична, натисни да актуализирате!</string>\n    <string name=\"launch_app\">Стартирай</string>\n    <string name=\"force_stop_app\">Принудително спиране</string>\n    <string name=\"restart_app\">Радтартирай</string>\n    <string name=\"failed_to_update_sepolicy\">Неуспешно актуализиране на SELinux правила за %s</string>\n    <string name=\"su_not_allowed\">Даването на Суперпотребителски права на %s не беше успешно</string>\n    <string name=\"module_changelog\">Списък с промени</string>\n    <string name=\"settings_profile_template\">Шаблон за профил на приложение</string>\n    <string name=\"settings_profile_template_summary\">Управлявайте локален и онлайн шаблон на профил на приложението.</string>\n    <string name=\"app_profile_template_create\">Създаване на шаблон</string>\n    <string name=\"app_profile_template_edit\">Модифициране на шаблон</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"app_profile_template_id_invalid\">Невалидно шаблонено ID</string>\n    <string name=\"app_profile_template_name\">Име</string>\n    <string name=\"app_profile_template_description\">Описание</string>\n    <string name=\"app_profile_template_save\">Запази</string>\n    <string name=\"app_profile_template_delete\">Премахни</string>\n    <string name=\"app_profile_template_view\">Разгледай шаблон</string>\n    <string name=\"app_profile_template_id_exist\">Шаблонно ID вече съществува!</string>\n    <string name=\"app_profile_import_export\">Внасяне/Изкарване</string>\n    <string name=\"app_profile_import_from_clipboard\">Внасяне от клипборда</string>\n    <string name=\"app_profile_export_to_clipboard\">Изнасяне във клипборда</string>\n    <string name=\"app_profile_template_export_empty\">Не може да се намери локален шаблон за изнасяне!</string>\n    <string name=\"app_profile_template_import_success\">Внасяне успешно</string>\n    <string name=\"app_profile_template_save_failed\">Неуспешно запазване на шаблон</string>\n    <string name=\"app_profile_template_import_empty\">Клипборда е празен!</string>\n    <string name=\"app_profile_affects_following_apps\">Засегва следните приложения</string>\n    <string name=\"settings_check_update\">Провери за актуализаций</string>\n    <string name=\"settings_check_update_summary\">Автоматично провери за актуализаций когато отворите приложението.</string>\n    <string name=\"settings_module_check_update\">Провери за актуализаций на модулите</string>\n    <string name=\"grant_root_failed\">Неуспешно даване на root права!</string>\n    <string name=\"action\">Действие</string>\n    <string name=\"open\">Отвори</string>\n    <string name=\"enable_web_debugging\">Отстраняване на грешки на WebView</string>\n    <string name=\"enable_web_debugging_summary\">Може да се използва за отстраняване на грешки в WebUI. Моля активирайте само когато е необходимо.</string>\n    <string name=\"direct_install\">Директно изтегляне (Препоръчано)</string>\n    <string name=\"select_file\">Изберете файл</string>\n    <string name=\"install_inactive_slot\">Изтеглете в неактивния слот (След софтуерна актуализация)</string>\n    <string name=\"install_inactive_slot_warning\">Устройството ви ще бъде **ПРИНУДЕНО** да се стартира от неактивния слот след рестартиране!\\nИзползвайте тази опция само след като софтуерната актуализация приключи.\\nИскате ли да продължите?</string>\n    <string name=\"install_next\">Следваш</string>\n    <string name=\"install_select_partition\">Избери раздел</string>\n    <string name=\"install_upload_lkm_file\">Използвай локален LKM файл</string>\n    <string name=\"install_only_support_ko_file\">Само .ko файлове се поддържат</string>\n    <string name=\"select_file_tip\">Препоръчва се образ на дял %1$s</string>\n    <string name=\"select_kmi\">Изберете KMI</string>\n    <string name=\"settings_uninstall\">Премахнете</string>\n    <string name=\"settings_uninstall_temporary\">Премахнете временно</string>\n    <string name=\"settings_uninstall_permanent\">Премахнете завинаги</string>\n    <string name=\"settings_restore_stock_image\">Възстановяване на стоков .img фаил</string>\n    <string name=\"settings_uninstall_temporary_message\">Временно премахнете KernelSU, ще се върне в нормално състояние след следващото рестартиране.</string>\n    <string name=\"settings_uninstall_permanent_message\">Премахване на KernelSU (root и всички модули) напълно, завинаги.</string>\n    <string name=\"settings_restore_stock_image_message\">Възстановете фабричното копие (ако съществува резервно копие), обикновено използвано преди софтуерна актуализация; ако трябва да премахнете KernelSU, моля, използвайте „Премахване завинаги“.</string>\n    <string name=\"flashing\">Флашване</string>\n    <string name=\"flash_success\">Флашване успешно</string>\n    <string name=\"flash_failed\">Флашване не бе успешно</string>\n    <string name=\"selected_lkm\">Избран LKM: %s</string>\n    <string name=\"save_log\">Запази логове</string>\n    <string name=\"log_saved\">Логовете бяха запазени</string>\n    <string name=\"feature_status_unsupported_summary\">Кърнълът не поддържа тази функция.</string>\n    <string name=\"feature_status_managed_summary\">Тази функция е управлявана от модул.</string>\n    <string name=\"settings_sucompat\">Пренасочване на su бинарния файл</string>\n    <string name=\"settings_sucompat_summary\">Позволяет приложениям с предоставени права на Superuser в профила на приложението да получат superuser shell чрез изпълнение на /system/bin/su; в сила само за нови процеси.</string>\n    <string name=\"settings_kernel_umount\">Kernel umount</string>\n    <string name=\"settings_kernel_umount_summary\">Поведение за umount на ниво ядро, контролирано от KernelSU</string>\n    <string name=\"processing\">Обработва се…</string>\n    <string name=\"refresh_pulling\">Опънете надолу за да обновите</string>\n    <string name=\"refresh_release\">Пуснете за да обновите</string>\n    <string name=\"refresh_refresh\">Обновяване…</string>\n    <string name=\"refresh_complete\">Обновено успешно</string>\n    <string name=\"undo\">Откажи</string>\n    <string name=\"module_undo_uninstall_success\">Успешно отказано премахването на %s</string>\n    <string name=\"module_undo_uninstall_failed\">Неуспешно отказване на премахване: %s</string>\n    <string name=\"group_contains_apps\">Съдържа %d приложения</string>\n    <string name=\"settings_theme\">Тема</string>\n    <string name=\"settings_theme_summary\">Изберете режима на темата на приложението.</string>\n    <string name=\"settings_theme_mode_system\">Следвай системата</string>\n    <string name=\"settings_theme_mode_light\">Светъл</string>\n    <string name=\"settings_theme_mode_dark\">Тъмен</string>\n    <string name=\"settings_key_color\">Цвят на копчето</string>\n    <string name=\"settings_key_color_default\">По подразбиране</string>\n    <string name=\"color_blue\">Син</string>\n    <string name=\"color_red\">Червен</string>\n    <string name=\"color_green\">Зелен</string>\n    <string name=\"color_purple\">Лилав</string>\n    <string name=\"color_orange\">Оранжево</string>\n    <string name=\"color_teal\">Тил</string>\n    <string name=\"color_pink\">Розово</string>\n    <string name=\"color_brown\">Кафяво</string>\n    <string name=\"module_repos\">Репота</string>\n    <string name=\"module_repos_sort_name\">По име (А → Я)</string>\n    <string name=\"module_repos_source_code\">Изходен код</string>\n    <string name=\"home_gki_warning\">Започвайки от v3.0.0, работният режим на GKI ще се използва само в тестови среди. Не го препоръчваме за ежедневна употреба и файлове с изображения вече няма да се предоставят.</string>\n    <string name=\"network_offline\">Не е свързан с интернет</string>\n    <string name=\"network_retry\">Пробвай отново</string>\n    <string name=\"tab_readme\">ПРОЧЕТИ МЕ</string>\n    <string name=\"tab_releases\">Версии</string>\n    <string name=\"tab_info\">Информация</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-bn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">হোম</string>\n    <string name=\"home_not_installed\">ইনস্টল করা হয়নি</string>\n    <string name=\"home_click_to_install\">ইনস্টল করার জন্য ক্লিক করুন</string>\n    <string name=\"home_working\"> ওয়ার্কিং</string>\n    <string name=\"home_working_version\">ওয়ার্কিং সংস্করণ: %d</string>\n    <string name=\"home_unsupported\">অসমর্থিত</string>\n    <string name=\"home_unsupported_reason\">KernelSU শুধুমাত্র GKI কার্নেল সমর্থন করে</string>\n    <string name=\"home_kernel\">কার্নেল সংস্করণ</string>\n    <string name=\"home_manager_version\">ম্যানেজার সংস্করণ</string>\n    <string name=\"home_fingerprint\">ফিঙ্গারপ্রিন্ট</string>\n    <string name=\"home_selinux_status\">SELinux স্টেটাস</string>\n    <string name=\"selinux_status_disabled\">ডিজেবল</string>\n    <string name=\"selinux_status_enforcing\">কার্যকর</string>\n    <string name=\"selinux_status_permissive\">অনুমতিমূলক</string>\n    <string name=\"selinux_status_unknown\">অজানা</string>\n    <string name=\"superuser\">সুপার ইউজার</string>\n    <string name=\"module_failed_to_enable\">মডিউল সক্ষম করতে ব্যর্থ হয়েছে: %s</string>\n    <string name=\"module_failed_to_disable\">মডিউল নিষ্ক্রিয় করতে ব্যর্থ হয়েছে: %s</string>\n    <string name=\"module_empty\">কোন মডিউল ইনস্টল করা নেই</string>\n    <string name=\"module\">মডিউল</string>\n    <string name=\"uninstall\">আনইন্সটল</string>\n    <string name=\"module_install\">মডিউল ইনস্টল</string>\n    <string name=\"install\">ইনস্টল</string>\n    <string name=\"reboot\">রিবুট</string>\n    <string name=\"settings\">সেটিংস</string>\n    <string name=\"reboot_userspace\">সফট রিবুট</string>\n    <string name=\"reboot_recovery\">রিবুট রিকোভারি</string>\n    <string name=\"reboot_bootloader\">রিবুট বুটলোডার</string>\n    <string name=\"reboot_download\">রিবুট ডাউনলোড</string>\n    <string name=\"reboot_edl\">রিবুট ইডিএল</string>\n    <string name=\"about\">এবাউট</string>\n    <string name=\"module_uninstall_confirm\">মডিউল আনইনস্টল নিশ্চিত করুন %s?</string>\n    <string name=\"module_uninstall_success\">%s আনইনস্টল সফল</string>\n    <string name=\"module_uninstall_failed\">আনইন্সটল ব্যর্থ: %s</string>\n    <string name=\"module_version\">ভার্সন</string>\n    <string name=\"module_author\">লেখক</string>\n    <string name=\"show_system_apps\">শো সিস্টেম অ্যাপস</string>\n    <string name=\"send_log\">সেন্ড লগ</string>\n    <string name=\"safe_mode\">সেইফ মোড</string>\n    <string name=\"reboot_to_apply\">রিবুট এপ্লাই</string>\n    <string name=\"module_magisk_conflict\">মডিউলগুলি অক্ষম কারণ তারা ম্যাজিস্কের সাথে বিরোধিতা করে!</string>\n    <string name=\"home_learn_kernelsu\">লার্ন কার্নেলএসইউ</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">কিভাবে কার্নেলএসইউ ইনস্টল করতে হয় এবং মডিউল ব্যবহার করতে হয় তা শিখুন।</string>\n    <string name=\"home_support_title\">সাপোর্ট টাইটেল</string>\n    <string name=\"home_support_content\">কার্নেলএসইউ ফ্রি ও ওপেন সোর্স, এবং সবসময় এমনই থাকবে। তবে, আপনি অনুদান দিয়ে আপনার কৃতজ্ঞতা প্রদর্শন করতে পারেন।</string>\n    <string name=\"about_source_code\"><![CDATA[Bekijk source code op %1$s<br/>আমাদের %2$s চ্যানেল মার্জ করুন]]></string>\n    <string name=\"profile_name\">প্রফাইলের নাম</string>\n    <string name=\"profile_namespace\">নেমস্পেস মাউন্ট</string>\n    <string name=\"profile_groups\">গ্রুপস</string>\n    <string name=\"profile_capabilities\">যোগ্যতা</string>\n    <string name=\"profile_selinux_context\">এসই লিনাক্স কনটেক্সট</string>\n    <string name=\"profile_default\">ডিফল্ট</string>\n    <string name=\"profile_template\">টেমপ্লেট</string>\n    <string name=\"profile_custom\">কাস্টম</string>\n    <string name=\"profile_namespace_global\">গ্লোবাল</string>\n    <string name=\"profile_namespace_individual\">আলাদাভাবে</string>\n    <string name=\"profile_umount_modules\">আনমাউন্ট মোডিউল</string>\n    <string name=\"require_kernel_version\">ম্যানেজার সঠিকভাবে কাজ করার জন্য বর্তমান KernelSU সংস্করণ %1$d খুবই কম। অনুগ্রহ করে %2$d বা উচ্চতর সংস্করণে আপগ্রেড করুন!</string>\n    <string name=\"save_log\">লগ সংরক্ষণ করুন</string>\n    <string name=\"module_install_prompt_with_name\">এই মডিউলগুলো ইনস্টল করা হবেঃ %1$s</string>\n    <string name=\"module_sort_enabled_first\">চালুগুলো আগে</string>\n    <string name=\"module_repos_source_code\">সোর্স কোড</string>\n    <string name=\"confirm\">নিশ্চিত</string>\n    <string name=\"metamodule_uninstall_confirm\">আপনি কি নিশ্চিত আপনি মডিউল %s আনইনস্টল করতে চান? এটি সকল মডিউলকে প্রভাবিত করবে, এবং মেটামডিউল দ্বারা প্রদত্ত কিছু বৈশিষ্ট্য (যেমন মাউন্ট করা) আর কাজ করবে না।</string>\n    <string name=\"home_gki_warning\">v3.0.0 থেকে শুরু করে, GKI মোড শুধুমাত্র নিরীক্ষণ এর জন্য ব্যবহার করা যাবে। আমরা দৈনন্দিন ব্যবহারের জন্য এটি সুপারিশ করি না, এবং ইমেজ ফাইল আর প্রদান করা হবে না।</string>\n    <string name=\"failed_to_update_app_profile\">%s এর জন্য অ্যাপ প্রোফাইল আপডেট করতে ব্যর্থ হয়েছে</string>\n    <string name=\"settings_sucompat\">su বাইনারি রি-রুট করুন</string>\n    <string name=\"settings_sucompat_summary\">অ্যাপ প্রোফাইলে সুপার ইউজার অনুমতি প্রাপ্ত অ্যাপগুলির জন্য /system/bin/su কে ksud এ রিডাইরেক্ট করুন; শুধুমাত্র নতুন প্রসেসের জন্য কার্যকর।</string>\n    <string name=\"settings_kernel_umount\">কার্নেল আনমাউন্ট</string>\n    <string name=\"settings_kernel_umount_summary\">KernelSU দ্বারা নিয়ন্ত্রিত কার্নেল-লেভেল আনমাউন্ট আচরণ</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-bn-rBD/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_unsupported_reason\">কর্নেল এস ইউ কেবল মাত্র জিকআই কর্নেল সাপোর্ট করে</string>\n    <string name=\"home_selinux_status\">এসইলিনাক্স স্টেটাস</string>\n    <string name=\"selinux_status_unknown\">আননোন</string>\n    <string name=\"module_failed_to_enable\">মোডিউল ইনেবল করা যায়নি: %s</string>\n    <string name=\"home_click_to_install\">ইন্সটল করটে চাপুন</string>\n    <string name=\"home_working\">কাজ করছে</string>\n    <string name=\"home_unsupported\">অমূলক</string>\n    <string name=\"home_kernel\">কর্নেল</string>\n    <string name=\"home_manager_version\">ম্যানেজার ভারসন</string>\n    <string name=\"home_fingerprint\">ফিঙ্গারপ্রিন্ট</string>\n    <string name=\"selinux_status_disabled\">ডিসেবল</string>\n    <string name=\"selinux_status_enforcing\">এনফোর্সিং</string>\n    <string name=\"superuser\">সুপার ইউজার</string>\n    <string name=\"module\">মোডিউল</string>\n    <string name=\"uninstall\">আনইন্সটল</string>\n    <string name=\"module_install\">ইন্সটল</string>\n    <string name=\"install\">ইন্সটল</string>\n    <string name=\"reboot\">রিবুট</string>\n    <string name=\"settings\">সেটিংস</string>\n    <string name=\"reboot_userspace\">সফট রিবুট</string>\n    <string name=\"profile_namespace_global\">গ্লোবাল</string>\n    <string name=\"profile_groups\">গ্রুপস</string>\n    <string name=\"profile_selinux_context\">এসইলিনাক্স কন্টেক্সট</string>\n    <string name=\"failed_to_update_app_profile\">%s এর জন্য অ্যাপ প্রফাইল আপডেট করা যায়নি</string>\n    <string name=\"settings_umount_modules_default\">বাইডিফল্ট মোডিউল আনমাউন্ট</string>\n    <string name=\"home\">হোম</string>\n    <string name=\"home_not_installed\">ইন্সটল হয়নী</string>\n    <string name=\"selinux_status_permissive\">পারমিসিভ</string>\n    <string name=\"module_failed_to_disable\">মোডিউল ডিসেবল করা যায়নি: %s</string>\n    <string name=\"module_empty\">কোনো মোডিউল ইন্সটল করা নেই</string>\n    <string name=\"home_working_version\">সংস্করণ: %d</string>\n    <string name=\"profile_namespace\">নেইম স্পেস মাউন্ট</string>\n    <string name=\"profile_namespace_inherited\">ইনহেরিটেড</string>\n    <string name=\"profile_namespace_individual\">ইন্ডিভিজুয়াল</string>\n    <string name=\"profile_capabilities\">ক্যাপাবিলিটিস</string>\n    <string name=\"profile_umount_modules\">আনমাউন্ট মোডিউলস</string>\n    <string name=\"reboot_recovery\">রিকভারিতে বুট</string>\n    <string name=\"reboot_bootloader\">বুটলোডারে বুট</string>\n    <string name=\"reboot_download\">ডাউনলোড মডে বুট</string>\n    <string name=\"reboot_edl\">ইমারজেন্সি ডাউনলোড মডে বুট</string>\n    <string name=\"about\">অ্যাবাউট</string>\n    <string name=\"module_uninstall_confirm\">%s মোডিউল আনইনস্টলের বেপারে নিশ্চিৎ\\?</string>\n    <string name=\"module_uninstall_success\">%s আনইনস্টলড</string>\n    <string name=\"module_uninstall_failed\">%s আনইনস্টল করা যায়নি</string>\n    <string name=\"module_version\">ভার্সন</string>\n    <string name=\"module_author\">অথার</string>\n    <string name=\"save_log\">লগ সংরক্ষণ করুন</string>\n    <string name=\"settings_sucompat\">su বাইনারি পুনর্নির্দেশ করুন</string>\n    <string name=\"settings_sucompat_summary\">অ্যাপ প্রোফাইলে সুপারইউজার অনুমতি সহ অ্যাপের জন্য /system/bin/su কে ksud-এ পুনর্নির্দেশ করুন; শুধুমাত্র নতুন প্রক্রিয়ার জন্য কার্যকর।</string>\n    <string name=\"settings_kernel_umount\">কার্নেল আনমাউন্ট</string>\n    <string name=\"settings_kernel_umount_summary\">KernelSU দ্বারা নিয়ন্ত্রিত কার্নেল-লেভেল আনমাউন্ট আচরণ</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-bs/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"profile_namespace\">Imenski prostor nosača</string>\n    <string name=\"profile_namespace_inherited\">Naslijeđen</string>\n    <string name=\"profile_namespace_global\">Globalan</string>\n    <string name=\"profile_namespace_individual\">Pojedinačan</string>\n    <string name=\"profile_groups\">Grupe</string>\n    <string name=\"profile_capabilities\">Sposobnosti</string>\n    <string name=\"profile_selinux_context\">SELinux kontekst</string>\n    <string name=\"profile_umount_modules\">Umount module</string>\n    <string name=\"failed_to_update_app_profile\">Ažuriranje Profila Aplikacije za %s nije uspjelo</string>\n    <string name=\"require_kernel_version\">Trenutna KernelSU verzija %1$d je preniska da bi upravitelj ispravno radio. Molimo vas da nadogradite na verziju %2$d ili noviju!</string>\n    <string name=\"settings_umount_modules_default\">Umount module po zadanom</string>\n    <string name=\"settings_umount_modules_default_summary\">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.</string>\n    <string name=\"profile_umount_modules_summary\">Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju.</string>\n    <string name=\"module_update\">Ažuriranje</string>\n    <string name=\"module_downloading\">Skidanje module: %s</string>\n    <string name=\"module_start_downloading\">Započnite sa skidanjem: %s</string>\n    <string name=\"new_version_available\">Nova verzija: %s je dostupna, kliknite da skinete</string>\n    <string name=\"launch_app\">Pokrenite</string>\n    <string name=\"force_stop_app\">Prisilno Zaustavite</string>\n    <string name=\"restart_app\">Resetujte</string>\n    <string name=\"selinux_status_enforcing\">U Provođenju</string>\n    <string name=\"home\">Početna</string>\n    <string name=\"home_not_installed\">Nije instalirano</string>\n    <string name=\"home_click_to_install\">Kliknite da instalirate</string>\n    <string name=\"home_unsupported\">Nepodržano</string>\n    <string name=\"home_unsupported_reason\">KernelSU samo podržava GKI kernele sad</string>\n    <string name=\"home_manager_version\">Verzija Upravitelja</string>\n    <string name=\"home_fingerprint\">Otisak prsta</string>\n    <string name=\"home_selinux_status\">SELinux stanje</string>\n    <string name=\"module_install\">Instalirajte</string>\n    <string name=\"install\">Instalirajte</string>\n    <string name=\"reboot\">Ponovo pokrenite</string>\n    <string name=\"settings\">Podešavanja</string>\n    <string name=\"module_version\">Verzija</string>\n    <string name=\"module_author\">Autor</string>\n    <string name=\"show_system_apps\">Prikažite sistemske aplikacije</string>\n    <string name=\"safe_mode\">Sigurnosni mod</string>\n    <string name=\"reboot_to_apply\">Ponovo pokrenite da bi proradilo</string>\n    <string name=\"module_magisk_conflict\">\"Moduli su nedostupni  jer su u sukobu sa Magisk-om!\"</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Naučite kako da instalirate KernelSU i da koristite module</string>\n    <string name=\"home_support_title\">Podržite Nas</string>\n    <string name=\"send_log\">Pošaljite Izvještaj</string>\n    <string name=\"home_learn_kernelsu\">Naučite KernelSU</string>\n    <string name=\"about_source_code\">Pogledajte izvornu kodu na %1$s<br/>Pridružite nam se na %2$s kanalu</string>\n    <string name=\"profile_selinux_domain\">Domena</string>\n    <string name=\"profile_selinux_rules\">Pravila</string>\n    <string name=\"failed_to_update_sepolicy\">Neuspješno ažuriranje SELinux pravila za: %s</string>\n    <string name=\"home_working\">Radi</string>\n    <string name=\"home_working_version\">Verzija: %d</string>\n    <string name=\"home_kernel\">Kernel verzija</string>\n    <string name=\"selinux_status_permissive\">Permisivno</string>\n    <string name=\"uninstall\">Deinstalirajte</string>\n    <string name=\"selinux_status_unknown\">Nepoznato</string>\n    <string name=\"module_empty\">Nema instaliranih modula</string>\n    <string name=\"superuser\">Superkorisnik</string>\n    <string name=\"module\">Modula</string>\n    <string name=\"reboot_bootloader\">Ponovo pokrenite u Pogonski Učitavatelj</string>\n    <string name=\"reboot_recovery\">Ponovo pokrenite u Oporavu</string>\n    <string name=\"module_uninstall_success\">%s deinstalirana</string>\n    <string name=\"reboot_userspace\">Lagano Ponovo pokretanje</string>\n    <string name=\"module_failed_to_enable\">Neuspješno uključivanje module: %s</string>\n    <string name=\"reboot_download\">Ponovo pokrenite u Preuzimanje</string>\n    <string name=\"module_failed_to_disable\">Neuspješno isključivanje module: %s</string>\n    <string name=\"reboot_edl\">Ponovo pokrenite u EDL</string>\n    <string name=\"module_uninstall_failed\">Neuspješna deinstalacija: %s</string>\n    <string name=\"selinux_status_disabled\">Isključeno</string>\n    <string name=\"about\">O</string>\n    <string name=\"module_uninstall_confirm\">Jeste li sigurni da želite deinstalirati modulu %s\\?</string>\n    <string name=\"home_support_content\">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.</string>\n    <string name=\"profile_default\">Zadano</string>\n    <string name=\"profile_template\">Šablon</string>\n    <string name=\"profile_custom\">Prilagođeno</string>\n    <string name=\"profile_name\">Naziv profila</string>\n    <string name=\"save_log\">Sačuvaj Dnevnike</string>\n    <string name=\"module_install_prompt_with_name\">Modul će biti instaliran</string>\n    <string name=\"module_sort_action_first\">Sortiraj</string>\n    <string name=\"confirm\">Potvrdi</string>\n    <string name=\"settings_sucompat\">Preusmjeri su binarnu datoteku</string>\n    <string name=\"settings_sucompat_summary\">Preusmjeri /system/bin/su na ksud za aplikacije kojima je dodijeljeno dopuštenje Superuser u Profilu aplikacije; učinkovito samo za nove procese.</string>\n    <string name=\"settings_kernel_umount\">Kernel umount</string>\n    <string name=\"settings_kernel_umount_summary\">Ponašanje umount na razini kernela kontrolira KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-da/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_working\">Arbejder</string>\n    <string name=\"home_unsupported\">Ikke understøttet</string>\n    <string name=\"home_kernel\">Kernel-version</string>\n    <string name=\"home_unsupported_reason\">KernelSU understøtter nu kun GKI-kerner.</string>\n    <string name=\"home_manager_version\">Manager version</string>\n    <string name=\"home_selinux_status\">SELinux-status</string>\n    <string name=\"selinux_status_disabled\">Deaktiveret</string>\n    <string name=\"selinux_status_permissive\">Tilladende</string>\n    <string name=\"superuser\">Superbruger</string>\n    <string name=\"selinux_status_enforcing\">Håndhævende</string>\n    <string name=\"module_failed_to_disable\">Deaktivering af modul fejlede: %s</string>\n    <string name=\"module_empty\">Intet modul installeret</string>\n    <string name=\"uninstall\">Afinstaller</string>\n    <string name=\"module_install\">Installer</string>\n    <string name=\"install\">Installer</string>\n    <string name=\"reboot\">Genstart</string>\n    <string name=\"settings\">Indstillinger</string>\n    <string name=\"reboot_userspace\">Blød genstart</string>\n    <string name=\"reboot_download\">Genstart til Download</string>\n    <string name=\"reboot_edl\">Genstart til EDL</string>\n    <string name=\"about\">Om</string>\n    <string name=\"module_uninstall_confirm\">Er du sikker på, at du vil afinstallere modulet %s\\?</string>\n    <string name=\"module_uninstall_success\">%s afinstalleret</string>\n    <string name=\"module_uninstall_failed\">Afinstallation af: %s fejlede</string>\n    <string name=\"send_log\">Send logfiler</string>\n    <string name=\"safe_mode\">Sikker tilstand</string>\n    <string name=\"reboot_to_apply\">Genstart for at tage effekt</string>\n    <string name=\"home_learn_kernelsu\">Lær KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Lær hvordan man installerer KernelSU og moduler.</string>\n    <string name=\"about_source_code\"><![CDATA[View source code at %1$s<br/>Join our %2$s channel]]></string>\n    <string name=\"profile_default\">Standard</string>\n    <string name=\"profile_template\">Skabelon</string>\n    <string name=\"profile_namespace\">Monter namespace</string>\n    <string name=\"profile_namespace_inherited\">Arvet</string>\n    <string name=\"profile_namespace_global\">Global</string>\n    <string name=\"profile_groups\">Grupper</string>\n    <string name=\"profile_capabilities\">Evner</string>\n    <string name=\"profile_selinux_context\">SELinux-kontext</string>\n    <string name=\"profile_umount_modules\">Afmonteret moduler</string>\n    <string name=\"settings_umount_modules_default\">Afmontere moduler</string>\n    <string name=\"profile_umount_modules_summary\">Hvis denne indstilling aktiveres, kan KernelSU gendanne alle ændrede filer af modulerne til denne app.</string>\n    <string name=\"module_update\">Opdatering</string>\n    <string name=\"module_downloading\">Downloader modulet: %s</string>\n    <string name=\"new_version_available\">Ny version %s er tilgængelig, klik for at opgradere!</string>\n    <string name=\"launch_app\">Start</string>\n    <string name=\"force_stop_app\">Tving stop</string>\n    <string name=\"failed_to_update_sepolicy\">Opdatering af SELinux-regler mislykkedes for %s</string>\n    <string name=\"module_start_downloading\">Start download: %s</string>\n    <string name=\"home_click_to_install\">Klik for at installere</string>\n    <string name=\"home_working_version\">Version: %d</string>\n    <string name=\"home\">Hjem</string>\n    <string name=\"home_not_installed\">Ikke installeret</string>\n    <string name=\"home_fingerprint\">Fingeraftryk</string>\n    <string name=\"selinux_status_unknown\">Ukendt</string>\n    <string name=\"module_failed_to_enable\">Aktivering af modul fejlede: %s</string>\n    <string name=\"reboot_recovery\">Genstart til Recovery</string>\n    <string name=\"module\">Modul</string>\n    <string name=\"module_author\">Forfatter</string>\n    <string name=\"reboot_bootloader\">Genstart til Bootloader</string>\n    <string name=\"module_version\">Version</string>\n    <string name=\"show_system_apps\">Vis system-apps</string>\n    <string name=\"module_magisk_conflict\">Moduler er utilgængelige på grund af en konflikt med Magisk!</string>\n    <string name=\"home_support_title\">Støt Os</string>\n    <string name=\"home_support_content\">KernelSU er, og vil altid være, gratis og åben kildekode. Du kan dog vise os, at du holder af os, ved at give en donation.</string>\n    <string name=\"profile_custom\">Brugerdefineret</string>\n    <string name=\"profile_name\">Profilnavn</string>\n    <string name=\"profile_namespace_individual\">Individuel</string>\n    <string name=\"failed_to_update_app_profile\">Opdatering af App Profil for %s fejlede</string>\n    <string name=\"settings_umount_modules_default_summary\">Den globale standardværdi for \\\"Umount moduler\\\" i App Profile. Hvis aktiveret, fjernes alle modulændringer til systemet for apps, der ikke har en profil angivet.</string>\n    <string name=\"profile_selinux_domain\">Domæne</string>\n    <string name=\"profile_selinux_rules\">Regler</string>\n    <string name=\"restart_app\">Genstart</string>\n    <string name=\"require_kernel_version\">Den nuværende KernelSU-version %1$d er for lav til, at manageren fungerer korrekt. Opgrader venligst til version %2$d eller højere!</string>\n    <string name=\"save_log\">Gem logs</string>\n    <string name=\"module_install_prompt_with_name\">Følgende moduler installeres: %1$s</string>\n    <string name=\"module_sort_action_first\">Sorter (Handling først)</string>\n    <string name=\"module_sort_enabled_first\">Sorter (Aktiveret først)</string>\n    <string name=\"confirm\">Bekræft</string>\n    <string name=\"su_not_allowed\">Kunne ikke tildele superbruger-adgang til %s</string>\n    <string name=\"module_changelog\">Ændringslog</string>\n    <string name=\"settings_profile_template\">App-profilskabelon</string>\n    <string name=\"settings_profile_template_summary\">Administrer lokale og online skabeloner til App-profil.</string>\n    <string name=\"app_profile_template_create\">Opret skabelon</string>\n    <string name=\"app_profile_template_edit\">Rediger skabelon</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"app_profile_template_id_invalid\">Ugyldigt skabelon-ID</string>\n    <string name=\"app_profile_template_name\">Navn</string>\n    <string name=\"app_profile_template_description\">Beskrivelse</string>\n    <string name=\"app_profile_template_save\">Gem</string>\n    <string name=\"app_profile_template_delete\">Slet</string>\n    <string name=\"app_profile_template_view\">Visningsskabelon</string>\n    <string name=\"app_profile_template_id_exist\">Skabelon-ID findes allerede!</string>\n    <string name=\"app_profile_import_export\">Import/Eksport</string>\n    <string name=\"app_profile_import_from_clipboard\">Importér fra udklipsholder</string>\n    <string name=\"app_profile_export_to_clipboard\">Eksporter til udklipsholder</string>\n    <string name=\"app_profile_template_export_empty\">Kan ikke finde lokal skabelon til eksport!</string>\n    <string name=\"app_profile_template_import_success\">Importér med succes</string>\n    <string name=\"app_profile_template_save_failed\">Kunne ikke gemme skabelon</string>\n    <string name=\"app_profile_template_import_empty\">Udklipsholderen er tom!</string>\n    <string name=\"settings_check_update\">Check for opdateringer</string>\n    <string name=\"settings_check_update_summary\">Søg automatisk efter opdateringer, når appen åbnes.</string>\n    <string name=\"grant_root_failed\">Kunne ikke tildelle root!</string>\n    <string name=\"action\">Handling</string>\n    <string name=\"open\">Åbn</string>\n    <string name=\"enable_web_debugging\">WebView-fejlsøgning</string>\n    <string name=\"enable_web_debugging_summary\">Kan bruges til at fejlfinde af WebUI. Aktiver kun når det er nødvendigt.</string>\n    <string name=\"direct_install\">Direkte installation (Anbefalet)</string>\n    <string name=\"select_file\">Vælg en fil</string>\n    <string name=\"install_inactive_slot\">Installer på inaktiv slot (efter OTA-opdatering)</string>\n    <string name=\"install_inactive_slot_warning\">Din enhed vil blive **TVUNGET** til at starte op fra det aktuelt inaktive slot efter en genstart!\\nBrug kun denne mulighed, når OTA er færdig.\\nFortsæt?</string>\n    <string name=\"install_next\">Næste</string>\n    <string name=\"select_file_tip\">%1$s partitionsbillede anbefales</string>\n    <string name=\"select_kmi\">Vælg KMI</string>\n    <string name=\"settings_uninstall\">Afinstaller</string>\n    <string name=\"settings_uninstall_temporary\">Afinstaller midlertidigt</string>\n    <string name=\"settings_uninstall_permanent\">Afinstaller permanent</string>\n    <string name=\"settings_restore_stock_image\">Gendan systemets standardbillede</string>\n    <string name=\"settings_uninstall_temporary_message\">Afinstaller KernelSU midlertidigt, og gendan til den oprindelige tilstand efter næste genstart.</string>\n    <string name=\"settings_uninstall_permanent_message\">Afinstallation af KernelSU (root og alle moduler) fuldstændigt og permanent.</string>\n    <string name=\"settings_restore_stock_image_message\">Gendan det originale fabriksbillede (hvis en sikkerhedskopi eksisterer), hvilket normalt bruges før en OTA-opdatering; hvis du skal afinstallere KernelSU, skal du bruge \\\"Afinstaller permanent\\\".</string>\n    <string name=\"flashing\">Flashing</string>\n    <string name=\"flash_success\">Flash-succes</string>\n    <string name=\"flash_failed\">Flash mislykkedes</string>\n    <string name=\"selected_lkm\">Valgt LKM: %s</string>\n    <string name=\"log_saved\">Logs gemt</string>\n    <string name=\"settings_module_check_update\">Søg efter modulopdateringer</string>\n    <string name=\"install_upload_lkm_file\">Brug lokal KLM-fil</string>\n    <string name=\"install_only_support_ko_file\">Kun .ko-filer understøttes</string>\n    <string name=\"settings_sucompat\">Omdiriger su binary</string>\n    <string name=\"settings_sucompat_summary\">Tillader apps med Superuser-tilladelse i App Profile at få superuser shell ved at udføre /system/bin/su; gælder kun for nye processer.</string>\n    <string name=\"settings_kernel_umount\">Kernel umount</string>\n    <string name=\"settings_kernel_umount_summary\">Kernel-niveau umount adfærd kontrolleret af KernelSU</string>\n    <string name=\"processing\">Behandler…</string>\n    <string name=\"refresh_pulling\">Træk ned for at opdatere</string>\n    <string name=\"refresh_release\">Slip for at opdatere</string>\n    <string name=\"refresh_refresh\">Opdaterer…</string>\n    <string name=\"refresh_complete\">Opdateret</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-de/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Startseite</string>\n    <string name=\"home_not_installed\">Nicht installiert</string>\n    <string name=\"selinux_status_permissive\">Permissiv</string>\n    <string name=\"home_working\">Funktioniert</string>\n    <string name=\"home_working_version\">Version: %d</string>\n    <string name=\"superuser\">Superuser</string>\n    <string name=\"home_click_to_install\">Tippe zum Installieren</string>\n    <string name=\"selinux_status_unknown\">Unbekannt</string>\n    <string name=\"selinux_status_enforcing\">Erzwingen</string>\n    <string name=\"reboot_bootloader\">In den Bootloader-Modus neustarten</string>\n    <string name=\"reboot_download\">In den Download-Modus neustarten</string>\n    <string name=\"reboot_edl\">In den EDL-Modus neustarten</string>\n    <string name=\"module_author\">Autor</string>\n    <string name=\"about\">Über KernelSU</string>\n    <string name=\"module_magisk_conflict\">Module sind aufgrund eines Konfliktes mit Magisk nicht verfügbar!</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Erfahre, wie KernelSU installiert wird und wie Module verwendet werden.</string>\n    <string name=\"home_support_title\">Unterstütze uns</string>\n    <string name=\"home_support_content\">KernelSU ist und wird immer frei und quelloffen sein. Du kannst uns jedoch deine Unterstützung zeigen, indem du eine Spende tätigst.</string>\n    <string name=\"profile_selinux_context\">SELinux-Kontext</string>\n    <string name=\"settings_umount_modules_default\">Module standardmäßig aushängen</string>\n    <string name=\"settings_umount_modules_default_summary\">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.</string>\n    <string name=\"profile_default\">Standard</string>\n    <string name=\"profile_template\">Vorlage</string>\n    <string name=\"profile_custom\">Benutzerdefiniert</string>\n    <string name=\"failed_to_update_app_profile\">App-Profilaktualisierung für %s fehlgeschlagen</string>\n    <string name=\"profile_namespace_inherited\">Geerbt</string>\n    <string name=\"profile_namespace_global\">Global</string>\n    <string name=\"profile_namespace_individual\">Individuell</string>\n    <string name=\"profile_selinux_domain\">Domäne</string>\n    <string name=\"module_update\">Aktualisieren</string>\n    <string name=\"profile_umount_modules_summary\">Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen.</string>\n    <string name=\"profile_selinux_rules\">Regeln</string>\n    <string name=\"module_start_downloading\">Starte Download: %s</string>\n    <string name=\"failed_to_update_sepolicy\">Aktualisieren der SELinux-Regeln schlug fehl für %s</string>\n    <string name=\"launch_app\">Starten</string>\n    <string name=\"new_version_available\">Neue Version %s verfügbar, tippen zum Aktualisieren!</string>\n    <string name=\"force_stop_app\">Stopp erzwingen</string>\n    <string name=\"restart_app\">Neustarten</string>\n    <string name=\"home_manager_version\">Manager-Version</string>\n    <string name=\"home_selinux_status\">SELinux Status</string>\n    <string name=\"selinux_status_disabled\">Deaktiviert</string>\n    <string name=\"module_failed_to_enable\">Modulaktivierung fehlgeschlagen: %s</string>\n    <string name=\"module_failed_to_disable\">Moduldeaktivierung fehlgeschlagen: %s</string>\n    <string name=\"module_empty\">Keine Modul installiert</string>\n    <string name=\"module\">Modul</string>\n    <string name=\"uninstall\">Deinstallieren</string>\n    <string name=\"install\">Installieren</string>\n    <string name=\"reboot\">Neustarten</string>\n    <string name=\"settings\">Einstellungen</string>\n    <string name=\"reboot_recovery\">In den Recovery-Modus neustarten</string>\n    <string name=\"module_uninstall_success\">%s deinstalliert</string>\n    <string name=\"module_version\">Version</string>\n    <string name=\"show_system_apps\">System-Apps anzeigen</string>\n    <string name=\"send_log\">Protokoll senden</string>\n    <string name=\"home_learn_kernelsu\">KernelSU verstehen</string>\n    <string name=\"safe_mode\">Sicherer Modus</string>\n    <string name=\"reboot_to_apply\">Neustarten, damit Änderungen wirksam werden</string>\n    <string name=\"about_source_code\"><![CDATA[Quellcode einsehen unter %1$s<br/>Unserem %2$s-Kanal beitreten]]></string>\n    <string name=\"profile_name\">Profilname</string>\n    <string name=\"profile_namespace\">Namespace einhängen</string>\n    <string name=\"profile_groups\">Gruppen</string>\n    <string name=\"profile_capabilities\">Fähigkeiten</string>\n    <string name=\"profile_umount_modules\">Module aushängen</string>\n    <string name=\"module_downloading\">Lädt Modul %s herunter</string>\n    <string name=\"home_unsupported\">Nicht unterstützt</string>\n    <string name=\"home_unsupported_reason\">KernelSU unterstützt derzeit nur GKI-Kernel.</string>\n    <string name=\"home_kernel\">Kernelversion</string>\n    <string name=\"home_fingerprint\">Fingerabdruck</string>\n    <string name=\"module_install\">Installieren</string>\n    <string name=\"reboot_userspace\">Soft Reboot</string>\n    <string name=\"module_uninstall_confirm\">Möchtest du wirklich Modul %s deinstallieren?</string>\n    <string name=\"module_uninstall_failed\">Deinstallation fehlgeschlagen: %s</string>\n    <string name=\"require_kernel_version\">Die aktuelle KernelSU-Version %1$d ist zu alt für diese Manager-Version. Bitte auf Version %2$d oder höher aktualisieren!</string>\n    <string name=\"module_changelog\">Änderungsprotokoll</string>\n    <string name=\"app_profile_template_import_success\">Erfolgreich importiert</string>\n    <string name=\"app_profile_export_to_clipboard\">In Zwischenablage exportieren</string>\n    <string name=\"app_profile_template_export_empty\">Kann lokale Vorlage nicht finden!</string>\n    <string name=\"app_profile_template_id_exist\">Vorlagen-ID existiert bereits!</string>\n    <string name=\"app_profile_import_from_clipboard\">Aus Zwischenablage importieren</string>\n    <string name=\"app_profile_template_name\">Name</string>\n    <string name=\"app_profile_template_id_invalid\">Ungültige Vorlagen-ID</string>\n    <string name=\"app_profile_template_create\">Vorlage erstellen</string>\n    <string name=\"app_profile_import_export\">Import/Export</string>\n    <string name=\"app_profile_template_save_failed\">Schlug beim Speichern der Vorlage fehl</string>\n    <string name=\"app_profile_template_edit\">Vorlage bearbeiten</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"settings_profile_template\">App Profil-Vorlage</string>\n    <string name=\"app_profile_template_description\">Beschreibung</string>\n    <string name=\"app_profile_template_save\">Speichern</string>\n    <string name=\"settings_profile_template_summary\">Verwalte die lokale und online Vorlage des App-Profils.</string>\n    <string name=\"app_profile_template_delete\">Löschen</string>\n    <string name=\"app_profile_template_import_empty\">Zwischenablage ist leer!</string>\n    <string name=\"app_profile_template_view\">Vorlage ansehen</string>\n    <string name=\"enable_web_debugging\">WebView-Debugging</string>\n    <string name=\"enable_web_debugging_summary\">Kann zum Fehlerbeheben der WebUI verwendet werden. Bitte nur im Notfall aktivieren.</string>\n    <string name=\"select_file_tip\">%1$s Partitionsabbild empfohlen</string>\n    <string name=\"select_kmi\">KMI auswählen</string>\n    <string name=\"install_next\">Weiter</string>\n    <string name=\"direct_install\">Direkte Installation (empfohlen)</string>\n    <string name=\"select_file\">Datei auswählen</string>\n    <string name=\"install_inactive_slot\">In inaktiven Slot installieren (nach OTA)</string>\n    <string name=\"install_inactive_slot_warning\">Nach einem Neustart wird dein Gerät **GEZWUNGEN** in den derzeit inaktiven Slot zu starten!\n\\nBenutze dies nur nach Fertigstellung des OTA.\n\\nFortfahren?</string>\n    <string name=\"grant_root_failed\">Root-Zugriff konnte nicht gewährt werden!</string>\n    <string name=\"open\">Öffnen</string>\n    <string name=\"settings_check_update\">Auf Aktualisierungen prüfen</string>\n    <string name=\"settings_check_update_summary\">Prüfe automatisch auf Aktualisierungen, wenn die App geöffnet wird.</string>\n    <string name=\"settings_uninstall_temporary\">Temporär deinstallieren</string>\n    <string name=\"settings_uninstall\">Deinstallieren</string>\n    <string name=\"settings_uninstall_permanent_message\">KernelSU (Root und alle Module) vollständig und dauerhaft deinstallieren.</string>\n    <string name=\"save_log\">Protokolle Speichern</string>\n    <string name=\"settings_uninstall_permanent\">Permanent deinstallieren</string>\n    <string name=\"settings_restore_stock_image\">Standard-Abbild wiederherstellen</string>\n    <string name=\"settings_uninstall_temporary_message\">KernelSU temporär deinstallieren, originalen Status nach dem nächsten Neustart wiederherstellen.</string>\n    <string name=\"settings_restore_stock_image_message\">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\\\".</string>\n    <string name=\"flashing\">Schreibt</string>\n    <string name=\"flash_success\">Schreiben erfolgreich</string>\n    <string name=\"flash_failed\">Schreiben fehlgeschlagen</string>\n    <string name=\"selected_lkm\">Wähle LKM: %s</string>\n    <string name=\"action\">Aktion</string>\n    <string name=\"log_saved\">Protokolle gespeichert</string>\n    <string name=\"module_install_prompt_with_name\">Folgende Module werden installiert: %1$s</string>\n    <string name=\"confirm\">Bestätigen</string>\n    <string name=\"module_sort_action_first\">Aktion zuerst</string>\n    <string name=\"module_sort_enabled_first\">Aktiviert zuerst</string>\n    <string name=\"module_repos\">Repos</string>\n    <string name=\"module_repos_sort_name\">Name (A → Z)</string>\n    <string name=\"module_repos_source_code\">Quellcode</string>\n    <string name=\"metamodule_uninstall_confirm\">Sind Sie sich sicher, das Modul %s zu deinstallieren? Diese Aktion wirkt sich auf alle Module aus und spezielle Funktionen, welche von dem Metamodul bereitgestellt werden (sowie montieren), werden nicnt mehr funktioneren.</string>\n    <string name=\"safe_mode_module_disabled\">Modulinstallation ist im sicheren Modus deaktiviert</string>\n    <string name=\"home_gki_warning\">Ab v3.0.0 wird der GKI Arbeitsmodus nur in Testumgebungen genutzt werden. Wir empfehlen es nicht für die tägliche Nutzung und Bilddateien werden nicht mehr bereitgestellt werden.</string>\n    <string name=\"su_not_allowed\">Es konnte für %s keine Superuser-Berechtigung gegeben werden</string>\n    <string name=\"app_profile_affects_following_apps\">Wirkt sich auf folgende Apps aus</string>\n    <string name=\"settings_sucompat\">Su-Binärdatei umleiten</string>\n    <string name=\"settings_sucompat_summary\">Ermöglicht Anwendungen mit Superuser-Berechtigung im App-Profil, eine Superuser-Shell durch Ausführung von /system/bin/su zu erhalten; wirksam nur für neue Prozesse.</string>\n    <string name=\"settings_module_check_update\">Auf Modulaktualisierungen prüfen</string>\n    <string name=\"install_select_partition\">Partition wählen</string>\n    <string name=\"install_upload_lkm_file\">Nutze lokale LKM Datei</string>\n    <string name=\"install_only_support_ko_file\">Nur .ko Dateien unterstützt</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-es/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Inicio</string>\n    <string name=\"home_not_installed\">No instalado</string>\n    <string name=\"home_click_to_install\">Haz clic para instalar</string>\n    <string name=\"home_working\">Funcionando</string>\n    <string name=\"home_working_version\">Versión: %d</string>\n    <string name=\"home_unsupported\">Sin soporte</string>\n    <string name=\"home_unsupported_reason\">KernelSU solo admite kernels GKI por ahora</string>\n    <string name=\"home_kernel\">Versión del kernel</string>\n    <string name=\"home_manager_version\">Versión del gestor</string>\n    <string name=\"home_fingerprint\">Huella del dispositivo</string>\n    <string name=\"home_selinux_status\">Estado de SELinux</string>\n    <!-- It may be better to leave SELinux statuses untranslated -->\n    <string name=\"selinux_status_disabled\">Desactivado</string>\n    <string name=\"selinux_status_enforcing\">Estricto</string>\n    <string name=\"selinux_status_permissive\">Permisivo</string>\n    <string name=\"selinux_status_unknown\">Desconocido</string>\n    <string name=\"superuser\">Superusuario</string>\n    <string name=\"module_failed_to_enable\">Error al activar el módulo: %s</string>\n    <string name=\"module_failed_to_disable\">Error al desactivar el módulo: %s</string>\n    <string name=\"module_empty\">Ningún módulo instalado</string>\n    <string name=\"module\">Módulo</string>\n    <string name=\"uninstall\">Desinstalar</string>\n    <string name=\"module_install\">Instalar</string>\n    <string name=\"install\">Instalar</string>\n    <string name=\"reboot\">Reiniciar</string>\n    <string name=\"settings\">Ajustes</string>\n    <string name=\"reboot_userspace\">Reinicio suave</string>\n    <string name=\"reboot_recovery\">Reiniciar en modo de recuperación</string>\n    <string name=\"reboot_bootloader\">Reiniciar en modo de arranque</string>\n    <string name=\"reboot_download\">Reiniciar en modo Download</string>\n    <string name=\"reboot_edl\">Reiniciar en modo EDL</string>\n    <string name=\"about\">Acerca de</string>\n    <string name=\"module_uninstall_confirm\">¿Está seguro de que desea desinstalar el módulo %s?</string>\n    <string name=\"module_uninstall_success\">%s desinstalado</string>\n    <string name=\"module_uninstall_failed\">Fallo al desinstalar: %s</string>\n    <string name=\"module_version\">Versión</string>\n    <string name=\"module_author\">Autor</string>\n    <string name=\"show_system_apps\">Mostrar aplicaciones del sistema</string>\n    <string name=\"send_log\">Enviar registros</string>\n    <string name=\"safe_mode\">Modo seguro</string>\n    <string name=\"reboot_to_apply\">Reinicia para aplicar cambios</string>\n    <string name=\"module_magisk_conflict\">¡Los módulos no están disponibles debido a un conflicto con Magisk!</string>\n    <string name=\"home_learn_kernelsu\">Aprende KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Aprende a instalar KernelSU y a utilizar módulos</string>\n    <string name=\"home_support_title\">Apóyanos</string>\n    <string name=\"home_support_content\">KernelSU es, y siempre será, gratuito y de código abierto. Sin embargo, puedes demostrarnos que te importamos haciendo una donación.</string>\n    <string name=\"about_source_code\">Ver código fuente en %1$s<br/>\nÚnete a nuestro canal %2$s</string>\n    <string name=\"profile_default\">Predeterminado</string>\n    <string name=\"profile_template\">Plantilla</string>\n    <string name=\"profile_custom\">Personalizado</string>\n    <string name=\"profile_name\">Nombre de perfil</string>\n    <string name=\"profile_namespace\">Montaje del espacio de nombres</string>\n    <string name=\"profile_namespace_inherited\">Heredado</string>\n    <string name=\"profile_namespace_global\">Global</string>\n    <string name=\"profile_namespace_individual\">Individual</string>\n    <string name=\"profile_groups\">Grupos</string>\n    <string name=\"profile_capabilities\">Capacidades</string>\n    <string name=\"profile_selinux_context\">Contexto SELinux</string>\n    <string name=\"profile_umount_modules\">Desmontar módulos</string>\n    <string name=\"failed_to_update_app_profile\">Error al actualizar el perfil de la aplicación para %s</string>\n    <string name=\"settings_umount_modules_default\">Desmontar módulos por defecto</string>\n    <string name=\"settings_umount_modules_default_summary\">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.</string>\n    <string name=\"profile_umount_modules_summary\">Activar esta opción permitirá a KernelSU restaurar cualquier archivo modificado por los módulos para esta aplicación.</string>\n    <string name=\"profile_selinux_domain\">Dominio</string>\n    <string name=\"profile_selinux_rules\">Reglas</string>\n    <string name=\"module_update\">Actualizar</string>\n    <string name=\"module_downloading\">Descargando módulo: %s</string>\n    <string name=\"module_start_downloading\">Iniciar descarga: %s</string>\n    <string name=\"new_version_available\">La nueva versión %s está disponible, haga clic para actualizar.</string>\n    <string name=\"launch_app\">Iniciar</string>\n    <string name=\"force_stop_app\">Forzar detención</string>\n    <string name=\"restart_app\">Reiniciar</string>\n    <string name=\"failed_to_update_sepolicy\">Error al actualizar las reglas SELinux para: %s</string>\n    <string name=\"require_kernel_version\">La versión %1$d actual de KernelSU es demasiado baja para que el gestor funcione correctamente. Por favor, ¡actualice a la versión %2$d o superior!</string>\n    <string name=\"module_changelog\">Registro de cambios</string>\n    <string name=\"app_profile_template_import_success\">Importado con éxito</string>\n    <string name=\"app_profile_export_to_clipboard\">Exportar al portapapeles</string>\n    <string name=\"app_profile_template_export_empty\">¡No se encuentra la plantilla local para exportar!</string>\n    <string name=\"app_profile_template_id_exist\">¡El ID de plantilla ya existe!</string>\n    <string name=\"app_profile_import_from_clipboard\">Importar desde el portapapeles</string>\n    <string name=\"app_profile_template_name\">Nombre</string>\n    <string name=\"app_profile_template_id_invalid\">ID de plantilla no válida</string>\n    <string name=\"app_profile_template_create\">Crear plantilla</string>\n    <string name=\"app_profile_import_export\">Importar/Exportar</string>\n    <string name=\"app_profile_template_save_failed\">No se ha podido guardar la plantilla</string>\n    <string name=\"app_profile_template_edit\">Editar plantilla</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"settings_profile_template\">Plantilla de perfil de aplicación</string>\n    <string name=\"app_profile_template_description\">Descripción</string>\n    <string name=\"app_profile_template_save\">Guardar</string>\n    <string name=\"settings_profile_template_summary\">Gestionar la plantilla local y en línea de App Profile</string>\n    <string name=\"app_profile_template_delete\">Eliminar</string>\n    <string name=\"app_profile_template_import_empty\">¡El portapapeles está vacío!</string>\n    <string name=\"app_profile_template_view\">Ver plantilla</string>\n    <string name=\"save_log\">Guardar registros</string>\n    <string name=\"enable_web_debugging\">Activar la depuración de WebView</string>\n    <string name=\"select_file_tip\">Se recomienda la imagen de partición %1$s</string>\n    <string name=\"select_kmi\">Selecciona KMI</string>\n    <string name=\"install_next\">Siguiente</string>\n    <string name=\"direct_install\">Instalación directa (Recomendada)</string>\n    <string name=\"install_inactive_slot_warning\">¡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?</string>\n    <string name=\"settings_uninstall\">Desinstalar</string>\n    <string name=\"settings_restore_stock_image\">Restaurar imagen de archivo</string>\n    <string name=\"settings_uninstall_temporary_message\">Desinstalar temporalmente KernelSU, restaurar al estado original tras el siguiente reinicio.</string>\n    <string name=\"selected_lkm\">LKM seleccionado: %s</string>\n    <string name=\"flash_failed\">Flash falló</string>\n    <string name=\"flash_success\">Éxito de Flash</string>\n    <string name=\"grant_root_failed\">¡No se ha podido conceder el acceso root!</string>\n    <string name=\"open\">Abrir</string>\n    <string name=\"select_file\">Seleccione un archivo</string>\n    <string name=\"install_inactive_slot\">Instalar en ranura inactiva (Después de OTA)</string>\n    <string name=\"settings_uninstall_temporary\">Desinstalar temporalmente</string>\n    <string name=\"settings_uninstall_permanent\">Desinstalar permanentemente</string>\n    <string name=\"settings_uninstall_permanent_message\">Desinstalar KernelSU (Root y todos los módulos) completa y permanentemente.</string>\n    <string name=\"settings_check_update\">Comprobar actualización</string>\n    <string name=\"settings_check_update_summary\">Comprobación automática de actualizaciones al abrir la aplicación</string>\n    <string name=\"enable_web_debugging_summary\">Puede ser usado para depurar WebUI, por favor habilítalo sólo cuando sea necesario.</string>\n    <string name=\"settings_restore_stock_image_message\">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\\\".</string>\n    <string name=\"settings_sucompat\">Redirigir binario su</string>\n    <string name=\"settings_sucompat_summary\">Permite que las aplicaciones con permiso de Superusuario en el perfil de la aplicación obtengan un shell de superusuario ejecutando /system/bin/su; efectivo solo para nuevos procesos.</string>\n    <string name=\"settings_kernel_umount\">Desmontaje del kernel</string>\n    <string name=\"settings_kernel_umount_summary\">Comportamiento de desmontaje a nivel de kernel controlado por KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-et/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_working\">Töötamine</string>\n    <string name=\"home_working_version\">Versioon: %d</string>\n    <string name=\"home_kernel\">Tuum</string>\n    <string name=\"home_manager_version\">Manageri versioon</string>\n    <string name=\"home_fingerprint\">Sõrmejälg</string>\n    <string name=\"selinux_status_permissive\">Lubav</string>\n    <string name=\"module_failed_to_enable\">Mooduli lubamine ebaõnnestus: %s</string>\n    <string name=\"module_empty\">Mooduleid pole paigaldatud</string>\n    <string name=\"reboot\">Taaskäivita</string>\n    <string name=\"reboot_recovery\">Taaskäivita taastusesse</string>\n    <string name=\"module_uninstall_confirm\">Kas soovid kindlasti eemaldada mooduli %s?</string>\n    <string name=\"module_uninstall_success\">%s eemaldatud</string>\n    <string name=\"send_log\">Saada logid</string>\n    <string name=\"safe_mode\">Turvarežiim</string>\n    <string name=\"reboot_to_apply\">Muudatuste rakendamiseks taaskäivita</string>\n    <string name=\"home_learn_kernelsu\">Õpi KernelSUd</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"profile_default\">Vaikimisi</string>\n    <string name=\"profile_namespace\">Haagi nimeruum</string>\n    <string name=\"profile_umount_modules\">Lahtihaagitud moodulid</string>\n    <string name=\"failed_to_update_app_profile\">Rakenduseprofiili uuendamine %s jaoks ebaõnnestus</string>\n    <string name=\"settings_umount_modules_default\">Haagi moodulid vaikimisi lahti</string>\n    <string name=\"module_start_downloading\">Allalaadimise alustamine: %s</string>\n    <string name=\"failed_to_update_sepolicy\">SELinux reeglite uuendamine ebaõnnestus: %s</string>\n    <string name=\"app_profile_template_edit\">Muuda malli</string>\n    <string name=\"settings_profile_template\">Rakenduseprofiili mall</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"app_profile_template_id_exist\">Malli ID juba eksisteerib!</string>\n    <string name=\"app_profile_export_to_clipboard\">Ekspordi lõikelauale</string>\n    <string name=\"home\">Kodu</string>\n    <string name=\"home_click_to_install\">Klõpsa paigaldamiseks</string>\n    <string name=\"home_not_installed\">Pole paigaldatud</string>\n    <string name=\"home_unsupported\">Mittetoetatud</string>\n    <string name=\"home_unsupported_reason\">KernelSU toetab hetkel vaid GSI tuumasid</string>\n    <string name=\"home_selinux_status\">SELinuxi olek</string>\n    <string name=\"selinux_status_disabled\">Keelatud</string>\n    <string name=\"selinux_status_enforcing\">Jõustav</string>\n    <string name=\"selinux_status_unknown\">Teadmata</string>\n    <string name=\"superuser\">Superkasutaja</string>\n    <string name=\"module_failed_to_disable\">Mooduli keelamine ebaõnnestus: %s</string>\n    <string name=\"module\">Moodul</string>\n    <string name=\"reboot_bootloader\">Taaskäivita käivituslaadurisse</string>\n    <string name=\"uninstall\">Eemalda</string>\n    <string name=\"install\">Paigalda</string>\n    <string name=\"about\">Teave</string>\n    <string name=\"module_install\">Paigalda</string>\n    <string name=\"settings\">Seaded</string>\n    <string name=\"reboot_userspace\">Pehme taaskäivitus</string>\n    <string name=\"reboot_download\">Taaskäivita allalaadimisrežiimi</string>\n    <string name=\"reboot_edl\">Taaskäivita EDL-i</string>\n    <string name=\"module_author\">Autor</string>\n    <string name=\"module_uninstall_failed\">Eemaldamine ebaõnnestus: %s</string>\n    <string name=\"module_version\">Versioon</string>\n    <string name=\"show_system_apps\">Kuva süsteemirakendused</string>\n    <string name=\"module_magisk_conflict\">Moodulid pole saadaval Magiski konflikti tõttu!</string>\n    <string name=\"home_click_to_learn_kernelsu\">Õpi KernelSUd paigaldama ja mooduleid kasutama</string>\n    <string name=\"home_support_title\">Toeta meid</string>\n    <string name=\"profile_groups\">Grupid</string>\n    <string name=\"home_support_content\">KernelSU on, ja alati jääb, tasuta ning avatud lähtekoodiga kättesaadavaks. Sellegipoolest võid sa näidata, et hoolid, ning teha annetuse.</string>\n    <string name=\"profile_template\">Mall</string>\n    <string name=\"about_source_code\">Vaata lähtekoodi %1$sis<br/>\nLiitu meie %2$si kanaliga</string>\n    <string name=\"profile_name\">Profiili nimi</string>\n    <string name=\"profile_custom\">Kohandatud</string>\n    <string name=\"profile_namespace_inherited\">Päritud</string>\n    <string name=\"profile_namespace_global\">Globaalne</string>\n    <string name=\"profile_namespace_individual\">Individuaalne</string>\n    <string name=\"profile_capabilities\">Võimekused</string>\n    <string name=\"app_profile_template_id_invalid\">Sobimatu malli ID</string>\n    <string name=\"profile_selinux_context\">SELinux kontekst</string>\n    <string name=\"require_kernel_version\">Praegune KernelSU versioon %1$d on liiga madal, haldur ei saa korrektselt töötada. Palun täienda versioonile %2$d või kõrgem!</string>\n    <string name=\"profile_selinux_domain\">Domeen</string>\n    <string name=\"launch_app\">Käivita</string>\n    <string name=\"force_stop_app\">Sundpeata</string>\n    <string name=\"profile_selinux_rules\">Reeglid</string>\n    <string name=\"module_update\">Uuenda</string>\n    <string name=\"module_downloading\">Mooduli allalaadimine: %s</string>\n    <string name=\"new_version_available\">Uus versioon %s on saadaval, klõpsa täiendamiseks.</string>\n    <string name=\"restart_app\">Taaskäivita</string>\n    <string name=\"module_changelog\">Muudatuste logi</string>\n    <string name=\"app_profile_template_name\">Nimi</string>\n    <string name=\"app_profile_template_description\">Kirjeldus</string>\n    <string name=\"app_profile_template_import_success\">Edukalt imporditud</string>\n    <string name=\"app_profile_template_save\">Salvesta</string>\n    <string name=\"app_profile_template_import_empty\">Lõikelaud on tühi!</string>\n    <string name=\"app_profile_template_delete\">Kustuta</string>\n    <string name=\"app_profile_template_view\">Vaata malli</string>\n    <string name=\"app_profile_import_export\">Impordi/ekspordi</string>\n    <string name=\"app_profile_import_from_clipboard\">Impordi lõikelaualt</string>\n    <string name=\"app_profile_template_save_failed\">Malli salvestamine ebaõnnestus</string>\n    <string name=\"app_profile_template_create\">Loo mall</string>\n    <string name=\"settings_profile_template_summary\">Halda kohalikke ja võrgusolevaid rakenduseprofiili malle</string>\n    <string name=\"profile_umount_modules_summary\">Selle valiku lubamine lubab KernelSU-l taastada selle rakenduse moodulite poolt mistahes muudetud faile.</string>\n    <string name=\"app_profile_template_export_empty\">Ei saa eksportida, kohalikku malli ei leitud!</string>\n    <string name=\"settings_umount_modules_default_summary\">Globaalne vaikeväärtus \\\"Lahtihaagitud moodulitele\\\" rakenduseprofiilis. Lubamisel eemaldab see kõik moodulite süsteemimuudatused rakendustele, millel ei ole profiili määratud.</string>\n    <string name=\"enable_web_debugging_summary\">Saab kasutada WebUI silumiseks, palun luba ainult vajadusel.</string>\n    <string name=\"grant_root_failed\">Juurkasutaja andmine ebaõnnestus!</string>\n    <string name=\"settings_check_update\">Kontrolli uuendusi</string>\n    <string name=\"settings_check_update_summary\">Rakenduse avamisel kontrolli automaatselt uuendusi</string>\n    <string name=\"open\">Ava</string>\n    <string name=\"enable_web_debugging\">Luba WebView silumine</string>\n    <string name=\"save_log\">Salvesta Logid</string>\n    <string name=\"select_kmi\">Vali KMI</string>\n    <string name=\"select_file_tip\">%1$s partitsioonitõmmis on soovitatud</string>\n    <string name=\"install_next\">Edasi</string>\n    <string name=\"install_inactive_slot_warning\">Sinu seade **SUNNITAKSE** pärast taaskäivitust ebaaktiivsesse lahtrisse käivituma!\\nKasuta seda valikut vaid siis, kui tegid üle-õhu uuenduse.\\nJätkad?</string>\n    <string name=\"settings_uninstall\">Eemalda</string>\n    <string name=\"settings_uninstall_temporary_message\">Eemalda KernelSU ajutiselt, taasta pärast taaskäivitust algseisu.</string>\n    <string name=\"settings_uninstall_permanent_message\">KernelSU eemaldamine (juurkasutaja ja kõik moodulid) täielikult ja püsivalt.</string>\n    <string name=\"settings_restore_stock_image_message\">Taasta tehase-vaiketõmmis (kui varundus eksisteerib), tavaliselt kasutatakse enne üle-õhu uuendust; kui soovid KernelSU-d eemaldada, palun kasuta \\\"Eemalda püsivalt\\\".</string>\n    <string name=\"flashing\">Välgutamine</string>\n    <string name=\"flash_success\">Välgutamine õnnestus</string>\n    <string name=\"flash_failed\">Välgutamine ebaõnnestus</string>\n    <string name=\"selected_lkm\">Valitud LKM: %s</string>\n    <string name=\"direct_install\">Otsene paigaldus (soovitatud)</string>\n    <string name=\"select_file\">Vali fail</string>\n    <string name=\"install_inactive_slot\">Paigalda ebaaktiivsesse lahtrisse (pärast üle-õhu uuendust)</string>\n    <string name=\"settings_uninstall_temporary\">Eemalda ajutiselt</string>\n    <string name=\"settings_uninstall_permanent\">Eemalda püsivalt</string>\n    <string name=\"settings_restore_stock_image\">Taasta vaikimisi tõmmis</string>\n    <string name=\"settings_sucompat\">Suuna su binaarfail ümber</string>\n    <string name=\"settings_sucompat_summary\">Võimaldab rakendustel, kellele on antud Superuser luba rakenduse profiilis, saada superuser shell, käivitades /system/bin/su; kehtib ainult uute protsesside puhul.</string>\n    <string name=\"settings_kernel_umount\">Kernel umount</string>\n    <string name=\"settings_kernel_umount_summary\">Kerneli tasemel lahtihaakimise käitumine, mida kontrollib KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-fa/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">خانه</string>\n    <string name=\"home_not_installed\">نصب نشده است</string>\n    <string name=\"home_click_to_install\">برای نصب ضربه بزنید</string>\n    <string name=\"home_working\">به درستی کار می‌کند</string>\n    <string name=\"home_working_version\">نسخه: %d</string>\n    <string name=\"home_unsupported\">پشتیبانی نشده</string>\n    <string name=\"home_unsupported_reason\">کرنل اس یو فقط هسته های gki را پشتیبانی میکند</string>\n    <string name=\"home_kernel\">هسته</string>\n    <string name=\"home_manager_version\">نسخه برنامه</string>\n    <string name=\"home_fingerprint\">اثرانگشت</string>\n    <string name=\"home_selinux_status\">وضعیت SELinux</string>\n    <string name=\"selinux_status_disabled\">غیرفعال</string>\n    <string name=\"selinux_status_enforcing\">قانونمند</string>\n    <string name=\"selinux_status_permissive\">آزاد</string>\n    <string name=\"selinux_status_unknown\">ناشناخته</string>\n    <string name=\"superuser\">دسترسی روت</string>\n    <string name=\"module_failed_to_enable\">فعال کردن ماژول ناموفق بود: %s</string>\n    <string name=\"module_failed_to_disable\">غیرفعال کردن ماژول ناموفق بود: %s</string>\n    <string name=\"module_empty\">هیچ ماژولی نصب نشده است</string>\n    <string name=\"module\">ماژول</string>\n    <string name=\"uninstall\">لغو نصب</string>\n    <string name=\"module_install\">نصب</string>\n    <string name=\"install\">نصب</string>\n    <string name=\"reboot\">راه اندازی دوباره</string>\n    <string name=\"settings\">تنظیمات</string>\n    <string name=\"reboot_userspace\">راه اندازی نرم</string>\n    <string name=\"reboot_recovery\">راه اندازی به ریکاوری </string>\n    <string name=\"reboot_bootloader\">راه اندازی به بوتلودر</string>\n    <string name=\"reboot_download\">راه اندازی به حالت دانلود</string>\n    <string name=\"reboot_edl\">راه اندازی به EDL</string>\n    <string name=\"about\">درباره</string>\n    <string name=\"module_uninstall_confirm\">آیا مطمئنید که میخواهید ماژول %s را پاک کنید؟</string>\n    <string name=\"module_uninstall_success\">%s پاک شد</string>\n    <string name=\"module_uninstall_failed\">پاک کردن ناموفق بود: %s</string>\n    <string name=\"module_version\">نسخه</string>\n    <string name=\"module_author\">سازنده</string>\n    <string name=\"show_system_apps\">نمایش برنامه های سیستمی</string>\n    <string name=\"send_log\">ارسال وقایع</string>\n    <string name=\"safe_mode\">حالت امن</string>\n    <string name=\"reboot_to_apply\">راه‌اندازی مجدد برای تاثیرگذاری</string>\n    <string name=\"module_magisk_conflict\">مازول به دلیل تعارض با مجیسک غیرفعال شده اند\\'s!</string>\n    <string name=\"home_learn_kernelsu\">یادگیری کرنل اس یو</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید</string>\n    <string name=\"home_support_title\">از ما حمایت کنید</string>\n    <string name=\"home_support_content\">KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است.</string>\n    <string name=\"about_source_code\">\n<![CDATA[ View source code at %1$s<br/>Join our %2$s channel ]]>\n</string>\n    <string name=\"profile\">پروفایل برنامه</string>\n    <string name=\"profile_default\">پیش‌فرض</string>\n    <string name=\"profile_template\">قالب</string>\n    <string name=\"profile_custom\">شخصی سازی شده</string>\n    <string name=\"profile_name\">اسم پروفایل</string>\n    <string name=\"profile_namespace\">Mount namespace</string>\n    <string name=\"profile_namespace_inherited\">اثر گرفته</string>\n    <string name=\"profile_namespace_global\">گلوبال</string>\n    <string name=\"profile_namespace_individual\">تکی</string>\n    <string name=\"profile_umount_modules\">جداکردن ماژول ها</string>\n    <string name=\"save_log\">ذخیره گزارش‌ها</string>\n    <string name=\"settings_sucompat\">تغییر مسیر باینری su</string>\n    <string name=\"settings_sucompat_summary\">تغییر مسیر /system/bin/su به ksud برای برنامه‌هایی که مجوز Superuser در پروفایل برنامه دارند؛ فقط برای فرآیندهای جدید موثر است.</string>\n    <string name=\"settings_kernel_umount\">جداسازی هسته</string>\n    <string name=\"settings_kernel_umount_summary\">رفتار جداسازی در سطح هسته که توسط KernelSU کنترل می‌شود</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-fil/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_selinux_status\">Katayuan ng SELinux</string>\n    <string name=\"selinux_status_disabled\">Naka-disable</string>\n    <string name=\"selinux_status_enforcing\">Enforcing</string>\n    <string name=\"selinux_status_permissive\">Permissive</string>\n    <string name=\"home_not_installed\">Hindi naka-install</string>\n    <string name=\"home\">Panimula</string>\n    <string name=\"home_click_to_install\">I-click para i-install</string>\n    <string name=\"home_working\">Gumagana</string>\n    <string name=\"home_working_version\">Bersyon: %d</string>\n    <string name=\"selinux_status_unknown\">Hindi matukoy</string>\n    <string name=\"home_unsupported\">Hindi Suportado</string>\n    <string name=\"home_unsupported_reason\">Sinusuportahan lamang ng KernelSU ang mga GKI na kernel.</string>\n    <string name=\"module_failed_to_enable\">Nabigong paganahin ang module: %s</string>\n    <string name=\"module_failed_to_disable\">Nabigong i-disable ang module: %s</string>\n    <string name=\"module_empty\">Walang naka-install na modyul</string>\n    <string name=\"module\">Modyul</string>\n    <string name=\"module_install\">I-install</string>\n    <string name=\"install\">I-install</string>\n    <string name=\"reboot\">I-reboot</string>\n    <string name=\"reboot_userspace\">I-soft reboot</string>\n    <string name=\"reboot_download\">I-reboot sa Download</string>\n    <string name=\"reboot_edl\">I-reboot sa EDL</string>\n    <string name=\"about\">Tungkol</string>\n    <string name=\"module_uninstall_confirm\">Sigurado ka bang gusto mong i-uninstall ang module na %s?</string>\n    <string name=\"module_uninstall_success\">Na-uninstall ang %s</string>\n    <string name=\"module_uninstall_failed\">Nabigong i-uninstall: %s</string>\n    <string name=\"module_author\">May-akda</string>\n    <string name=\"show_system_apps\">Ipakita ang mga application ng system</string>\n    <string name=\"send_log\">Ipadala ang mga log</string>\n    <string name=\"reboot_to_apply\">I-reboot para umepekto</string>\n    <string name=\"module_magisk_conflict\">Hindi magagamit ang mga module dahil sa isang salungatan sa Magisk!</string>\n    <string name=\"home_learn_kernelsu\">Alamin ang KernelSU</string>\n    <string name=\"home_click_to_learn_kernelsu\">Matuto kung paano i-install ang KernelSU at gumamit ng mga module</string>\n    <string name=\"home_support_title\">Suportahan Kami</string>\n    <string name=\"home_support_content\">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.</string>\n    <string name=\"about_source_code\"><![CDATA[Tignan ang source code sa %1$s<br/>Sumali sa aming %2$s channel]]></string>\n    <string name=\"profile_namespace\">I-mount ang namespace</string>\n    <string name=\"profile_namespace_individual\">Indibidwal</string>\n    <string name=\"profile_groups\">Mga Grupo</string>\n    <string name=\"profile_capabilities\">Mga Kakayanan</string>\n    <string name=\"profile_selinux_context\">Konteksto ng SELinux</string>\n    <string name=\"profile_umount_modules\">I-unmount ang mga module</string>\n    <string name=\"failed_to_update_app_profile\">Nabigong i-update ang App Profile para sa %s</string>\n    <string name=\"require_kernel_version\">Ang kasalukuyang bersyon ng KernelSU %1$d ay masyadong mababa para gumana nang maayos ang manager. Mangyaring mag-upgrade sa bersyon %2$d o mas mataas!</string>\n    <string name=\"profile_umount_modules_summary\">Ang pag-enable sa opsyong ito ay magbibigay-daan sa KernelSU na ibalik ang anumang binagong file ng mga module para sa app na ito.</string>\n    <string name=\"profile_selinux_rules\">Mga Tuntunin</string>\n    <string name=\"module_downloading\">Nagda-download ng modyul: %s</string>\n    <string name=\"module_start_downloading\">Simulan ang pag-download: %s</string>\n    <string name=\"new_version_available\">Bagong bersyon: Available ang %s, i-click para mag-upgrade.</string>\n    <string name=\"launch_app\">Ilunsad</string>\n    <string name=\"force_stop_app\">Sapilitang itigil</string>\n    <string name=\"restart_app\">I-restart</string>\n    <string name=\"failed_to_update_sepolicy\">Nabigong i-update ang mga panuntunan ng SELinux para sa: %s</string>\n    <string name=\"home_manager_version\">Bersyon ng manager</string>\n    <string name=\"settings\">Mga setting</string>\n    <string name=\"reboot_recovery\">I-reboot sa Recovery</string>\n    <string name=\"reboot_bootloader\">I-reboot sa Bootloader</string>\n    <string name=\"module_version\">Bersyon</string>\n    <string name=\"uninstall\">I-uninstall</string>\n    <string name=\"profile_name\">Pangalan ng profile</string>\n    <string name=\"profile_namespace_inherited\">Minana</string>\n    <string name=\"settings_umount_modules_default_summary\">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.</string>\n    <string name=\"save_log\">I-save ang mga Log</string>\n    <string name=\"home_kernel\">Bersyon ng kernel</string>\n    <string name=\"home_fingerprint\">Fingerprint</string>\n    <string name=\"superuser\">Superuser</string>\n    <string name=\"module_install_prompt_with_name\">Ii-install ang mga sumusunod na module: %1$s</string>\n    <string name=\"module_sort_action_first\">Aksyon muna</string>\n    <string name=\"module_sort_enabled_first\">Pinagana muna</string>\n    <string name=\"confirm\">Kumpirmahin</string>\n    <string name=\"safe_mode\">Safe mode</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"profile_default\">Default</string>\n    <string name=\"profile_template\">Template</string>\n    <string name=\"profile_custom\">Pasadya</string>\n    <string name=\"profile_namespace_global\">Global</string>\n    <string name=\"settings_umount_modules_default\">I-unmount ang mga module bilang default</string>\n    <string name=\"profile_selinux_domain\">Domain</string>\n    <string name=\"module_update\">I-update</string>\n    <string name=\"su_not_allowed\">Hindi mabigay ang Superuser access sa %s</string>\n    <string name=\"module_changelog\">Mga pagbabago</string>\n    <string name=\"settings_profile_template\">Template ng App Profile</string>\n    <string name=\"settings_profile_template_summary\">Ipamahala ang lokal at online na template ng App Profile</string>\n    <string name=\"app_profile_template_create\">Gumawa ng template</string>\n    <string name=\"app_profile_template_edit\">I-edit ang template</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"app_profile_template_id_invalid\">Hindi wastong template ID</string>\n    <string name=\"app_profile_template_name\">Pangalan</string>\n    <string name=\"app_profile_template_description\">Paksa</string>\n    <string name=\"app_profile_template_save\">I-save</string>\n    <string name=\"app_profile_template_delete\">Burahin</string>\n    <string name=\"app_profile_template_view\">Tignan ang template</string>\n    <string name=\"app_profile_template_id_exist\">Umiiral na ang Template ID!</string>\n    <string name=\"app_profile_import_export\">I-import/I-export</string>\n    <string name=\"app_profile_import_from_clipboard\">Mag-import mula sa clipboard</string>\n    <string name=\"app_profile_export_to_clipboard\">I-export sa clipboard</string>\n    <string name=\"app_profile_template_export_empty\">Hindi makahanap ng lokal na template na ie-export!</string>\n    <string name=\"app_profile_template_import_success\">Matagumpay na na-import</string>\n    <string name=\"app_profile_template_save_failed\">Nabigong i-save ang template</string>\n    <string name=\"app_profile_template_import_empty\">Walang laman ang clipboard!</string>\n    <string name=\"settings_check_update\">Tumingin para sa mga update</string>\n    <string name=\"settings_check_update_summary\">Awtomatikong tumingin para sa mga update kapag binubuksan ang app</string>\n    <string name=\"grant_root_failed\">Nabigong ibigay ang root!</string>\n    <string name=\"action\">Aksyon</string>\n    <string name=\"open\">Buksan</string>\n    <string name=\"enable_web_debugging\">I-enable ang pag-debug ng WebView</string>\n    <string name=\"enable_web_debugging_summary\">Maaaring gamitin para i-debug ang WebUI. Mangyaring paganahin kung kinakailangan lang.</string>\n    <string name=\"direct_install\">Direktang pag-install (Inirerekomenda)</string>\n    <string name=\"select_file\">Pumili ng file</string>\n    <string name=\"install_inactive_slot\">I-install sa hindi aktibong slot (Pagkatapos ng OTA)</string>\n    <string name=\"install_inactive_slot_warning\">Ang iyong device ay **PIPILITIN** na i-boot sa kasalukuyang hindi aktibong slot pagkatapos ng reboot!\\nGamitin lamang ang opsyon na ito kung tapos na ang OTA.\\nMagpatuloy?</string>\n    <string name=\"install_next\">Susunod</string>\n    <string name=\"select_file_tip\">Inirerekomenda ang %1$s partition image</string>\n    <string name=\"select_kmi\">Pumili ng KMI</string>\n    <string name=\"settings_uninstall\">I-uninstall</string>\n    <string name=\"settings_uninstall_temporary\">Pansamantalang i-uninstall</string>\n    <string name=\"settings_uninstall_permanent\">Permanenteng i-uninstall</string>\n    <string name=\"settings_restore_stock_image\">Ibalik ang stock image</string>\n    <string name=\"settings_uninstall_temporary_message\">Pansamantalang i-uninstall ang KernelSU, ibabalik sa orihinal na kalagayan pagkatapos ng susunod na reboot.</string>\n    <string name=\"settings_uninstall_permanent_message\">Ina-uninstall ang KernelSU (root at lahat ng mga module) nang tuluyan at permanente.</string>\n    <string name=\"settings_restore_stock_image_message\">Ibalik ang stock factory image (kung may umiiral na backup), kadalasan na ginagamit bago ng OTA; kung kailangan mong i-uninstall ang KernelSU, mangyaring gamitin ang \\\"Permanenteng i-uninstall\\\".</string>\n    <string name=\"flashing\">Nagfa-flash</string>\n    <string name=\"flash_success\">Matagumpay ang pag-flash</string>\n    <string name=\"flash_failed\">Nabigo ang pag-flash</string>\n    <string name=\"selected_lkm\">Piniling LKM: %s</string>\n    <string name=\"log_saved\">Nai-save ang mga log</string>\n    <string name=\"settings_sucompat\">Klasikong su command</string>\n    <string name=\"settings_sucompat_summary\">Payagan ang root access gamit ang /system/bin/su, sa mga bagong proseso.</string>\n    <string name=\"module_repos\">Mga Repo</string>\n    <string name=\"module_repos_sort_name\">Pangalan (A → Z)</string>\n    <string name=\"module_repos_source_code\">Source code</string>\n    <string name=\"metamodule_uninstall_confirm\">Sigurado ka ba gusto mong i-uninstall ang module na %s? Aapektuhan ng aksyon na ito ang lahat ng mga module, at hindi na gagana ang mga feature na ibinigay ng metamodule (tulad ng pag-mount).</string>\n    <string name=\"safe_mode_module_disabled\">Naka-disable sa safe mode ang pag-install ng mga module</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-fr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_not_installed\">Non installé</string>\n    <string name=\"home_working\">Fonctionnel</string>\n    <string name=\"home_working_version\">Version : %d</string>\n    <string name=\"home_unsupported_reason\">KernelSU prend désormais en charge seulement les noyaux GKI, mais vous pouvez patcher l\\'image pour les appareils GKI.</string>\n    <string name=\"home_kernel\">Version du noyau</string>\n    <string name=\"home_fingerprint\">Empreinte</string>\n    <string name=\"home_selinux_status\">Statut SELinux</string>\n    <string name=\"selinux_status_disabled\">Désactivé</string>\n    <string name=\"selinux_status_permissive\">Permissif (permissive)</string>\n    <string name=\"selinux_status_unknown\">Inconnu</string>\n    <string name=\"superuser\">Super-utilisateur</string>\n    <string name=\"module_empty\">Aucun module installé</string>\n    <string name=\"home\">Accueil</string>\n    <string name=\"home_click_to_install\">Appuyez pour installer</string>\n    <string name=\"home_unsupported\">Non pris en charge</string>\n    <string name=\"module_uninstall_failed\">Échec de la désinstallation : %s</string>\n    <string name=\"module_version\">Version</string>\n    <string name=\"home_manager_version\">Version du gestionnaire</string>\n    <string name=\"selinux_status_enforcing\">Imposition (enforcing)</string>\n    <string name=\"module_failed_to_enable\">Échec de l\\'activation du module : %s</string>\n    <string name=\"module\">Module</string>\n    <string name=\"uninstall\">Désinstaller</string>\n    <string name=\"module_install\">Installer</string>\n    <string name=\"module_failed_to_disable\">Échec de la désactivation du module : %s</string>\n    <string name=\"reboot\">Redémarrer</string>\n    <string name=\"install\">Installer</string>\n    <string name=\"settings\">Paramètres</string>\n    <string name=\"reboot_bootloader\">Redémarrer vers le bootloader</string>\n    <string name=\"reboot_userspace\">Redémarrage logiciel</string>\n    <string name=\"reboot_recovery\">Redémarrer en mode Recovery</string>\n    <string name=\"reboot_edl\">Redémarrer en mode EDL</string>\n    <string name=\"about\">À propos</string>\n    <string name=\"module_uninstall_success\">%s désinstallé</string>\n    <string name=\"reboot_download\">Redémarrer en mode Download</string>\n    <string name=\"module_author\">Auteur</string>\n    <string name=\"module_uninstall_confirm\">Voulez-vous vraiment désinstaller le module %s ?</string>\n    <string name=\"home_learn_kernelsu\">Apprenez à utiliser KernelSU</string>\n    <string name=\"show_system_apps\">Afficher les applications système</string>\n    <string name=\"safe_mode\">Mode sans échec</string>\n    <string name=\"send_log\">Envoyer les journaux</string>\n    <string name=\"reboot_to_apply\">Redémarrez pour appliquer les modifications</string>\n    <string name=\"module_magisk_conflict\">Les modules sont indisponibles en raison d\\'un conflit avec Magisk !</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_support_title\">Soutenez-nous</string>\n    <string name=\"home_click_to_learn_kernelsu\">Découvrez comment installer KernelSU et utiliser les modules.</string>\n    <string name=\"home_support_content\">KernelSU est, et restera toujours, gratuit et open-source. Néanmoins, vous pouvez nous témoigner de votre soutien en nous faisant un don.</string>\n    <string name=\"about_source_code\"><![CDATA[Voir le code source à %1$s<br/>Rejoignez notre canal %2$s]]></string>\n    <string name=\"profile_template\">Modèle</string>\n    <string name=\"profile_default\">Par défaut</string>\n    <string name=\"profile_custom\">Personnalisé</string>\n    <string name=\"profile_name\">Nom du profil</string>\n    <string name=\"profile_namespace\">Espace de noms de montage</string>\n    <string name=\"profile_namespace_inherited\">Hérité</string>\n    <string name=\"profile_namespace_individual\">Individuel</string>\n    <string name=\"profile_selinux_context\">Contexte SELinux</string>\n    <string name=\"profile_namespace_global\">Global</string>\n    <string name=\"profile_groups\">Groupes</string>\n    <string name=\"profile_capabilities\">Capacités</string>\n    <string name=\"profile_umount_modules\">Démonter les modules</string>\n    <string name=\"failed_to_update_app_profile\">Échec de la modification du profil d\\'application pour %s</string>\n    <string name=\"profile_umount_modules_summary\">Activer cette option autorisera KernelSU à restaurer tous les fichiers de cette application qui ont été modifiés par les modules.</string>\n    <string name=\"settings_umount_modules_default\">Démonter par défaut les modules</string>\n    <string name=\"settings_umount_modules_default_summary\">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.</string>\n    <string name=\"profile_selinux_domain\">Domaine</string>\n    <string name=\"profile_selinux_rules\">Règles</string>\n    <string name=\"module_update\">Mettre à jour</string>\n    <string name=\"module_downloading\">Téléchargement du module : %s</string>\n    <string name=\"launch_app\">Lancer</string>\n    <string name=\"new_version_available\">La nouvelle version %s est disponible, appuyez pour mettre à jour !</string>\n    <string name=\"module_start_downloading\">Début du téléchargement de : %s</string>\n    <string name=\"force_stop_app\">Forcer l\\'arrêt</string>\n    <string name=\"restart_app\">Relancer l\\'application</string>\n    <string name=\"failed_to_update_sepolicy\">Échec de la mise à jour des règles SELinux pour %s</string>\n    <string name=\"require_kernel_version\">La version actuelle de KernelSU (%1$d) est trop ancienne pour que le gestionnaire puisse fonctionner correctement. Veuillez passer à la version %2$d ou ultérieure.</string>\n    <string name=\"app_profile_template_import_success\">Importation réussie</string>\n    <string name=\"app_profile_export_to_clipboard\">Exporter vers le presse-papiers</string>\n    <string name=\"app_profile_template_export_empty\">Impossible de trouver un modèle local à exporter !</string>\n    <string name=\"app_profile_template_id_exist\">L\\'ID du modèle existe déjà !</string>\n    <string name=\"module_changelog\">Historique</string>\n    <string name=\"app_profile_import_from_clipboard\">Importer à partir du presse-papiers</string>\n    <string name=\"app_profile_template_name\">Nom</string>\n    <string name=\"app_profile_template_id_invalid\">ID de modèle invalide</string>\n    <string name=\"app_profile_template_create\">Créez un modèle</string>\n    <string name=\"app_profile_import_export\">Importer/Exporter</string>\n    <string name=\"app_profile_template_save_failed\">Échec de l\\'enregistrement du modèle</string>\n    <string name=\"app_profile_template_edit\">Modifier le modèle</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"settings_profile_template\">Modèles de profils d\\'application</string>\n    <string name=\"app_profile_template_description\">Description</string>\n    <string name=\"app_profile_template_save\">Enregistrer</string>\n    <string name=\"settings_profile_template_summary\">Gérez les modèles de profils d\\'application locaux et en ligne.</string>\n    <string name=\"app_profile_template_delete\">Supprimer</string>\n    <string name=\"app_profile_template_import_empty\">Le presse-papiers est vide !</string>\n    <string name=\"app_profile_template_view\">Voir le modèle</string>\n    <string name=\"settings_check_update_summary\">Rechercher automatiquement des mises à jour à l\\'ouverture de l\\'application.</string>\n    <string name=\"settings_check_update\">Rechercher des mises à jour</string>\n    <string name=\"enable_web_debugging\">Débogage WebView</string>\n    <string name=\"enable_web_debugging_summary\">Peut être utilisé pour déboguer WebUI. Activez uniquement cette option si nécessaire.</string>\n    <string name=\"grant_root_failed\">Impossible d\\'accorder les privilèges root !</string>\n    <string name=\"open\">Ouvrir</string>\n    <string name=\"direct_install\">Installation directe (recommandé)</string>\n    <string name=\"select_file\">Sélectionner un fichier</string>\n    <string name=\"install_inactive_slot\">Installer dans l\\'emplacement inactif (après OTA)</string>\n    <string name=\"install_inactive_slot_warning\">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 ?</string>\n    <string name=\"install_next\">Suivant</string>\n    <string name=\"select_file_tip\">L\\'image de la partition %1$s est recommandée</string>\n    <string name=\"select_kmi\">Sélectionner une KMI</string>\n    <string name=\"settings_uninstall\">Désinstaller</string>\n    <string name=\"settings_uninstall_temporary\">Désinstaller temporairement</string>\n    <string name=\"settings_uninstall_permanent\">Désinstaller définitivement</string>\n    <string name=\"settings_restore_stock_image\">Restaurer l\\'image d\\'origine</string>\n    <string name=\"settings_restore_stock_image_message\">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\\\".</string>\n    <string name=\"flashing\">Flash en cours</string>\n    <string name=\"flash_success\">Flash réussi</string>\n    <string name=\"flash_failed\">Échec du flash</string>\n    <string name=\"selected_lkm\">LKM sélectionné : %s</string>\n    <string name=\"settings_uninstall_permanent_message\">Désinstallation complète et permanente de KernelSU (root et tous les modules).</string>\n    <string name=\"settings_uninstall_temporary_message\">Désinstaller temporairement KernelSU, et rétablir l\\'état original au redémarrage suivant.</string>\n    <string name=\"save_log\">Enregistrer les journaux</string>\n    <string name=\"module_sort_action_first\">Par action</string>\n    <string name=\"module_sort_enabled_first\">Par activation</string>\n    <string name=\"action\">Action</string>\n    <string name=\"log_saved\">Journaux enregistrés</string>\n    <string name=\"module_install_prompt_with_name\">Les modules suivants seront installés : %1$s</string>\n    <string name=\"su_not_allowed\">Impossible d\\'accorder les autorisations superutilisateur à %s</string>\n    <string name=\"confirm\">Confirmer</string>\n    <string name=\"install_upload_lkm_file\">Utiliser un fichier LKM local</string>\n    <string name=\"install_only_support_ko_file\">Seuls les fichiers .ko sont pris en charge</string>\n    <string name=\"processing\">Traitement…</string>\n    <string name=\"refresh_pulling\">Tirez pour actualiser</string>\n    <string name=\"refresh_release\">Relâchez pour actualiser</string>\n    <string name=\"refresh_refresh\">Actualisation…</string>\n    <string name=\"refresh_complete\">Actualisé avec succès</string>\n    <string name=\"settings_module_check_update\">Rechercher des mises à jour des modules</string>\n    <string name=\"settings_sucompat\">Commande SU classique</string>\n    <string name=\"settings_sucompat_summary\">Permettre l\\'accès root via /system/bin/su, dans les nouveaux processus.</string>\n    <string name=\"settings_kernel_umount\">Démonter les modules (niveau noyau)</string>\n    <string name=\"settings_kernel_umount_summary\">Démontez les modules noyau dans le profil d\\'application.</string>\n    <string name=\"install_select_partition\">Sélectionner une partition</string>\n    <string name=\"metamodule_uninstall_confirm\">Voulez-vous vraiment désinstaller le module %s ? Cette action affectera tous les modules, et certaines fonctionnalités fournies par le métamodule (telles que le montage) ne fonctionneront plus.</string>\n    <string name=\"app_profile_affects_following_apps\">Affecte les applis suivantes</string>\n    <string name=\"group_contains_apps\">Contient %d applis</string>\n    <string name=\"undo\">Annuler</string>\n    <string name=\"module_undo_uninstall_success\">Désinstallation de %s annulée avec succès</string>\n    <string name=\"module_undo_uninstall_failed\">Échec de l\\'annulation de la désinstallation : %s</string>\n    <string name=\"settings_theme\">Thème</string>\n    <string name=\"settings_theme_summary\">Choisissez le thème de l\\'application.</string>\n    <string name=\"settings_theme_mode_system\">Suivre le système</string>\n    <string name=\"settings_theme_mode_light\">Clair</string>\n    <string name=\"settings_theme_mode_dark\">Sombre</string>\n    <string name=\"settings_key_color\">Couleur clé</string>\n    <string name=\"settings_key_color_default\">Par défaut</string>\n    <string name=\"color_blue\">Bleu</string>\n    <string name=\"color_red\">Rouge</string>\n    <string name=\"color_green\">Vert</string>\n    <string name=\"color_purple\">Violet</string>\n    <string name=\"color_orange\">Orange</string>\n    <string name=\"color_teal\">Turquoise</string>\n    <string name=\"color_pink\">Rose</string>\n    <string name=\"color_brown\">Marron</string>\n    <string name=\"feature_status_unsupported_summary\">Le noyau ne prend pas en charge cette fonctionnalité</string>\n    <string name=\"feature_status_managed_summary\">Cette fonctionnalité est gérée par un module</string>\n    <string name=\"module_repos\">Dépôts</string>\n    <string name=\"module_repos_sort_name\">Nom (A → Z)</string>\n    <string name=\"module_repos_source_code\">Code source</string>\n    <string name=\"home_gki_warning\">À partir de la version 3.0.0, le mode de fonctionnement GKI sera utilisé uniquement dans les environnements de test. Nous vous le déconseillons pour une utilisation quotidienne, et les fichiers images ne seront plus fournis.</string>\n    <string name=\"network_offline\">Non connecté au réseau</string>\n    <string name=\"network_retry\">Réessayer</string>\n    <string name=\"tab_readme\">README</string>\n    <string name=\"tab_releases\">Versions</string>\n    <string name=\"tab_info\">Infos</string>\n    <string name=\"safe_mode_module_disabled\">L\\'installation de modules est désactivée en mode sans échec</string>\n    <string name=\"module_action_success\">Action de module exécutée avec succès.</string>\n    <string name=\"settings_mode_enable_by_default\">Activer (par défaut)</string>\n    <string name=\"settings_mode_disable_until_reboot\">Désactiver jusqu\\'au redémarrage</string>\n    <string name=\"settings_mode_disable_always\">Toujours désactiver</string>\n    <string name=\"module_shortcut_title\">Créer un raccourci</string>\n    <string name=\"module_shortcut_name_label\">Nom du raccourci</string>\n    <string name=\"module_shortcut_icon_pick\">Choisir une icône personnalisée</string>\n    <string name=\"module_shortcut_not_supported\">Le lanceur d\\'application ne prend pas en charge les raccourcis.</string>\n    <string name=\"module_shortcut_created\">Raccourci créé sur le bureau.</string>\n    <string name=\"module_shortcut_updated\">Raccourci mis à jour.</string>\n    <string name=\"module_shortcut_delete\">Supprimer le raccourci</string>\n    <string name=\"module_shortcut_permission_tip_xiaomi\">Activez pour cette application l\\'autorisation \\\"Créer des raccourcis de bureau\\\" dans les paramètres Xiaomi.</string>\n    <string name=\"module_shortcut_permission_tip_oppo\">Activez pour cette application l\\'autorisation \\\"Raccourci de bureau\\\" dans les paramètres OPPO.</string>\n    <string name=\"module_shortcut_permission_tip_default\">Si la création du raccourci échoue, veuillez activer pour cette application l\\'autorisation relative aux raccourcis de bureau dans les paramètres du système.</string>\n    <string name=\"no_such_module\">Le module %s n\\'existe pas</string>\n    <string name=\"module_unavailable\">Le module %s est désactivé, en cours de mise à jour, ou en attente de suppression</string>\n    <string name=\"select_file_tip_nogki\">Sélectionnez le fichier image d\\'appareil GKI que vous souhaitez patcher</string>\n    <string name=\"current_kmi\">Version KMI de cet appareil : %s</string>\n    <string name=\"current_device_kmi\">KMI de cet appareil</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-gl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Inicio</string>\n    <string name=\"settings_sucompat\">Redirixir o binario su</string>\n    <string name=\"settings_sucompat_summary\">Permite que as aplicacións con permiso de Superusuario no perfil da aplicación obteñan un shell de superusuario executando /system/bin/su; efectivo só para novos procesos.</string>\n    <string name=\"settings_kernel_umount\">Desmontar núcleo</string>\n    <string name=\"settings_kernel_umount_summary\">Comportamento de desmontaxe a nivel de núcleo controlado por KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-hi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"reboot_to_apply\">प्रभाव में होने के लिए रीबूट करें</string>\n    <string name=\"home_click_to_learn_kernelsu\">जानें कि KernelSU कैसे स्थापित करें और मॉड्यूल का उपयोग कैसे करें</string>\n    <string name=\"selinux_status_unknown\">अज्ञात</string>\n    <string name=\"show_system_apps\">सिस्टम एप्प दिखाए</string>\n    <string name=\"module_uninstall_success\">%s अनइंस्टॉल सफल हुआ</string>\n    <string name=\"profile_umount_modules\">मॉड्यूल्स अनमाउंट करें</string>\n    <string name=\"send_log\">लॉग भेजे</string>\n    <string name=\"selinux_status_disabled\">डिसेबल्ड (बंद)</string>\n    <string name=\"home_support_title\">हमें प्रोत्साहन दें</string>\n    <string name=\"profile_namespace_inherited\">Inherited</string>\n    <string name=\"module_magisk_conflict\">मॉड्यूल बंद कर दिए गए हैं क्योंकि यह मैजिक के साथ टकरा रहे है!</string>\n    <string name=\"module_changelog\">क्या बदलाव हुए है</string>\n    <string name=\"selinux_status_permissive\">पर्मिसिव</string>\n    <string name=\"reboot_download\">डाउनलोड में रिबूट करें</string>\n    <string name=\"settings_umount_modules_default\">डिफ़ॉल्ट रूप से मॉड्यूल अनमाउन्ट करें</string>\n    <string name=\"profile_umount_modules_summary\">इस विकल्प को चालू करने से KernelSU को इस एप्लिकेशन के लिए मॉड्यूल द्वारा किसी भी मोडिफाइड फ़ाइल को रिस्टोर करें।</string>\n    <string name=\"profile_namespace_individual\">Individual</string>\n    <string name=\"module_failed_to_enable\">%s मॉड्यूल चालू करने में विफल</string>\n    <string name=\"force_stop_app\">जबर्दस्ती बंद करें</string>\n    <string name=\"reboot_edl\">EDL मोड में रिबूट करें</string>\n    <string name=\"restart_app\">फिर से चालू करें</string>\n    <string name=\"profile_capabilities\">क्षमताएं</string>\n    <string name=\"module_start_downloading\">%s की डाउनलोडिंग स्टार्ट करें</string>\n    <string name=\"profile_namespace_global\">Global</string>\n    <string name=\"settings_umount_modules_default_summary\">ऐप प्रोफाइल में \\\"अनमाउंट मॉड्यूल\\\" के लिए ग्लोबल डिफ़ॉल्ट वैल्यू। यदि चालू किया गया है, तो यह एप्लीकेशंस के लिऐ सिस्टम के सभी मॉड्यूल मोडिफिकेशन को हटा देगा जिनकी प्रोफ़ाइल सेट नहीं है।</string>\n    <string name=\"selinux_status_enforcing\">एनफोर्सिंग</string>\n    <string name=\"profile_selinux_context\">SELinux context</string>\n    <string name=\"home_fingerprint\">फिंगरप्रिंट</string>\n    <string name=\"profile_default\">डिफॉल्ट</string>\n    <string name=\"launch_app\">लॉन्च करें</string>\n    <string name=\"safe_mode\">सेफ मोड</string>\n    <string name=\"require_kernel_version\">मैनेजर के ठीक से काम करने के लिए वर्तमान KernelSU वर्जन %1$d बहुत कम है। कृपया वर्जन %2$d या उच्चतर में अपग्रेड करें!</string>\n    <string name=\"reboot_recovery\">रिकवरी में रिबूट करें</string>\n    <string name=\"reboot_userspace\">सॉफ्ट रिबूट</string>\n    <string name=\"profile_name\">प्रोफाइल का नाम</string>\n    <string name=\"home_support_content\">KernelSU मुफ़्त और ओपन सोर्स और हमेशा रहेगा। हालाँकि आप दान देकर हमें दिखा सकते हैं कि आप संरक्षण करते हैं।</string>\n    <string name=\"uninstall\">अनइंस्टॉल करें</string>\n    <string name=\"profile_namespace\">Namspace माउंट करें</string>\n    <string name=\"module_install\">इंस्टाल करें</string>\n    <string name=\"home_click_to_install\">इंस्टाल करने के लिए क्लिक करें</string>\n    <string name=\"profile_selinux_rules\">नियम</string>\n    <string name=\"profile_groups\">समूह</string>\n    <string name=\"module\">मॉड्यूल</string>\n    <string name=\"module_author\">निर्माता</string>\n    <string name=\"about\">हमारे बारे में</string>\n    <string name=\"home_working_version\">वर्जन: %d</string>\n    <string name=\"reboot\">रीबूट करें</string>\n    <string name=\"home_unsupported_reason\">KernelSU अभी केवल GKI कर्नल्स को सपोर्ट करता है</string>\n    <string name=\"home_selinux_status\">SELinux स्थिति</string>\n    <string name=\"module_version\">वर्जन</string>\n    <string name=\"home_unsupported\">सपोर्ट नहीं करता है</string>\n    <string name=\"profile_selinux_domain\">डोमेन</string>\n    <string name=\"home\">होम</string>\n    <string name=\"profile_custom\">कस्टम</string>\n    <string name=\"profile_template\">टेम्पलेट</string>\n    <string name=\"module_downloading\">%s मॉड्यूल डाउनलोड हो रहा है</string>\n    <string name=\"module_update\">अपडेट</string>\n    <string name=\"home_learn_kernelsu\">KernelSU सीखें</string>\n    <string name=\"module_uninstall_confirm\">क्या आप सच में मॉड्यूल %s को अनइंस्टॉल करना चाहते हैं\\?</string>\n    <string name=\"module_uninstall_failed\">%s अनइंस्टल करने में असफल</string>\n    <string name=\"superuser\">सुपरयूजर</string>\n    <string name=\"settings\">सेटिंग</string>\n    <string name=\"home_working\">काम कर रहा है</string>\n    <string name=\"module_failed_to_disable\">%s मॉड्यूल बंद करने में विफल</string>\n    <string name=\"module_empty\">कोई मॉड्यूल इंस्टाल नहीं हुआ</string>\n    <string name=\"install\">इंस्टाल करें</string>\n    <string name=\"home_kernel\">कर्नल</string>\n    <string name=\"home_not_installed\">इंस्टाल नहीं हुआ</string>\n    <string name=\"failed_to_update_app_profile\">%s के लिए ऐप प्रोफ़ाइल अपडेट करने में विफल</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"failed_to_update_sepolicy\">%s के लिए SELinux नियमों को अपटेड करने में विफल</string>\n    <string name=\"reboot_bootloader\">बुटलोडर में रिबूट करें</string>\n    <string name=\"about_source_code\">%1$s पर स्रोत कोड देखें<br/>\nहमारे %2$s चैनल से जुड़ें</string>\n    <string name=\"home_manager_version\">मैनेजर वर्जन</string>\n    <string name=\"new_version_available\">नया वर्जन: %s उपलब्ध है,अपग्रेड के लिए क्लिक करें</string>\n    <string name=\"save_log\">लॉग सहेजें</string>\n    <string name=\"settings_sucompat\">su बाइनरी को फिर से रूट करें</string>\n    <string name=\"settings_sucompat_summary\">ऐप प्रोफ़ाइल में सुपरयूज़र अनुमति दिए गए ऐप्स को /system/bin/su को निष्पादित करके सुपरयूज़र शेल प्राप्त करने की अनुमति देता है; केवल नई प्रक्रियाओं के लिए प्रभावी।</string>\n    <string name=\"settings_kernel_umount\">कर्नेल अनमाउंट</string>\n    <string name=\"settings_kernel_umount_summary\">KernelSU द्वारा नियंत्रित कर्नेल-स्तरीय अनमाउंट व्यवहार</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-hr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"show_system_apps\">Prikažite sistemske aplikacije</string>\n    <string name=\"send_log\">Pošaljite izvještaje</string>\n    <string name=\"safe_mode\">Sigurnosni mod</string>\n    <string name=\"reboot_to_apply\">Ponovno pokrenite da bi proradilo</string>\n    <string name=\"failed_to_update_sepolicy\">Nije uspjelo ažuriranje SELinux pravila za %s</string>\n    <string name=\"home\">Početna</string>\n    <string name=\"home_not_installed\">Nije instalirano</string>\n    <string name=\"home_working_version\">Verzija: %d</string>\n    <string name=\"home_click_to_install\">Kliknite da instalirate</string>\n    <string name=\"home_working\">Radi</string>\n    <string name=\"home_unsupported\">Nepodržano</string>\n    <string name=\"home_unsupported_reason\">KernelSU sada samo podržava GKI kernele.</string>\n    <string name=\"home_kernel\">Verzija kernela</string>\n    <string name=\"home_manager_version\">Verzija voditelja</string>\n    <string name=\"home_fingerprint\">Otisak prsta</string>\n    <string name=\"selinux_status_disabled\">Isključeno</string>\n    <string name=\"selinux_status_enforcing\">U Provođenju</string>\n    <string name=\"selinux_status_permissive\">Permisivno</string>\n    <string name=\"home_selinux_status\">SELinux stanje</string>\n    <string name=\"selinux_status_unknown\">Nepoznato</string>\n    <string name=\"superuser\">Superkorisnik</string>\n    <string name=\"module_failed_to_enable\">Neuspješno uključivanje module: %s</string>\n    <string name=\"module_failed_to_disable\">Neuspješno isključivanje module: %s</string>\n    <string name=\"module_empty\">Nema instaliranih modula</string>\n    <string name=\"module\">Br. modula</string>\n    <string name=\"uninstall\">Deinstalirajte</string>\n    <string name=\"module_install\">Instalirajte</string>\n    <string name=\"install\">Instalirajte</string>\n    <string name=\"reboot\">Ponovno pokrenite</string>\n    <string name=\"settings\">Postavke</string>\n    <string name=\"reboot_userspace\">Lagano ponovno pokretanje</string>\n    <string name=\"reboot_recovery\">Ponovno pokrenite u Oporavu</string>\n    <string name=\"reboot_bootloader\">Ponovno pokrenite u Pogonski Učitavalac</string>\n    <string name=\"reboot_download\">Ponovno pokrenite u Preuzimanje</string>\n    <string name=\"reboot_edl\">Ponovo pokrenite u EDL</string>\n    <string name=\"about\">O</string>\n    <string name=\"module_uninstall_confirm\">Jeste li sigurni da želite deinstalirati modulu %s\\?</string>\n    <string name=\"module_uninstall_success\">%s deinstalirana</string>\n    <string name=\"module_uninstall_failed\">Neuspješna deinstalacija: %s</string>\n    <string name=\"module_version\">Verzija</string>\n    <string name=\"module_author\">Autor</string>\n    <string name=\"module_magisk_conflict\">Module su isključene jer je u sukobu sa Magisk-om!</string>\n    <string name=\"home_learn_kernelsu\">Naučite KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Naučite kako da instalirate KernelSU i da koristite module.</string>\n    <string name=\"home_support_title\">Podržite Nas</string>\n    <string name=\"home_support_content\">KernelSU je i uvijek će biti besplatan i otvorenog koda. Međutim, možete nam pokazati da vam je stalo donacijom.</string>\n    <string name=\"about_source_code\"><![CDATA[Pogledajte izvor na %1$s<br/>Pridružite se našem %2$s kanalu]]></string>\n    <string name=\"profile_default\">Zadano</string>\n    <string name=\"profile_template\">Šablon</string>\n    <string name=\"profile_custom\">Prilagođeno</string>\n    <string name=\"profile_name\">Naziv profila</string>\n    <string name=\"profile_namespace_inherited\">Naslijeđen</string>\n    <string name=\"profile_namespace\">Imenski prostor nosača</string>\n    <string name=\"failed_to_update_app_profile\">Ažuriranje Profila Aplikacije za %s nije uspjelo</string>\n    <string name=\"profile_namespace_global\">Globalan</string>\n    <string name=\"profile_namespace_individual\">Pojedinačan</string>\n    <string name=\"profile_umount_modules\">Umount module</string>\n    <string name=\"profile_groups\">Grupe</string>\n    <string name=\"profile_capabilities\">Sposobnosti</string>\n    <string name=\"profile_selinux_context\">SELinux kontekst</string>\n    <string name=\"require_kernel_version\">Trenutna verzija KernelSU-a %1$d je preniska da bi upravitelj ispravno radio. Molimo vas da nadogradite na verziju %2$d ili noviju!</string>\n    <string name=\"settings_umount_modules_default\">Umontiraj module prema zadanim postavkama</string>\n    <string name=\"settings_umount_modules_default_summary\">Globalna zadana vrijednost za \\\"Umontiraj module\\\" u profilu aplikacije. Ako je omogućeno, uklonit će sve modifikacije modula sustava za aplikacije koje nemaju postavljen profil.</string>\n    <string name=\"profile_selinux_domain\">Domena</string>\n    <string name=\"profile_umount_modules_summary\">Omogućavanje ove opcije omogućit će KernelSU-u da vrati sve izmijenjene datoteke od strane modula za ovu aplikaciju.</string>\n    <string name=\"profile_selinux_rules\">Pravila</string>\n    <string name=\"module_update\">Ažuriranje</string>\n    <string name=\"module_downloading\">Preuzimanje module: %s</string>\n    <string name=\"module_start_downloading\">Započnite sa preuzimanjem: %s</string>\n    <string name=\"new_version_available\">Nova verzija %s je dostupna, kliknite za nadogradnju!</string>\n    <string name=\"launch_app\">Pokrenite</string>\n    <string name=\"force_stop_app\">Prisilno zaustavi</string>\n    <string name=\"restart_app\">Resetujte</string>\n    <string name=\"save_log\">Spremi zapise</string>\n    <string name=\"module_install_prompt_with_name\">Sljedeći moduli će biti instalirani: %1$s</string>\n    <string name=\"module_sort_action_first\">Prvo radnja</string>\n    <string name=\"module_sort_enabled_first\">Prvo omogućeno</string>\n    <string name=\"confirm\">Potvrdi</string>\n    <string name=\"settings_profile_template\">Predložak profila aplikacije</string>\n    <string name=\"settings_profile_template_summary\">Upravljanje lokalnim i online predloškom profila aplikacije.</string>\n    <string name=\"app_profile_template_create\">Izradi predložak</string>\n    <string name=\"app_profile_template_edit\">Uredi predložak</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"app_profile_template_id_invalid\">Nevažeći ID predloška</string>\n    <string name=\"app_profile_template_name\">Naziv</string>\n    <string name=\"app_profile_template_description\">Opis</string>\n    <string name=\"app_profile_template_save\">Spremi</string>\n    <string name=\"app_profile_template_delete\">Izbriši</string>\n    <string name=\"app_profile_template_view\">Prikaži predložak</string>\n    <string name=\"app_profile_template_id_exist\">ID predloška već postoji!</string>\n    <string name=\"app_profile_import_export\">Uvoz/Izvoz</string>\n    <string name=\"app_profile_import_from_clipboard\">Uvezi iz međuspremnika</string>\n    <string name=\"app_profile_export_to_clipboard\">Izvezi u međuspremnik</string>\n    <string name=\"app_profile_template_export_empty\">Nije moguće pronaći lokalni predložak za izvoz!</string>\n    <string name=\"app_profile_template_import_success\">Uspješno uvezeno</string>\n    <string name=\"app_profile_template_save_failed\">Spremanje predloška nije uspjelo</string>\n    <string name=\"app_profile_template_import_empty\">Međuspremnik je prazan!</string>\n    <string name=\"settings_check_update\">Provjeri za ažuriranja</string>\n    <string name=\"settings_check_update_summary\">Automatski provjeri za ažuriranja prilikom otvaranja aplikacije.</string>\n    <string name=\"grant_root_failed\">Dodjeljivanje root pristupa nije uspjelo!</string>\n    <string name=\"action\">Radnja</string>\n    <string name=\"open\">Otvori</string>\n    <string name=\"enable_web_debugging\">WebView otklanjanje pogrešaka</string>\n    <string name=\"enable_web_debugging_summary\">Može se koristiti za otklanjanje pogrešaka u WebUI-ju. Omogućite samo kada je potrebno.</string>\n    <string name=\"direct_install\">Izravna instalacija (preporučeno)</string>\n    <string name=\"select_file\">Odaberite datoteku</string>\n    <string name=\"install_inactive_slot\">Instaliraj u neaktivni utor (nakon OTA)</string>\n    <string name=\"install_inactive_slot_warning\">Vaš će uređaj biti **PRISILNO** pokrenuti se u trenutno neaktivni utor nakon ponovnog pokretanja!\\nKoristite ovu opciju tek nakon što je OTA završen.\\nNastaviti?</string>\n    <string name=\"install_next\">Dalje</string>\n    <string name=\"install_upload_lkm_file\">Koristi lokalnu LKM datoteku</string>\n    <string name=\"install_only_support_ko_file\">Podržane su samo .ko datoteke</string>\n    <string name=\"select_file_tip\">Preporučuje se particija %1$s</string>\n    <string name=\"select_kmi\">Odaberi KMI</string>\n    <string name=\"settings_uninstall\">Deinstaliraj</string>\n    <string name=\"settings_uninstall_temporary\">Privremeno deinstaliraj</string>\n    <string name=\"settings_uninstall_permanent\">Trajno deinstaliraj</string>\n    <string name=\"settings_restore_stock_image\">Vrati stock image</string>\n    <string name=\"settings_uninstall_temporary_message\">Privremeno deinstaliraj KernelSU, vrati u izvorno stanje nakon sljedećeg ponovnog pokretanja.</string>\n    <string name=\"settings_uninstall_permanent_message\">Potpuno i trajno deinstaliranje KernelSU-a (root i svi moduli).</string>\n    <string name=\"settings_restore_stock_image_message\">Vrati stock factory image (ako postoji sigurnosna kopija), obično se koristi prije OTA-e; ako trebate deinstalirati KernelSU, koristite \\\"Trajno deinstaliraj\\\".</string>\n    <string name=\"flashing\">Flešanje</string>\n    <string name=\"flash_success\">Uspješno flešano</string>\n    <string name=\"flash_failed\">Neuspješno flešanje</string>\n    <string name=\"selected_lkm\">Odabrani LKM: %s</string>\n    <string name=\"log_saved\">Zapisi spremljeni</string>\n    <string name=\"processing\">Obrada…</string>\n    <string name=\"refresh_pulling\">Povuci dolje za osvježiti</string>\n    <string name=\"refresh_release\">Otpusti za osvježiti</string>\n    <string name=\"refresh_refresh\">Osvježavanje…</string>\n    <string name=\"refresh_complete\">Uspješno osvježeno</string>\n    <string name=\"su_not_allowed\">Nije moguće odobriti Superuser pristup za %s</string>\n    <string name=\"module_changelog\">Zapis promjena</string>\n    <string name=\"metamodule_uninstall_confirm\">Jeste li sigurni da želite deinstalirati modul %s? Ova radnja će utjecati na sve module, a određene značajke koje pruža metamodul (poput montiranja) više neće raditi.</string>\n    <string name=\"app_profile_affects_following_apps\">Utječe na sljedeće aplikacije</string>\n    <string name=\"settings_module_check_update\">Provjerite ažuriranja modula</string>\n    <string name=\"install_select_partition\">Odaberite particiju</string>\n    <string name=\"feature_status_unsupported_summary\">Kernel ne podržava ovu značajku.</string>\n    <string name=\"feature_status_managed_summary\">Ovom značajkom upravlja modul.</string>\n    <string name=\"settings_sucompat\">Preusmjeri su binarnu datoteku</string>\n    <string name=\"settings_sucompat_summary\">Omogućava aplikacijama kojima je dodijeljeno dopuštenje Superuser u Profilu aplikacije da dobiju superuser shell izvršavanjem /system/bin/su; učinkovito samo za nove procese.</string>\n    <string name=\"settings_kernel_umount\">Kernel umount</string>\n    <string name=\"settings_kernel_umount_summary\">Ponašanje umount na razini kernela koje kontrolira KernelSU</string>\n    <string name=\"undo\">Poništi</string>\n    <string name=\"module_undo_uninstall_success\">Deinstalacija %s uspješno otkazana</string>\n    <string name=\"module_undo_uninstall_failed\">Poništavanje deinstalacije nije uspjelo: %s</string>\n    <string name=\"group_contains_apps\">Sadrži %d aplikacija</string>\n    <string name=\"settings_theme\">Tema</string>\n    <string name=\"settings_theme_summary\">Odaberite temu aplikacije.</string>\n    <string name=\"settings_theme_mode_system\">Prati sustav</string>\n    <string name=\"settings_theme_mode_light\">Svijetla</string>\n    <string name=\"settings_theme_mode_dark\">Tamna</string>\n    <string name=\"settings_key_color\">Boja ključa</string>\n    <string name=\"settings_key_color_default\">Zadano</string>\n    <string name=\"color_blue\">Plava</string>\n    <string name=\"color_red\">Crvena</string>\n    <string name=\"color_green\">Zelena</string>\n    <string name=\"color_purple\">Ljubičasta</string>\n    <string name=\"color_orange\">Narančasta</string>\n    <string name=\"color_teal\">Tirkizna</string>\n    <string name=\"color_pink\">Ružičasta</string>\n    <string name=\"color_brown\">Smeđa</string>\n    <string name=\"module_repos\">Repozitoriji</string>\n    <string name=\"module_repos_sort_name\">Naziv (A → Z)</string>\n    <string name=\"module_repos_source_code\">Izvorni kod</string>\n    <string name=\"safe_mode_module_disabled\">Instalacija modula je onemogućena u sigurnom načinu rada</string>\n    <string name=\"home_gki_warning\">Počevši od verzije 3.0.0, GKI način rada koristit će se samo u testnim okruženjima. Ne preporučujemo ga za svakodnevnu upotrebu, a image datoteke više neće biti dostupne.</string>\n    <string name=\"network_offline\">Nije povezano s mrežom</string>\n    <string name=\"network_retry\">Pokušaj ponovno</string>\n    <string name=\"tab_readme\">PROČITAJ ME</string>\n    <string name=\"tab_releases\">Izdanja</string>\n    <string name=\"tab_info\">Info</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-hu/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_working\">Működik</string>\n    <string name=\"home_working_version\">Verzió: %d</string>\n    <string name=\"home_unsupported_reason\">A KernelSU jelenleg csak GKI kerneleket támogat</string>\n    <string name=\"home_kernel\">Kernel</string>\n    <string name=\"home_manager_version\">Alkalmazás verziója</string>\n    <string name=\"home_fingerprint\">Ujjlenyomat</string>\n    <string name=\"selinux_status_disabled\">Letiltva</string>\n    <string name=\"reboot_download\">Újraindítás letöltő módba</string>\n    <string name=\"reboot_edl\">Újraindítás EDL-be</string>\n    <string name=\"about\">Névjegy</string>\n    <string name=\"module_uninstall_confirm\">Biztos benne hogy eltávolítja a következő modult: %s?</string>\n    <string name=\"module_uninstall_failed\">Nem sikerült eltávolítani: %s</string>\n    <string name=\"module_author\">Készítő</string>\n    <string name=\"show_system_apps\">Rendszeralkalmazások megjelenítése</string>\n    <string name=\"safe_mode\">Biztonságos mód</string>\n    <string name=\"module_magisk_conflict\">A modulok nem érhetők el a Magiskkel való ütközés miatt!</string>\n    <string name=\"home_learn_kernelsu\">Tudjon meg többet a KernelSU-ról</string>\n    <string name=\"home_click_to_learn_kernelsu\">Ismerje meg a KernelSU telepítését és a modulok használatát</string>\n    <string name=\"home_support_title\">Támogasson minket</string>\n    <string name=\"about_source_code\">Tekintse meg a forráskódot a %1$s-on<br/>\nCsatlakozzon a %2$s csatornánkhoz</string>\n    <string name=\"profile_default\">Alapértelmezett</string>\n    <string name=\"profile_template\">Sablon</string>\n    <string name=\"profile_custom\">Egyedi</string>\n    <string name=\"profile_name\">Profil neve</string>\n    <string name=\"profile_namespace\">Névtér csatlakoztatása</string>\n    <string name=\"profile_namespace_inherited\">Örökölt</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"profile_namespace_individual\">Különálló</string>\n    <string name=\"profile_groups\">Csoportok</string>\n    <string name=\"profile_capabilities\">Jogosultságok</string>\n    <string name=\"profile_selinux_context\">SELinux kontextus</string>\n    <string name=\"settings_umount_modules_default\">Modulok leválasztása alapértelmezetten</string>\n    <string name=\"profile_umount_modules_summary\">Ha engedélyezi ezt az opciót, a KernelSU visszaállíthatja az alkalmazás moduljai által módosított fájlokat.</string>\n    <string name=\"profile_selinux_domain\">Tartomány</string>\n    <string name=\"profile_selinux_rules\">Szabályok</string>\n    <string name=\"module_update\">Frissítés</string>\n    <string name=\"module_downloading\">Modul letöltése: %s</string>\n    <string name=\"module_start_downloading\">Letöltés indítása: %s</string>\n    <string name=\"launch_app\">Indítás</string>\n    <string name=\"force_stop_app\">Kényszerített leállítás</string>\n    <string name=\"restart_app\">újraindítás</string>\n    <string name=\"home\">Kezdőlap</string>\n    <string name=\"home_not_installed\">Nincs telepítve</string>\n    <string name=\"home_click_to_install\">Kattintson a telepítéshez</string>\n    <string name=\"home_unsupported\">Nem támogatott</string>\n    <string name=\"home_selinux_status\">SELinux állapot</string>\n    <string name=\"selinux_status_enforcing\">Kényszerített</string>\n    <string name=\"selinux_status_permissive\">Engedélyezett</string>\n    <string name=\"selinux_status_unknown\">Ismeretlen</string>\n    <string name=\"superuser\">Superuser</string>\n    <string name=\"module_failed_to_enable\">Nem sikerült engedélyezni a következő modult: %s</string>\n    <string name=\"module_failed_to_disable\">Nem sikerült letiltani a következő modult: %s</string>\n    <string name=\"module_empty\">Nincs telepített modul</string>\n    <string name=\"module\">Modulok</string>\n    <string name=\"uninstall\">Eltávolítás</string>\n    <string name=\"module_install\">Telepítés</string>\n    <string name=\"install\">Telepítés</string>\n    <string name=\"reboot\">Újraindítás</string>\n    <string name=\"settings\">Beállítások</string>\n    <string name=\"reboot_userspace\">Rendszerfelület újraindítása</string>\n    <string name=\"reboot_recovery\">Újraindítás recovery-módba</string>\n    <string name=\"reboot_bootloader\">Újraindítás bootloader-módba</string>\n    <string name=\"module_uninstall_success\">%s eltávolítva</string>\n    <string name=\"module_version\">Verzió</string>\n    <string name=\"send_log\">Naplók küldése</string>\n    <string name=\"reboot_to_apply\">Indítsa újra a készüléket a változások érvényesítéséhez</string>\n    <string name=\"home_support_content\">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.</string>\n    <string name=\"profile_namespace_global\">Globális</string>\n    <string name=\"profile_umount_modules\">Modulok leválasztása</string>\n    <string name=\"failed_to_update_app_profile\">Nem sikerült frissíteni az App Profilt ehhez: %s</string>\n    <string name=\"settings_umount_modules_default_summary\">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.</string>\n    <string name=\"new_version_available\">Elérhető az új, %s verzió, kattintson a frissítéshez.</string>\n    <string name=\"failed_to_update_sepolicy\">Nem sikerült frissíteni az SELinux szabályokat a következőhöz: %s</string>\n    <string name=\"require_kernel_version\">A jelenlegi KernelSU verzió %1$d túlságosan elavult a megfelelő működéshez. Kérjük frissítsen a %2$d verzióra vagy újabbra!</string>\n    <string name=\"app_profile_template_import_success\">Sikeresen importálva</string>\n    <string name=\"app_profile_export_to_clipboard\">Exportálás a vágólapról</string>\n    <string name=\"app_profile_template_export_empty\">Nem található helyi sablon az exportáláshoz!</string>\n    <string name=\"app_profile_template_id_exist\">A sablon ID már létezik!</string>\n    <string name=\"module_changelog\">Változások</string>\n    <string name=\"app_profile_import_from_clipboard\">Importálás a vágólapról</string>\n    <string name=\"app_profile_template_name\">Név</string>\n    <string name=\"app_profile_template_id_invalid\">Hibás sablon ID</string>\n    <string name=\"app_profile_template_create\">Sablon készítése</string>\n    <string name=\"app_profile_import_export\">Import/Export</string>\n    <string name=\"app_profile_template_save_failed\">A sablon mentése sikertelen</string>\n    <string name=\"app_profile_template_edit\">Sablon szerkesztése</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"settings_profile_template\">App Profile sablon</string>\n    <string name=\"app_profile_template_description\">Leírás</string>\n    <string name=\"app_profile_template_save\">Mentés</string>\n    <string name=\"settings_profile_template_summary\">Az App Profile helyi és online sablonjának kezelése</string>\n    <string name=\"app_profile_template_delete\">Törlés</string>\n    <string name=\"app_profile_template_import_empty\">A vágólap üres!</string>\n    <string name=\"app_profile_template_view\">Sablon megtekintése</string>\n    <string name=\"save_log\">Naplók mentése</string>\n    <string name=\"enable_web_debugging_summary\">A WebUI hibakeresésére használható, csak szükség esetén engedélyezze.</string>\n    <string name=\"enable_web_debugging\">WebView hibakeresés engedélyezése</string>\n    <string name=\"open\">Megnyitás</string>\n    <string name=\"settings_uninstall_permanent\">Végleges eltávolítás</string>\n    <string name=\"select_file_tip\">%1$s partíció képfájl ajánlott</string>\n    <string name=\"select_kmi\">KMI kiválasztása</string>\n    <string name=\"install_next\">Következő</string>\n    <string name=\"settings_uninstall_temporary\">Ideiglenes eltávolítás</string>\n    <string name=\"settings_uninstall_temporary_message\">A KernelSU ideiglenes eltávolítása, az eredeti állapot visszaállítása a következő újraindítás után.</string>\n    <string name=\"settings_uninstall\">Eltávolítás</string>\n    <string name=\"flashing\">Telepítés</string>\n    <string name=\"flash_success\">Sikeres telepítés</string>\n    <string name=\"selected_lkm\">Kiválasztott LKM: %s</string>\n    <string name=\"flash_failed\">Sikertelen telepítés</string>\n    <string name=\"grant_root_failed\">A root jog megadása sikertelen!</string>\n    <string name=\"install_inactive_slot\">Telepítés inaktív helyre (OTA után)</string>\n    <string name=\"select_file\">Fájl kiválasztása</string>\n    <string name=\"settings_uninstall_permanent_message\">A KernelSU eltávolítása (root és az összes modul) teljesen és véglegesen.</string>\n    <string name=\"settings_restore_stock_image\">Eredeti képfájl visszaállítása</string>\n    <string name=\"action\">Művelet</string>\n    <string name=\"direct_install\">Közvetlen telepítés (Ajánlott)</string>\n    <string name=\"install_inactive_slot_warning\">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?</string>\n    <string name=\"settings_restore_stock_image_message\">Á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.</string>\n    <string name=\"settings_check_update\">Frissítés ellenőrzése</string>\n    <string name=\"settings_check_update_summary\">Automatikusan keressen frissítéseket az alkalmazás megnyitásakor</string>\n    <string name=\"log_saved\">Mentett naplók</string>\n    <string name=\"settings_sucompat\">su bináris átirányítása</string>\n    <string name=\"settings_sucompat_summary\">Lehetővé teszi az App Profile-ban Superuser engedéllyel rendelkező alkalmazások számára, hogy superuser shell-t kapjanak a /system/bin/su végrehajtásával; csak új folyamatoknál hatékony.</string>\n    <string name=\"settings_kernel_umount\">Kernel leválasztása</string>\n    <string name=\"settings_kernel_umount_summary\">KernelSU által vezérelt kernel szintű leválasztási viselkedés</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-in/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Beranda</string>\n    <string name=\"home_not_installed\">Tidak terpasang</string>\n    <string name=\"home_click_to_install\">Ketuk untuk memasang</string>\n    <string name=\"home_working\">Berfungsi</string>\n    <string name=\"home_working_version\">Versi: %d</string>\n    <string name=\"home_unsupported\">Tidak didukung</string>\n    <string name=\"home_unsupported_reason\">KernelSU sekarang hanya mendukung kernel GKI, tetapi Anda dapat menambal image untuk perangkat GKI.</string>\n    <string name=\"home_kernel\">Versi kernel</string>\n    <string name=\"home_manager_version\">Versi manajer</string>\n    <string name=\"home_fingerprint\">Identitas</string>\n    <string name=\"home_selinux_status\">Status SELinux</string>\n    <string name=\"selinux_status_disabled\">Dinonaktifkan</string>\n    <string name=\"selinux_status_enforcing\">Enforcing</string>\n    <string name=\"selinux_status_permissive\">Permisif</string>\n    <string name=\"selinux_status_unknown\">Tidak diketahui</string>\n    <string name=\"superuser\">Superuser</string>\n    <string name=\"module_failed_to_enable\">Gagal mengaktifkan modul: %s</string>\n    <string name=\"module_failed_to_disable\">Gagal menonaktifkan modul: %s</string>\n    <string name=\"module_empty\">Tidak ada modul yang terpasang</string>\n    <string name=\"module\">Modul</string>\n    <string name=\"uninstall\">Hapus</string>\n    <string name=\"module_install\">Pasang</string>\n    <string name=\"install\">Pasang</string>\n    <string name=\"reboot\">Reboot</string>\n    <string name=\"settings\">Setelan</string>\n    <string name=\"reboot_userspace\">Reboot secara halus</string>\n    <string name=\"reboot_recovery\">Reboot ke Recovery</string>\n    <string name=\"reboot_bootloader\">Reboot ke Bootloader</string>\n    <string name=\"reboot_download\">Reboot ke Download</string>\n    <string name=\"reboot_edl\">Reboot ke EDL</string>\n    <string name=\"about\">Tentang</string>\n    <string name=\"module_uninstall_confirm\">Yakin ingin menghapus modul %s?</string>\n    <string name=\"module_uninstall_success\">%s berhasil dihapus</string>\n    <string name=\"module_uninstall_failed\">Gagal menghapus: %s</string>\n    <string name=\"module_version\">Versi</string>\n    <string name=\"module_author\">Oleh</string>\n    <string name=\"show_system_apps\">Tampilkan apl sistem</string>\n    <string name=\"send_log\">Kirim Log</string>\n    <string name=\"safe_mode\">Mode aman</string>\n    <string name=\"reboot_to_apply\">Reboot agar berfungsi</string>\n    <string name=\"module_magisk_conflict\">Modul tidak tersedia karena konflik dengan Magisk!</string>\n    <string name=\"home_learn_kernelsu\">Pelajari KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/id_ID/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Pelajari cara memasang KernelSU dan menggunakan modul.</string>\n    <string name=\"home_support_title\">Dukung Kami</string>\n    <string name=\"home_support_content\">KernelSU akan selalu menjadi aplikasi gratis dan terbuka. Anda dapat memberikan donasi sebagai bentuk dukungan.</string>\n    <string name=\"about_source_code\"><![CDATA[Lihat sumber kode di %1$s<br/>Gabung saluran %2$s kami]]></string>\n    <string name=\"profile\">Profil Apl</string>\n    <string name=\"profile_default\">Default</string>\n    <string name=\"profile_template\">Templat</string>\n    <string name=\"profile_custom\">Khusus</string>\n    <string name=\"profile_name\">Nama profil</string>\n    <string name=\"profile_namespace\">Mount namespace</string>\n    <string name=\"profile_namespace_inherited\">Diwariskan</string>\n    <string name=\"profile_namespace_global\">Universal</string>\n    <string name=\"profile_namespace_individual\">Individual</string>\n    <string name=\"profile_groups\">Kelompok</string>\n    <string name=\"profile_capabilities\">Kemampuan</string>\n    <string name=\"profile_selinux_context\">Konteks SELinux</string>\n    <string name=\"profile_umount_modules\">Umount modul</string>\n    <string name=\"failed_to_update_app_profile\">Gagal memperbarui Profil Apl untuk %s</string>\n    <string name=\"settings_umount_modules_default\">Umount modul secara default</string>\n    <string name=\"settings_umount_modules_default_summary\">Nilai default universal untuk \\\"Umount modul\\\" pada Profil Aplikasi. Jika diaktifkan, akan menghapus semua modifikasi sistem untuk aplikasi yang tidak memiliki set profil.</string>\n    <string name=\"profile_umount_modules_summary\">Aktifkan opsi ini agar KernelSU dapat memulihkan kembali berkas termodifikasi oleh modul pada aplikasi ini.</string>\n    <string name=\"profile_selinux_domain\">Domain</string>\n    <string name=\"profile_selinux_rules\">Aturan</string>\n    <string name=\"module_update\">Perbarui</string>\n    <string name=\"module_downloading\">Mengunduh modul: %s</string>\n    <string name=\"module_start_downloading\">Mulai mengunduh: %s</string>\n    <string name=\"new_version_available\">Versi baru %s tersedia, klik untuk memperbarui!</string>\n    <string name=\"launch_app\">Luncurkan</string>\n    <string name=\"force_stop_app\">Paksa berhenti</string>\n    <string name=\"restart_app\">Mulai ulang apl</string>\n    <string name=\"failed_to_update_sepolicy\">Gagal memperbarui aturan SELinux untuk %s</string>\n    <string name=\"require_kernel_version\">Versi KernelSU saat ini %1$d terlalu rendah untuk manajer berfungsi normal. Harap memperbarui ke versi %2$d atau di atasnya!</string>\n    <string name=\"module_changelog\">Catatan perubahan</string>\n    <string name=\"app_profile_template_import_success\">Berhasil diimpor</string>\n    <string name=\"app_profile_export_to_clipboard\">Ekspor ke papan klip</string>\n    <string name=\"app_profile_template_export_empty\">Tidak ditemukan templat lokal untuk diekspor!</string>\n    <string name=\"app_profile_template_id_exist\">ID templat sudah ada!</string>\n    <string name=\"app_profile_import_from_clipboard\">Impor dari papan klip</string>\n    <string name=\"app_profile_template_name\">Nama</string>\n    <string name=\"app_profile_template_id_invalid\">ID templat tidak valid</string>\n    <string name=\"app_profile_template_create\">Buat templat</string>\n    <string name=\"app_profile_import_export\">Impor/Ekspor</string>\n    <string name=\"app_profile_template_save_failed\">Gagal menyimpan templat</string>\n    <string name=\"app_profile_template_edit\">Edit templat</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"settings_profile_template\">Templat Profil Aplikasi</string>\n    <string name=\"app_profile_template_description\">Deskripsi</string>\n    <string name=\"app_profile_template_save\">Simpan</string>\n    <string name=\"settings_profile_template_summary\">Kelola templat lokal dan online dari Profil Aplikasi.</string>\n    <string name=\"app_profile_template_delete\">Hapus</string>\n    <string name=\"app_profile_template_import_empty\">Papan klip kosong!</string>\n    <string name=\"app_profile_template_view\">Lihat templat</string>\n    <string name=\"enable_web_debugging\">Debugging WebView</string>\n    <string name=\"enable_web_debugging_summary\">Dapat digunakan untuk mendebug antarmuka web (WebUI). Harap aktifkan hanya saat diperlukan.</string>\n    <string name=\"select_file_tip\">%1$s partisi image disarankan</string>\n    <string name=\"select_kmi\">Pilih KMI</string>\n    <string name=\"install_next\">Selanjutnya</string>\n    <string name=\"install_inactive_slot_warning\">Perangkatmu akan **DIPAKSA** untuk boot ke slot nonaktif saat ini setelah reboot! \\nGunakan hanya setelah proses OTA selesai. \\nLanjutkan?</string>\n    <string name=\"direct_install\">Langsung pasang (Disarankan)</string>\n    <string name=\"select_file\">Pilih berkas</string>\n    <string name=\"install_inactive_slot\">Pasang ke slot nonaktif (setelah OTA)</string>\n    <string name=\"grant_root_failed\">Gagal memberikan akses root!</string>\n    <string name=\"open\">Buka</string>\n    <string name=\"settings_check_update\">Periksa pembaruan</string>\n    <string name=\"settings_check_update_summary\">Memeriksa pembaruan secara otomatis saat membuka aplikasi.</string>\n    <string name=\"settings_uninstall_permanent_message\">Menghapus KernelSU (root dan semua modul) secara menyeluruh dan permanen.</string>\n    <string name=\"settings_uninstall_temporary\">Hapus sementara</string>\n    <string name=\"settings_restore_stock_image\">Pulihkan image bawaan</string>\n    <string name=\"settings_uninstall\">Hapus</string>\n    <string name=\"settings_uninstall_temporary_message\">Hapus KernelSU untuk sementara, memulihkan ke kondisi asal setelah reboot berikutnya.</string>\n    <string name=\"settings_uninstall_permanent\">Hapus permanen</string>\n    <string name=\"settings_restore_stock_image_message\">Pulihkan image bawaan pabrik (jika dicadangkan), biasanya digunakan sebelum pembaruan OTA; jika Anda perlu menghapus KernelSU, silakan gunakan \\\"Hapus secara permanen\\\".</string>\n    <string name=\"flash_success\">Pemasangan berhasil</string>\n    <string name=\"selected_lkm\">LKM dipilih: %s</string>\n    <string name=\"flashing\">Memasang</string>\n    <string name=\"flash_failed\">Pemasangan gagal</string>\n    <string name=\"save_log\">Simpan Log</string>\n    <string name=\"action\">Aksi</string>\n    <string name=\"log_saved\">Log disimpan</string>\n    <string name=\"module_sort_enabled_first\">Diaktifkan dahulu</string>\n    <string name=\"module_sort_action_first\">Aksi lebih dulu</string>\n    <string name=\"module_install_prompt_with_name\">Modul berikut akan dipasang: %1$s</string>\n    <string name=\"confirm\">Oke</string>\n    <string name=\"su_not_allowed\">Gagal memberikan akses SU untuk %s</string>\n    <string name=\"settings_module_check_update\">Periksa pembaruan modul</string>\n    <string name=\"install_upload_lkm_file\">Gunakan berkas LKM lokal</string>\n    <string name=\"install_only_support_ko_file\">Hanya berkas .ko yang didukung</string>\n    <string name=\"settings_sucompat\">Perintah SU klasik</string>\n    <string name=\"settings_sucompat_summary\">Mengizinkan akses root melalui /system/bin/su, pada proses baru yang dibuat.</string>\n    <string name=\"settings_kernel_umount\">Unmount modul (tingkat kernel)</string>\n    <string name=\"settings_kernel_umount_summary\">Unmount modul dari kernel pada Profil Aplikasi.</string>\n    <string name=\"processing\">Memproses…</string>\n    <string name=\"refresh_pulling\">Tarik ke bawah untuk menyegarkan</string>\n    <string name=\"refresh_release\">Lepas untuk menyegarkan</string>\n    <string name=\"refresh_refresh\">Menyegarkan…</string>\n    <string name=\"refresh_complete\">Penyegaran berhasil</string>\n    <string name=\"module_repos\">Repositori</string>\n    <string name=\"module_repos_sort_name\">Nama (A → Z)</string>\n    <string name=\"module_repos_source_code\">Sumber kode</string>\n    <string name=\"metamodule_uninstall_confirm\">Yakin ingin menghapus modul %s? Tindakan ini akan memengaruhi semua modul, dan fitur tertentu yang disediakan oleh metamodul (seperti mounting) tidak akan berfungsi lagi.</string>\n    <string name=\"home_gki_warning\">Mulai dari v3.0.0, fungsi mode GKI dibatasi untuk lingkungan pengujian. Kami tidak menyarankan untuk penggunaan harian, dan berkas image tidak akan lagi disediakan.</string>\n    <string name=\"app_profile_affects_following_apps\">Mempengaruhi apl berikut</string>\n    <string name=\"install_select_partition\">Pilih partisi</string>\n    <string name=\"feature_status_unsupported_summary\">Kernel tidak mendukung fitur ini</string>\n    <string name=\"feature_status_managed_summary\">Fitur ini dikelola oleh modul</string>\n    <string name=\"undo\">Batalkan</string>\n    <string name=\"module_undo_uninstall_success\">Berhasil membatalkan penghapusan %s</string>\n    <string name=\"module_undo_uninstall_failed\">Gagal membatalkan penghapusan: %s</string>\n    <string name=\"group_contains_apps\">Berisi %d aplikasi</string>\n    <string name=\"settings_theme\">Tema</string>\n    <string name=\"settings_theme_summary\">Pilih mode tema aplikasi.</string>\n    <string name=\"settings_theme_mode_system\">Ikuti sistem</string>\n    <string name=\"settings_theme_mode_light\">Terang</string>\n    <string name=\"settings_theme_mode_dark\">Gelap</string>\n    <string name=\"settings_key_color\">Warna utama</string>\n    <string name=\"settings_key_color_default\">Default</string>\n    <string name=\"color_blue\">Biru</string>\n    <string name=\"color_red\">Merah</string>\n    <string name=\"color_green\">Hijau</string>\n    <string name=\"color_purple\">Ungu</string>\n    <string name=\"color_orange\">Jingga</string>\n    <string name=\"color_teal\">Hijau kebiruan</string>\n    <string name=\"color_pink\">Merah muda</string>\n    <string name=\"color_brown\">Coklat</string>\n    <string name=\"network_offline\">Tidak terhubung ke jaringan</string>\n    <string name=\"network_retry\">Coba lagi</string>\n    <string name=\"tab_readme\">Baca Aku</string>\n    <string name=\"tab_releases\">Rilis</string>\n    <string name=\"tab_info\">Informasi</string>\n    <string name=\"safe_mode_module_disabled\">Pemasangan modul dinonaktifkan pada mode aman</string>\n    <string name=\"module_action_success\">Tindakan modul berhasil dieksekusi.</string>\n    <string name=\"settings_mode_enable_by_default\">Aktif (Default)</string>\n    <string name=\"settings_mode_disable_until_reboot\">Nonaktif hingga Reboot</string>\n    <string name=\"settings_mode_disable_always\">Selalu nonaktif</string>\n    <string name=\"module_shortcut_title\">Buat pintasan</string>\n    <string name=\"module_shortcut_name_label\">Nama pintasan</string>\n    <string name=\"module_shortcut_icon_pick\">Pilih ikon khusus</string>\n    <string name=\"module_shortcut_not_supported\">Launcher tidak mendukung pintasan layar utama.</string>\n    <string name=\"module_shortcut_created\">Pintasan dibuat di layar utama.</string>\n    <string name=\"module_shortcut_updated\">Pintasan diperbarui.</string>\n    <string name=\"module_shortcut_delete\">Hapus pintasan</string>\n    <string name=\"module_shortcut_permission_tip_xiaomi\">Harap aktifkan izin \\\"Buat pintasan layar utama\\\" untuk aplikasi ini di pengaturan Xiaomi.</string>\n    <string name=\"module_shortcut_permission_tip_oppo\">Harap aktifkan izin \\\"Pintasan layar utama\\\" untuk aplikasi ini di pengaturan OPPO.</string>\n    <string name=\"module_shortcut_permission_tip_default\">Jika pembuatan pintasan gagal, harap aktifkan izin pintasan layar utama untuk aplikasi ini di pengaturan sistem.</string>\n    <string name=\"no_such_module\">Modul %s tidak tersedia</string>\n    <string name=\"module_unavailable\">Modul %s dinonaktifkan, diperbarui, atau menunggu penghapusan</string>\n    <string name=\"select_file_tip_nogki\">Silakan pilih berkas image perangkat GKI yang ingin Anda tambal.</string>\n    <string name=\"current_kmi\">Versi KMI perangkat ini: %s</string>\n    <string name=\"current_device_kmi\">Ini perangkat KMI</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-it/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Home</string>\n    <string name=\"home_not_installed\">Non installato</string>\n    <string name=\"home_click_to_install\">Clicca per installare</string>\n    <string name=\"home_working\">In esecuzione</string>\n    <string name=\"home_working_version\">Versione: %d</string>\n    <string name=\"home_unsupported\">Non supportato</string>\n    <string name=\"home_unsupported_reason\">KernelSU ora supporta solo i kernel GKI</string>\n    <string name=\"home_kernel\">Kernel</string>\n    <string name=\"home_manager_version\">Versione del manager</string>\n    <string name=\"home_fingerprint\">Impronta della build di Android</string>\n    <string name=\"home_selinux_status\">Stato di SELinux</string>\n    <string name=\"selinux_status_disabled\">Disabilitato</string>\n    <string name=\"selinux_status_enforcing\">Enforcing</string>\n    <string name=\"selinux_status_permissive\">Permissive</string>\n    <string name=\"selinux_status_unknown\">Sconosciuto</string>\n    <string name=\"superuser\">Accesso root</string>\n    <string name=\"module_failed_to_enable\">Impossibile abilitare il modulo: %s</string>\n    <string name=\"module_failed_to_disable\">Impossibile disabilitare il modulo: %s</string>\n    <string name=\"module_empty\">Nessun modulo installato</string>\n    <string name=\"module\">Modulo</string>\n    <string name=\"uninstall\">Disinstalla</string>\n    <string name=\"module_install\">Installa</string>\n    <string name=\"install\">Installa</string>\n    <string name=\"reboot\">Riavvia</string>\n    <string name=\"settings\">Impostazioni</string>\n    <string name=\"reboot_userspace\">Riavvio rapido</string>\n    <string name=\"reboot_recovery\">Riavvia in modalità Recovery</string>\n    <string name=\"reboot_bootloader\">Riavvia in modalità Bootloader</string>\n    <string name=\"reboot_download\">Riavvia in modalità Download</string>\n    <string name=\"reboot_edl\">Riavvia in modalità EDL</string>\n    <string name=\"about\">Informazioni</string>\n    <string name=\"module_uninstall_confirm\">Sei sicuro di voler disinstallare il modulo %s?</string>\n    <string name=\"module_uninstall_success\">%s disinstallato</string>\n    <string name=\"module_uninstall_failed\">Impossibile disinstallare: %s</string>\n    <string name=\"module_version\">Versione</string>\n    <string name=\"module_author\">Autore</string>\n    <string name=\"show_system_apps\">Mostra app di sistema</string>\n    <string name=\"send_log\">Invia log</string>\n    <string name=\"safe_mode\">Modalità provvisoria</string>\n    <string name=\"reboot_to_apply\">Riavvia per applicare la modifica</string>\n    <string name=\"module_magisk_conflict\">I moduli sono disabilitati perché in conflitto con Magisk!</string>\n    <string name=\"home_learn_kernelsu\">Scopri KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Scopri come installare KernelSU e utilizzare i moduli</string>\n    <string name=\"home_support_title\">Supportaci</string>\n    <string name=\"home_support_content\">KernelSU è, e sempre sarà, gratuito e open source. Puoi comunque mostrarci il tuo apprezzamento facendo una donazione.</string>\n    <string name=\"about_source_code\"><![CDATA[Visualizza il codice sorgente su %1$s<br/>Unisciti al nostro canale %2$s]]></string>\n    <string name=\"profile_name\">Nome profilo</string>\n    <string name=\"profile_namespace\">Spazio dei nomi del mount</string>\n    <string name=\"profile_namespace_global\">Globale</string>\n    <string name=\"profile_groups\">Gruppi</string>\n    <string name=\"profile_namespace_inherited\">Ereditato</string>\n    <string name=\"profile_namespace_individual\">Individuale</string>\n    <string name=\"profile_default\">Predefinito</string>\n    <string name=\"profile_custom\">Personalizzato</string>\n    <string name=\"profile_template\">Modello</string>\n    <string name=\"profile_umount_modules\">Scollega moduli</string>\n    <string name=\"profile_selinux_context\">Contesto SELinux</string>\n    <string name=\"failed_to_update_app_profile\">Aggiornamento App Profile per %s fallito</string>\n    <string name=\"module_update\">Aggiorna</string>\n    <string name=\"launch_app\">Apri</string>\n    <string name=\"profile_capabilities\">Capacità</string>\n    <string name=\"settings_umount_modules_default\">Scollega moduli da default</string>\n    <string name=\"profile_selinux_rules\">Regole</string>\n    <string name=\"module_downloading\">Sto scaricando il modulo: %s</string>\n    <string name=\"module_start_downloading\">Inizia a scaricare:%s</string>\n    <string name=\"new_version_available\">Nuova versione: %s disponibile, tocca per aggiornare</string>\n    <string name=\"force_stop_app\">Arresto forzato</string>\n    <string name=\"restart_app\">Riavvia</string>\n    <string name=\"failed_to_update_sepolicy\">Aggiornamento regole SELinux per %s fallito</string>\n    <string name=\"profile_umount_modules_summary\">Attivando questa opzione permetterai a KernelSU di ripristinare ogni file modificato dai moduli per questa app.</string>\n    <string name=\"profile_selinux_domain\">Dominio</string>\n    <string name=\"settings_umount_modules_default_summary\">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.</string>\n    <string name=\"require_kernel_version\">La versione attualmente installata di KernelSU (%1$d) è troppo vecchia ed il gestore non può funzionare correttamente. Si prega di aggiornare alla versione %2$d o successiva!</string>\n    <string name=\"module_changelog\">Registro aggiornamenti</string>\n    <string name=\"app_profile_template_create\">Crea modello</string>\n    <string name=\"app_profile_template_edit\">Modifica modello</string>\n    <string name=\"app_profile_template_id\">identificatore</string>\n    <string name=\"app_profile_template_id_invalid\">Identificativo modello non valido</string>\n    <string name=\"app_profile_template_name\">Nome</string>\n    <string name=\"app_profile_template_view\">Visualizza modello</string>\n    <string name=\"app_profile_template_id_exist\">L\\'identificatore del modello è già in uso!</string>\n    <string name=\"app_profile_import_export\">Importa/Esporta</string>\n    <string name=\"app_profile_import_from_clipboard\">Importa dagli appunti</string>\n    <string name=\"app_profile_export_to_clipboard\">Esporta negli appunti</string>\n    <string name=\"app_profile_template_export_empty\">Impossibile trovare un modello locale da esportare!</string>\n    <string name=\"app_profile_template_import_success\">Importato con successo</string>\n    <string name=\"app_profile_template_import_empty\">Gli appunti sono vuoti!</string>\n    <string name=\"grant_root_failed\">Impossibile ottenere l\\'accesso root!</string>\n    <string name=\"settings_profile_template\">Modelli App Profile</string>\n    <string name=\"settings_profile_template_summary\">Gestisci i modelli locali e remoti di App Profile</string>\n    <string name=\"app_profile_template_delete\">Elimina</string>\n    <string name=\"app_profile_template_description\">Descrizione</string>\n    <string name=\"app_profile_template_save\">Salva</string>\n    <string name=\"app_profile_template_save_failed\">Impossibile salvare il modello</string>\n    <string name=\"open\">Apri</string>\n    <string name=\"settings_check_update\">Controlla aggiornamenti</string>\n    <string name=\"settings_check_update_summary\">Controlla automaticamente la disponibilità di aggiornamenti all\\'apertura dell\\'applicazione</string>\n    <string name=\"enable_web_debugging\">Abilita il debug di WebView</string>\n    <string name=\"enable_web_debugging_summary\">Può essere usato per svolgere il debug di WebUI, è consigliato attivarlo solo quando necessario.</string>\n    <string name=\"select_file_tip\">È consigliato usare immagine della partizione %1$s</string>\n    <string name=\"select_kmi\">Scegli il KMI</string>\n    <string name=\"install_next\">Avanti</string>\n    <string name=\"direct_install\">Installazione diretta (Raccomandata)</string>\n    <string name=\"select_file\">Scegli un file</string>\n    <string name=\"install_inactive_slot\">Installa nello slot inattivo (dopo OTA)</string>\n    <string name=\"install_inactive_slot_warning\">Il tuo dispositivo sarà **FORZATO** ad avviarsi nello slot inattivo dopo il riavvio!\n\\nUsa questa opzione solo quando l\\'applicazione dell\\'aggiornamento OTA è terminata.\n\\nProcedere?</string>\n    <string name=\"settings_uninstall\">Disinstalla</string>\n    <string name=\"settings_uninstall_temporary\">Disinstalla temporaneamente</string>\n    <string name=\"settings_uninstall_permanent\">Disinstalla permanentemente</string>\n    <string name=\"settings_restore_stock_image\">Ripristina immagine originale del produttore</string>\n    <string name=\"settings_uninstall_temporary_message\">Disinstalla temporaneamente KernelSU, ripristina lo stato originale dopo il prossimo riavvio.</string>\n    <string name=\"settings_uninstall_permanent_message\">Disinstalla KernelSU (root e tutti i moduli) completamente e permanentemente.</string>\n    <string name=\"flashing\">Installazione</string>\n    <string name=\"flash_success\">Installazione completata</string>\n    <string name=\"flash_failed\">Installazione fallita</string>\n    <string name=\"selected_lkm\">LKM selezionato: %s</string>\n    <string name=\"settings_restore_stock_image_message\">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\\\".</string>\n    <string name=\"save_log\">Salva Registri</string>\n    <string name=\"settings_sucompat\">Reindirizza binario su</string>\n    <string name=\"settings_sucompat_summary\">Consente alle app con autorizzazione Superuser nel profilo app di ottenere una shell superuser eseguendo /system/bin/su; efficace solo per i nuovi processi.</string>\n    <string name=\"settings_kernel_umount\">Umount del kernel</string>\n    <string name=\"settings_kernel_umount_summary\">Comportamento di umount a livello di kernel controllato da KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-iw/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"reboot_to_apply\">הפעל מחדש כדי להכניס לתוקף</string>\n    <string name=\"home_click_to_learn_kernelsu\">למד כיצד להתקין את KernelSU ולהשתמש במודולים</string>\n    <string name=\"selinux_status_unknown\">לא ידוע</string>\n    <string name=\"show_system_apps\">הצג אפליקציות מערכת</string>\n    <string name=\"module_uninstall_success\">%s הוסר</string>\n    <string name=\"profile_umount_modules\">הסרת טעינת מודולים</string>\n    <string name=\"send_log\">שלח לוג</string>\n    <string name=\"selinux_status_disabled\">מושבת</string>\n    <string name=\"home_support_title\">תמכו בנו</string>\n    <string name=\"profile_namespace_inherited\">ירושה</string>\n    <string name=\"module_magisk_conflict\">מודולים מושבתים מכיוון שהם מתנגשים עם זה של Magisk!</string>\n    <string name=\"module_changelog\">יומן שינויים</string>\n    <string name=\"selinux_status_permissive\">התרים</string>\n    <string name=\"reboot_download\">הפעלה מחדש למצב הורדה</string>\n    <string name=\"settings_umount_modules_default\">טעינת מודולים כברירת מחדל</string>\n    <string name=\"profile_umount_modules_summary\">הפעלת אפשרות זו תאפשר ל-KernelSU לשחזר קבצים שהשתנו על ידי המודולים עבור יישום זה.</string>\n    <string name=\"profile_namespace_individual\">אישי</string>\n    <string name=\"module_failed_to_enable\">הפעלת המודל נכשלה: %s</string>\n    <string name=\"force_stop_app\">עצירה בכח</string>\n    <string name=\"reboot_edl\">הפעלה מחדש למצב EDL</string>\n    <string name=\"restart_app\">איתחול</string>\n    <string name=\"profile_capabilities\">יכולת</string>\n    <string name=\"module_start_downloading\">מפעיל מודל: %s</string>\n    <string name=\"profile_namespace_global\">גלובלי</string>\n    <string name=\"settings_umount_modules_default_summary\">ערך ברירת המחדל הגלובלי עבור \\\"טעינת מודולים\\\" בפרופילי אפליקציה. אם מופעל, זה יסיר את כל שינויי המודול למערכת עבור יישומים שאין להם ערכת פרופיל.</string>\n    <string name=\"selinux_status_enforcing\">אכיפה</string>\n    <string name=\"profile_selinux_context\">הקשר SELinux</string>\n    <string name=\"home_fingerprint\">טביעת אצבע</string>\n    <string name=\"profile_default\">ברירת מחדל</string>\n    <string name=\"launch_app\">להשיק</string>\n    <string name=\"safe_mode\">מצב בטוח</string>\n    <string name=\"require_kernel_version\">גרסת KernelSU הנוכחית %1$d נמוכה מדי כדי שהמנהל יפעל כראוי. אנא שדרג לגרסה %2$d ומעלה!</string>\n    <string name=\"reboot_recovery\">הפעלה מחדש לריקברי</string>\n    <string name=\"reboot_userspace\">רך Reboot</string>\n    <string name=\"profile_name\">שם פרופיל</string>\n    <string name=\"home_support_content\">KernelSU הוא, ותמיד יהיה, חינמי וקוד פתוח. עם זאת, תוכל להראות לנו שאכפת לך על ידי תרומה.</string>\n    <string name=\"uninstall\">הסרה</string>\n    <string name=\"profile_namespace\">טעינת מרחב שמות</string>\n    <string name=\"module_install\">התקנה</string>\n    <string name=\"home_click_to_install\">לחץ להתקנה</string>\n    <string name=\"profile_selinux_rules\">כללים</string>\n    <string name=\"profile_groups\">קבוצה</string>\n    <string name=\"module\">מודולים</string>\n    <string name=\"module_author\">יוצר</string>\n    <string name=\"about\">אודות</string>\n    <string name=\"home_working_version\">גרסה: %d</string>\n    <string name=\"reboot\">הפעלה מחדש</string>\n    <string name=\"home_unsupported_reason\">KernelSU תומך רק בליבת GKI כעת</string>\n    <string name=\"home_selinux_status\">סטטוס SELinux</string>\n    <string name=\"module_version\">גרסה</string>\n    <string name=\"home_unsupported\">אינו נתמך</string>\n    <string name=\"profile_selinux_domain\">תחום</string>\n    <string name=\"home\">בית</string>\n    <string name=\"profile_custom\">מותאם אישית</string>\n    <string name=\"profile_template\">תבנית</string>\n    <string name=\"module_downloading\">מוריד מודל: %s</string>\n    <string name=\"module_update\">עדכון</string>\n    <string name=\"home_learn_kernelsu\">למד אודות KernelSU</string>\n    <string name=\"module_uninstall_confirm\">האם אתה בטוח שברצונך להסיר את התקנת המודל %s\\?</string>\n    <string name=\"module_uninstall_failed\">הסרת התקנת %s נכשלה:</string>\n    <string name=\"superuser\">משתמש על</string>\n    <string name=\"settings\">הגדרות</string>\n    <string name=\"home_working\">עובד</string>\n    <string name=\"module_failed_to_disable\">השבתת מודל %s נכשלה:</string>\n    <string name=\"module_empty\">אין מודלים מותקנים</string>\n    <string name=\"install\">להתקין</string>\n    <string name=\"home_kernel\">Kernel</string>\n    <string name=\"home_not_installed\">לא מותקן</string>\n    <string name=\"failed_to_update_app_profile\">נכשל עדכון פרופיל האפליקציה עבור %s</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"failed_to_update_sepolicy\">נכשל עדכון כללי SELinux עבור: %s</string>\n    <string name=\"reboot_bootloader\">הפעלה מחדש לבוטלאודר</string>\n    <string name=\"about_source_code\">ראה את קוד המקור ב%1$s<br/>הצטרף אלינו %2$s בערוץ</string>\n    <string name=\"home_manager_version\">גרסת מנהל</string>\n    <string name=\"new_version_available\">גרסה חדשה עבור: %s זמינה, לחץ כדי לשדרג</string>\n    <string name=\"save_log\">שמור יומנים</string>\n    <string name=\"settings_sucompat\">ניתוב מחדש של su binary</string>\n    <string name=\"settings_sucompat_summary\">הפנה מחדש את /system/bin/su ל-ksud עבור אפליקציות שקיבלו הרשאת Superuser בפרופיל האפליקציה; יעיל לתהליכים חדשים בלבד.</string>\n    <string name=\"settings_kernel_umount\">ביטול טעינת קרנל</string>\n    <string name=\"settings_kernel_umount_summary\">התנהגות ביטול טעינה ברמת הקרנל הנשלטת על ידי KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-ja/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">ホーム</string>\n    <string name=\"home_not_installed\">未インストール</string>\n    <string name=\"home_click_to_install\">タップでインストール</string>\n    <string name=\"home_working\">動作中</string>\n    <string name=\"home_working_version\">バージョン: %d</string>\n    <string name=\"home_unsupported\">非対応</string>\n    <string name=\"home_unsupported_reason\">KernelSU は現在 GKI カーネルのみをサポートしています。ただし、GKI デバイス向けにイメージにパッチを適用することは可能です。</string>\n    <string name=\"home_kernel\">カーネル</string>\n    <string name=\"home_manager_version\">アプリのバージョン</string>\n    <string name=\"home_fingerprint\">Fingerprint</string>\n    <string name=\"home_selinux_status\">SELinux の状態</string>\n    <string name=\"selinux_status_disabled\">Disabled</string>\n    <string name=\"selinux_status_enforcing\">Enforcing</string>\n    <string name=\"selinux_status_permissive\">Permissive</string>\n    <string name=\"selinux_status_unknown\">不明</string>\n    <string name=\"superuser\">Superuser</string>\n    <string name=\"module_failed_to_enable\">モジュールの有効化に失敗しました: %s</string>\n    <string name=\"module_failed_to_disable\">モジュールの無効化に失敗しました: %s</string>\n    <string name=\"module_empty\">モジュールがインストールされていません</string>\n    <string name=\"module\">モジュール</string>\n    <string name=\"uninstall\">アンインストール</string>\n    <string name=\"module_install\">インストール</string>\n    <string name=\"install\">インストール</string>\n    <string name=\"reboot\">再起動</string>\n    <string name=\"settings\">設定</string>\n    <string name=\"reboot_userspace\">通常の再起動</string>\n    <string name=\"reboot_recovery\">リカバリーへ再起動</string>\n    <string name=\"reboot_bootloader\">ブートローダー へ再起動</string>\n    <string name=\"reboot_download\">ダウンロードモードへ再起動</string>\n    <string name=\"reboot_edl\">EDL へ再起動</string>\n    <string name=\"about\">アプリについて</string>\n    <string name=\"module_uninstall_confirm\">モジュール %s をアンインストールしますか？</string>\n    <string name=\"module_uninstall_success\">%s はアンインストールされました</string>\n    <string name=\"module_uninstall_failed\">アンインストールに失敗しました: %s</string>\n    <string name=\"module_version\">バージョン</string>\n    <string name=\"module_author\">制作者</string>\n    <string name=\"show_system_apps\">システムアプリを表示</string>\n    <string name=\"send_log\">ログを送信</string>\n    <string name=\"safe_mode\">セーフモード</string>\n    <string name=\"reboot_to_apply\">再起動すると有効化されます</string>\n    <string name=\"module_magisk_conflict\">モジュールが Magisk との競合により利用できません!</string>\n    <string name=\"home_learn_kernelsu\">KernelSU について</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/ja_JP/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">KernelSU のインストール方法やモジュールの使い方はこちら</string>\n    <string name=\"home_support_title\">支援する</string>\n    <string name=\"home_support_content\">KernelSU はこれからもずっと無料でオープンソースです。寄付をして頂くことで、開発を支援していただけます。</string>\n    <string name=\"about_source_code\"><![CDATA[%1$s でソースコードを表示<br/>%2$s チャンネルに参加]]></string>\n    <string name=\"profile\">アプリのプロファイル</string>\n    <string name=\"profile_default\">既定</string>\n    <string name=\"profile_template\">テンプレート</string>\n    <string name=\"profile_custom\">カスタム</string>\n    <string name=\"profile_name\">プロファイル名</string>\n    <string name=\"profile_namespace\">名前空間のマウント</string>\n    <string name=\"profile_namespace_inherited\">継承</string>\n    <string name=\"profile_namespace_global\">共通</string>\n    <string name=\"profile_namespace_individual\">分離</string>\n    <string name=\"profile_umount_modules\">モジュールのアンマウント</string>\n    <string name=\"profile_groups\">グループ</string>\n    <string name=\"profile_selinux_context\">SELinux コンテキスト</string>\n    <string name=\"failed_to_update_app_profile\">%s のアプリのプロファイルの更新をできませでした</string>\n    <string name=\"profile_selinux_domain\">ドメイン</string>\n    <string name=\"profile_selinux_rules\">ルール</string>\n    <string name=\"new_version_available\">新しいバージョン %s が利用可能です。タップしてダウンロード!</string>\n    <string name=\"module_update\">アップデート</string>\n    <string name=\"module_start_downloading\">ダウンロードを開始: %s</string>\n    <string name=\"launch_app\">起動</string>\n    <string name=\"force_stop_app\">強制停止</string>\n    <string name=\"restart_app\">再起動</string>\n    <string name=\"failed_to_update_sepolicy\">SELinux ルールの更新に失敗しました %s</string>\n    <string name=\"profile_capabilities\">ケーパビリティ</string>\n    <string name=\"module_downloading\">モジュールをダウンロード中: %s</string>\n    <string name=\"profile_umount_modules_summary\">このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。</string>\n    <string name=\"settings_umount_modules_default\">デフォルトでモジュールをアンマウントする</string>\n    <string name=\"settings_umount_modules_default_summary\">アプリプロファイルの「モジュールのアンマウント」の共通のデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。</string>\n    <string name=\"require_kernel_version\">現在の KernelSU バージョン %1$d はマネージャーが適切に機能するには低すぎます。 バージョン %2$d 以降にアップグレードしてください！</string>\n    <string name=\"module_changelog\">変更履歴</string>\n    <string name=\"app_profile_template_import_success\">インポート成功</string>\n    <string name=\"app_profile_export_to_clipboard\">クリップボードからエクスポート</string>\n    <string name=\"app_profile_template_export_empty\">エクスポートするローカル テンプレートが見つかりません！</string>\n    <string name=\"app_profile_template_id_exist\">テンプレート ID はすでに存在します！</string>\n    <string name=\"app_profile_import_from_clipboard\">クリップボードからインポート</string>\n    <string name=\"app_profile_template_name\">名前</string>\n    <string name=\"app_profile_template_id_invalid\">無効なテンプレート ID</string>\n    <string name=\"app_profile_template_create\">テンプレートの作成</string>\n    <string name=\"app_profile_import_export\">インポート/エクスポート</string>\n    <string name=\"app_profile_template_save_failed\">テンプレートの保存に失敗しました</string>\n    <string name=\"app_profile_template_edit\">テンプレートの編集</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"settings_profile_template\">アプリプロファイルのテンプレート</string>\n    <string name=\"app_profile_template_description\">説明</string>\n    <string name=\"app_profile_template_save\">保存</string>\n    <string name=\"settings_profile_template_summary\">アプリプロファイルのローカルおよびオンラインテンプレートを管理する。</string>\n    <string name=\"app_profile_template_delete\">消去</string>\n    <string name=\"app_profile_template_import_empty\">クリップボードが空です！</string>\n    <string name=\"app_profile_template_view\">テンプレートを表示</string>\n    <string name=\"settings_check_update\">アップデートを確認</string>\n    <string name=\"settings_check_update_summary\">アプリを開いたときにアップデートを自動的に確認する。</string>\n    <string name=\"grant_root_failed\">root の付与に失敗しました！</string>\n    <string name=\"open\">開く</string>\n    <string name=\"enable_web_debugging\">WebView デバッグ</string>\n    <string name=\"enable_web_debugging_summary\">WebUI のデバッグに使用できます。必要な場合にのみ有効にしてください。</string>\n    <string name=\"select_file_tip\">%1$s パーティション イメージが推奨されます</string>\n    <string name=\"select_kmi\">KMI を選択してください</string>\n    <string name=\"install_next\">次へ</string>\n    <string name=\"install_inactive_slot\">非アクティブなスロットにインストール (OTA 後)</string>\n    <string name=\"install_inactive_slot_warning\">再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。\\nこのオプションは、OTA が完了した後にのみ使用してください。\\n続行しますか？</string>\n    <string name=\"direct_install\">直接インストール (推奨)</string>\n    <string name=\"select_file\">ファイルを選択してください</string>\n    <string name=\"settings_uninstall_permanent\">完全にアンインストールする</string>\n    <string name=\"settings_restore_stock_image\">ストックイメージを復元</string>\n    <string name=\"settings_uninstall_temporary\">一時的にアンインストールする</string>\n    <string name=\"settings_uninstall\">アンインストール</string>\n    <string name=\"settings_uninstall_temporary_message\">KernelSU を一時的にアンインストールし、次回の再起動後に元の状態に戻します。</string>\n    <string name=\"settings_uninstall_permanent_message\">KernelSU (ルートおよびすべてのモジュール) を完全かつ永久にアンインストールします。</string>\n    <string name=\"settings_restore_stock_image_message\">バックアップが存在する場合、工場出荷時のイメージを復元できます (OTA の前に使用してください)。KernelSU をアンインストールする必要がある場合は、「完全にアンインストールする」を使用してください。</string>\n    <string name=\"flashing\">フラッシュ</string>\n    <string name=\"flash_success\">フラッシュ成功</string>\n    <string name=\"flash_failed\">フラッシュ失敗</string>\n    <string name=\"selected_lkm\">選択された LKM: %s</string>\n    <string name=\"save_log\">ログを保存</string>\n    <string name=\"action\">アクション</string>\n    <string name=\"log_saved\">保存されたログ</string>\n    <string name=\"module_sort_enabled_first\">並べ替え（最初に有効）</string>\n    <string name=\"module_sort_action_first\">並べ替え（アクション優先）</string>\n    <string name=\"su_not_allowed\">%s に superuser アクセスを許可できませんでした</string>\n    <string name=\"module_install_prompt_with_name\">次のモジュールがインストールされます: %1$s</string>\n    <string name=\"confirm\">確認</string>\n    <string name=\"settings_module_check_update\">モジュールの更新を確認する</string>\n    <string name=\"install_upload_lkm_file\">ローカル LKM ファイルを使用する</string>\n    <string name=\"install_only_support_ko_file\">.ko ファイルのみサポートされています</string>\n    <string name=\"settings_sucompat\">従来の SU コマンド</string>\n    <string name=\"settings_sucompat_summary\">新規プロセスで /system/bin/su 経由での root アクセスを許可します。</string>\n    <string name=\"settings_kernel_umount\">モジュールのアンマウント (カーネルレベル)</string>\n    <string name=\"settings_kernel_umount_summary\">アプリプロファイルに基づいて、カーネルからモジュールをアンマウントします。</string>\n    <string name=\"processing\">処理中…</string>\n    <string name=\"refresh_pulling\">下に引いて更新</string>\n    <string name=\"refresh_release\">リリースして更新</string>\n    <string name=\"refresh_refresh\">更新中…</string>\n    <string name=\"refresh_complete\">更新に成功しました</string>\n    <string name=\"module_repos\">リポジトリ</string>\n    <string name=\"module_repos_sort_name\">名前（A→Z）</string>\n    <string name=\"module_repos_source_code\">ソースコード</string>\n    <string name=\"metamodule_uninstall_confirm\">モジュール %s をアンインストールしてもよろしいですか? この操作はすべてのモジュールに影響し、メタモジュールによって提供される特定の機能 (マウントなど) は動作しなくなります。</string>\n    <string name=\"home_gki_warning\">バージョン 3.0.0 以降、GKI ワークモードはテスト環境でのみ使用されます。日常的な使用には推奨されません。また、イメージファイルの提供も停止されます。</string>\n    <string name=\"app_profile_affects_following_apps\">以下のアプリに影響します</string>\n    <string name=\"install_select_partition\">パーティションを選択</string>\n    <string name=\"feature_status_unsupported_summary\">カーネルはこの機能をサポートしていません</string>\n    <string name=\"safe_mode_module_disabled\">セーフモードではモジュールのインストールが無効になっています</string>\n    <string name=\"feature_status_managed_summary\">この機能はモジュールによって管理されます</string>\n    <string name=\"undo\">元に戻す</string>\n    <string name=\"module_undo_uninstall_success\">%s のアンインストールを正常にキャンセルしました</string>\n    <string name=\"module_undo_uninstall_failed\">アンインストールを元に戻すことができませんでした: %s</string>\n    <string name=\"group_contains_apps\">%d 個のアプリが含まれています</string>\n    <string name=\"settings_theme\">テーマ</string>\n    <string name=\"settings_theme_summary\">アプリのテーマモードを選択します。</string>\n    <string name=\"settings_theme_mode_system\">システムに従う</string>\n    <string name=\"settings_theme_mode_light\">ライト</string>\n    <string name=\"settings_theme_mode_dark\">ダーク</string>\n    <string name=\"settings_key_color\">キーカラー</string>\n    <string name=\"settings_key_color_default\">デフォルト</string>\n    <string name=\"color_blue\">ブルー</string>\n    <string name=\"color_red\">レッド</string>\n    <string name=\"color_green\">グリーン</string>\n    <string name=\"color_purple\">パープル</string>\n    <string name=\"color_orange\">オレンジ</string>\n    <string name=\"color_teal\">ティール</string>\n    <string name=\"color_pink\">ピンク</string>\n    <string name=\"color_brown\">ブラウン</string>\n    <string name=\"network_offline\">ネットワークに接続されていません</string>\n    <string name=\"network_retry\">再試行</string>\n    <string name=\"tab_readme\">README</string>\n    <string name=\"tab_releases\">リリース</string>\n    <string name=\"tab_info\">情報</string>\n    <string name=\"module_action_success\">モジュールのアクションが正常に実行されました。</string>\n    <string name=\"settings_mode_enable_by_default\">有効 (デフォルト)</string>\n    <string name=\"settings_mode_disable_until_reboot\">再起動するまで無効</string>\n    <string name=\"settings_mode_disable_always\">常に無効</string>\n    <string name=\"module_shortcut_title\">ショートカットを作成</string>\n    <string name=\"module_shortcut_name_label\">ショートカット名</string>\n    <string name=\"module_shortcut_icon_pick\">カスタムアイコンを選択</string>\n    <string name=\"module_shortcut_not_supported\">ランチャーはホーム画面ショートカットをサポートしていません。</string>\n    <string name=\"module_shortcut_created\">ホーム画面にショートカットを作成しました。</string>\n    <string name=\"module_shortcut_updated\">ショートカットが更新されました。</string>\n    <string name=\"module_shortcut_delete\">ショートカットを削除</string>\n    <string name=\"module_shortcut_permission_tip_xiaomi\">Xiaomi の設定で、このアプリに対して「ホーム画面ショートカット」権限を有効にしてください。</string>\n    <string name=\"module_shortcut_permission_tip_oppo\">OPPO の設定で、このアプリに対して「ホーム画面ショートカット」権限を有効にしてください。</string>\n    <string name=\"module_shortcut_permission_tip_default\">ショートカットの作成に失敗した場合は、システム設定でこのアプリに対してホーム画面ショートカットの権限を有効にしてください。</string>\n    <string name=\"no_such_module\">モジュール %s は存在しません</string>\n    <string name=\"module_unavailable\">モジュール %s は無効、更新中、または削除の待機状態にあります</string>\n    <string name=\"select_file_tip_nogki\">パッチを適用する GKI デバイスのイメージファイルを選択してください</string>\n    <string name=\"current_kmi\">このデバイスの KMI バージョン: %s</string>\n    <string name=\"current_device_kmi\">このデバイスの KMI</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-km/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">ទំព័រដើម</string>\n    <string name=\"home_not_installed\">មិនទាន់បានដំឡើង</string>\n    <string name=\"home_click_to_install\">ចុចដើម្បីដំឡើង</string>\n    <string name=\"settings_sucompat\">កែផ្លូវ su binary</string>\n    <string name=\"settings_sucompat_summary\">បញ្ជូន /system/bin/su ទៅកាន់ ksud សម្រាប់កម្មវិធីដែលបានទទួលសិទ្ធិ Superuser នៅក្នុងទម្រង់កម្មវិធី; មានប្រសិទ្ធភាពសម្រាប់ដំណើរការថ្មីប៉ុណ្ណោះ។</string>\n    <string name=\"settings_kernel_umount\">Kernel umount</string>\n    <string name=\"settings_kernel_umount_summary\">ឥរិយាបថ umount កម្រិតខឺណែលគ្រប់គ្រងដោយ KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-kn/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"reboot_to_apply\">ಪರಿಣಾಮ ಬೀರಲು ರೀಬೂಟ್ ಮಾಡಿ</string>\n    <string name=\"home_click_to_learn_kernelsu\">KernelSU ಅನ್ನು ಹೇಗೆ ಸ್ಥಾಪಿಸಬೇಕು ಮತ್ತು ಮಾಡ್ಯೂಲ್‌ಗಳನ್ನು ಬಳಸುವುದು ಹೇಗೆ ಎಂದು ತಿಳಿಯಿರಿ</string>\n    <string name=\"selinux_status_unknown\">ತಿಳಿಯದ</string>\n    <string name=\"show_system_apps\">ಸಿಸ್ಟಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ತೋರಿಸಿ</string>\n    <string name=\"module_uninstall_success\">%s ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ</string>\n    <string name=\"profile_umount_modules\">Umount ಮಾಡ್ಯೂಲ್‌ಗಳು</string>\n    <string name=\"send_log\">ಲಾಗ್ ಕಳುಹಿಸಿ</string>\n    <string name=\"home_support_title\">ನಮ್ಮನ್ನು ಬೆಂಬಲಿಸಿ</string>\n    <string name=\"profile_namespace_inherited\">ಪಿತ್ರಾರ್ಜಿತ</string>\n    <string name=\"module_magisk_conflict\">ಮಾಡ್ಯೂಲ್‌ಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ ಏಕೆಂದರೆ ಇದು ಮ್ಯಾಜಿಸ್ಕ್‌ನೊಂದಿಗೆ ಸಂಘರ್ಷವಾಗಿದೆ!</string>\n    <string name=\"module_changelog\">ಚೇಂಜ್ಲಾಗ್</string>\n    <string name=\"selinux_status_permissive\">Permissive</string>\n    <string name=\"settings_umount_modules_default\">ಡೀಫಾಲ್ಟ್ ಆಗಿ Umount ಮಾಡ್ಯೂಲ್</string>\n    <string name=\"profile_umount_modules_summary\">ಈ ಆಯ್ಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸುವುದರಿಂದ ಈ ಅಪ್ಲಿಕೇಶನ್‌ಗಾಗಿ ಮಾಡ್ಯೂಲ್‌ಗಳ ಮೂಲಕ ಯಾವುದೇ ಮಾರ್ಪಡಿಸಿದ ಫೈಲ್‌ಗಳನ್ನು ಮರುಸ್ಥಾಪಿಸಲು KernelSU ಗೆ ಅನುಮತಿಸುತ್ತದೆ.</string>\n    <string name=\"profile_namespace_individual\">ವೈಯಕ್ತಿಕ</string>\n    <string name=\"module_failed_to_enable\">ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ: %s</string>\n    <string name=\"force_stop_app\">ಫೋರ್ಸ್ ಸ್ಟಾಪ್</string>\n    <string name=\"reboot_edl\">EDL ಗೆ ರೀಬೂಟ್</string>\n    <string name=\"profile_capabilities\">ಸಾಮರ್ಥ್ಯಗಳು</string>\n    <string name=\"module_start_downloading\">ಡೌನ್‌ಲೋಡ್ ಮಾಡುವುದನ್ನು ಪ್ರಾರಂಭಿಸಿ: %s</string>\n    <string name=\"profile_namespace_global\">ಜಾಗತಿಕ</string>\n    <string name=\"settings_umount_modules_default_summary\">ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್‌ಗಳಲ್ಲಿ \\\"Umount ಮಾಡ್ಯೂಲ್\\\" ಗಾಗಿ ಜಾಗತಿಕ ಡೀಫಾಲ್ಟ್ ಮೌಲ್ಯ. ಸಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಪ್ರೊಫೈಲ್ ಸೆಟ್ ಅನ್ನು ಹೊಂದಿರದ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಿಗಾಗಿ ಸಿಸ್ಟಮ್‌ಗೆ ಎಲ್ಲಾ ಮಾಡ್ಯೂಲ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಇದು ತೆಗೆದುಹಾಕುತ್ತದೆ.</string>\n    <string name=\"profile_selinux_context\">SELinux ಸಂದರ್ಭ</string>\n    <string name=\"profile_default\">ಡೀಫಾಲ್ಟ್</string>\n    <string name=\"launch_app\">ಲಾಂಚ್</string>\n    <string name=\"safe_mode\">ಸುರಕ್ಷಿತ ಮೋಡ್</string>\n    <string name=\"require_kernel_version\">ಪ್ರಸ್ತುತ KernelSU ಆವೃತ್ತಿ %1$d ಮ್ಯಾನೇಜರ್ ಸರಿಯಾಗಿ ಕಾರ್ಯನಿರ್ವಹಿಸಲು ತುಂಬಾ ಕಡಿಮೆಯಾಗಿದೆ. ದಯವಿಟ್ಟು ಆವೃತ್ತಿ %2$d ಅಥವಾ ಹೆಚ್ಚಿನದಕ್ಕೆ ಅಪ್‌ಗ್ರೇಡ್ ಮಾಡಿ!</string>\n    <string name=\"reboot_userspace\">ಸಾಫ್ಟ್ ರೀಬೂಟ್</string>\n    <string name=\"profile_name\">ಪ್ರೊಫೈಲ್ ಹೆಸರು</string>\n    <string name=\"home_support_content\">KernelSU ಉಚಿತ ಮತ್ತು ಮುಕ್ತ ಮೂಲವಾಗಿದೆ ಮತ್ತು ಯಾವಾಗಲೂ ಇರುತ್ತದೆ. ಆದಾಗ್ಯೂ ನೀವು ದೇಣಿಗೆ ನೀಡುವ ಮೂಲಕ ನೀವು ಕಾಳಜಿ ವಹಿಸುತ್ತೀರಿ ಎಂದು ನಮಗೆ ತೋರಿಸಬಹುದು.</string>\n    <string name=\"uninstall\">ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್</string>\n    <string name=\"profile_namespace\">ಮೌಂಟ್ ನೇಮ್‌ಸ್ಪೇಸ್</string>\n    <string name=\"profile_selinux_rules\">ನಿಯಮಗಳು</string>\n    <string name=\"profile_groups\">ಗುಂಪುಗಳು</string>\n    <string name=\"module\">ಮಾಡ್ಯೂಲ್</string>\n    <string name=\"module_author\">ಲೇಖಕ</string>\n    <string name=\"about\">ಬಗ್ಗೆ</string>\n    <string name=\"home_working_version\">ವರ್ಷನ್: %d</string>\n    <string name=\"reboot\">ರೀಬೂಟ್</string>\n    <string name=\"home_unsupported_reason\">KernelSU ಈಗ GKI ಕರ್ನಲ್‌ಗಳನ್ನು ಮಾತ್ರ ಬೆಂಬಲಿಸುತ್ತದೆ</string>\n    <string name=\"home_selinux_status\">SELinux ಸ್ಥಿತಿ</string>\n    <string name=\"module_version\">ವರ್ಷನ್</string>\n    <string name=\"home_unsupported\">ಬೆಂಬಲಿತವಾಗಿಲ್ಲ</string>\n    <string name=\"profile_selinux_domain\">ಡೊಮೇನ್</string>\n    <string name=\"home\">ಮನೆ</string>\n    <string name=\"profile_custom\">ಕಸ್ಟಮ್</string>\n    <string name=\"profile_template\">ಟೆಂಪ್ಲೇಟ್</string>\n    <string name=\"module_downloading\">ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ: %s</string>\n    <string name=\"home_learn_kernelsu\">KernelSU ಕಲಿಯಿರಿ</string>\n    <string name=\"module_uninstall_confirm\">%s ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಅಸ್ಥಾಪಿಸಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ\\?</string>\n    <string name=\"module_uninstall_failed\">ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲು ವಿಫಲವಾಗಿದೆ: %s</string>\n    <string name=\"superuser\">ಸೂಪರ್ಯೂಸರ್</string>\n    <string name=\"home_working\">ಕೆಲಸ ಮಾಡುತ್ತಿದೆ</string>\n    <string name=\"module_failed_to_disable\">ಮಾಡ್ಯೂಲ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ: %s</string>\n    <string name=\"module_empty\">ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿಲ್ಲ</string>\n    <string name=\"home_kernel\">ಕರ್ನಲ್</string>\n    <string name=\"failed_to_update_app_profile\">%s ಗಾಗಿ ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್ ಅನ್ನು ನವೀಕರಿಸಲು ವಿಫಲವಾಗಿದೆ</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"about_source_code\">%1$s ನಲ್ಲಿ ಮೂಲ ಕೋಡ್ ಅನ್ನು ವೀಕ್ಷಿಸಿ<br/>ನಮ್ಮ %2$s ಚಾನಲ್‌ಗೆ ಸೇರಿ</string>\n    <string name=\"home_manager_version\">ಮ್ಯಾನೇಜರ್ ವರ್ಷನ್</string>\n    <string name=\"new_version_available\">ಹೊಸ ಆವೃತ್ತಿ: %s ಲಭ್ಯವಿದೆ, ಅಪ್‌ಗ್ರೇಡ್ ಮಾಡಲು ಕ್ಲಿಕ್ ಮಾಡಿ</string>\n    <string name=\"save_log\">ಲಾಗ್ಗಳನ್ನು ಉಳಿಸಿ</string>\n    <string name=\"settings_sucompat\">su ಬೈನರಿ ಮರುಮಾರ್ಗಗೊಳಿಸಿ</string>\n    <string name=\"settings_sucompat_summary\">ಆಪ್ ಪ್ರೊಫೈಲ್‌ನಲ್ಲಿ ಸೂಪರ್‌ಯೂಸರ್ ಅನುಮತಿ ಪಡೆದ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಿಗೆ /system/bin/su ಅನ್ನು ksud ಗೆ ಮರು ನಿರ್ದೇಶಿಸಿ; ಹೊಸ ಪ್ರಕ್ರಿಯೆಗಳಿಗೆ ಮಾತ್ರ ಪರಿಣಾಮಕಾರಿ.</string>\n    <string name=\"settings_kernel_umount\">ಕರ್ನಲ್ ಅನ್‌ಮೌಂಟ್</string>\n    <string name=\"settings_kernel_umount_summary\">KernelSU ನಿಂದ ನಿಯಂತ್ರಿಸಲ್ಪಡುವ ಕರ್ನಲ್-ಮಟ್ಟದ ಅನ್‌ಮೌಂಟ್ ನಡವಳಿಕೆ</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-ko/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">홈</string>\n    <string name=\"home_not_installed\">설치되지 않음</string>\n    <string name=\"home_click_to_install\">이 곳을 눌러 설치하기</string>\n    <string name=\"home_working\">작동 중</string>\n    <string name=\"home_working_version\">버전: %d</string>\n    <string name=\"home_unsupported\">지원되지 않음</string>\n    <string name=\"home_unsupported_reason\">KernelSU는 현재 GKI 커널만 지원합니다.</string>\n    <string name=\"home_kernel\">커널 버전</string>\n    <string name=\"home_manager_version\">매니저 버전</string>\n    <string name=\"home_fingerprint\">핑거프린트</string>\n    <string name=\"home_selinux_status\">SELinux 상태</string>\n    <string name=\"selinux_status_disabled\">비활성화됨</string>\n    <string name=\"selinux_status_enforcing\">강제</string>\n    <string name=\"selinux_status_permissive\">허용</string>\n    <string name=\"selinux_status_unknown\">알 수 없음</string>\n    <string name=\"superuser\">슈퍼유저</string>\n    <string name=\"module_failed_to_enable\">모듈 활성화 실패: %s</string>\n    <string name=\"module_failed_to_disable\">모듈 비활성화 실패: %s</string>\n    <string name=\"module_empty\">설치된 모듈 없음</string>\n    <string name=\"module\">모듈</string>\n    <string name=\"uninstall\">제거</string>\n    <string name=\"module_install\">설치</string>\n    <string name=\"install\">설치</string>\n    <string name=\"reboot\">다시 시작</string>\n    <string name=\"settings\">설정</string>\n    <string name=\"reboot_userspace\">빠른 다시 시작</string>\n    <string name=\"reboot_recovery\">리커버리로 다시 시작</string>\n    <string name=\"reboot_bootloader\">부트로더로 다시 시작</string>\n    <string name=\"reboot_download\">다운로드 모드로 다시 시작</string>\n    <string name=\"reboot_edl\">EDL 모드로 다시 시작</string>\n    <string name=\"about\">정보</string>\n    <string name=\"module_uninstall_confirm\">%s 모듈을 삭제할까요?</string>\n    <string name=\"module_uninstall_success\">%s 모듈 삭제됨</string>\n    <string name=\"module_uninstall_failed\">모듈 삭제 실패: %s</string>\n    <string name=\"module_version\">버전</string>\n    <string name=\"module_author\">제작자</string>\n    <string name=\"show_system_apps\">시스템 앱 표시</string>\n    <string name=\"send_log\">로그 보내기</string>\n    <string name=\"safe_mode\">안전 모드</string>\n    <string name=\"reboot_to_apply\">다시 시작하여 변경 사항 적용</string>\n    <string name=\"module_magisk_conflict\">Magisk와 충돌로 모듈을 사용할 수 없습니다!</string>\n    <string name=\"home_learn_kernelsu\">KernelSU 알아보기</string>\n    <string name=\"home_click_to_learn_kernelsu\">KernelSU 설치 및 모듈 사용 방법을 확인합니다.</string>\n    <string name=\"home_support_title\">지원하기</string>\n    <string name=\"home_support_content\">KernelSU는 현재와 미래에도 항상 무료이며 오픈 소스입니다. 기부를 통해 여러분의 관심과 지원을 표현해 주세요.</string>\n    <string name=\"about_source_code\"><![CDATA[%1$s에서 소스 코드 보기<br/>%2$s 채널 참가하기]]></string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"settings_umount_modules_default_summary\">앱 프로필 메뉴의 \\\"모듈 마운트 해제\\\" 설정에 대한 전역 기본값을 지정합니다. 활성화하면 개별 프로필이 설정되지 않은 앱에는 시스템에 대한 모듈의 모든 수정 사항이 제외됩니다.</string>\n    <string name=\"restart_app\">다시 시작</string>\n    <string name=\"profile_selinux_rules\">규칙</string>\n    <string name=\"new_version_available\">새 %s 버전을 사용할 수 있습니다, 여기를 눌러 업그레이드하세요!</string>\n    <string name=\"module_start_downloading\">다운로드 시작: %s</string>\n    <string name=\"force_stop_app\">강제 중지</string>\n    <string name=\"profile_default\">기본값</string>\n    <string name=\"profile_custom\">사용자 지정</string>\n    <string name=\"profile_template\">템플릿</string>\n    <string name=\"profile_name\">프로필 이름</string>\n    <string name=\"profile_namespace\">마운트 대상 네임스페이스</string>\n    <string name=\"profile_namespace_inherited\">상속</string>\n    <string name=\"profile_namespace_global\">전역</string>\n    <string name=\"profile_namespace_individual\">개별</string>\n    <string name=\"profile_groups\">사용자 그룹</string>\n    <string name=\"profile_umount_modules\">모듈 마운트 해제를 기본값으로</string>\n    <string name=\"profile_selinux_context\">SELinux 컨텍스트</string>\n    <string name=\"profile_capabilities\">권한</string>\n    <string name=\"failed_to_update_app_profile\">%s에 대한 앱 프로필 업데이트 실패</string>\n    <string name=\"settings_umount_modules_default\">모듈 마운트 해제를 기본값으로</string>\n    <string name=\"profile_umount_modules_summary\">이 옵션이 활성화되면 KernelSU는 이 앱에 대한 모듈의 모든 수정 사항을 복구합니다.</string>\n    <string name=\"module_update\">업데이트</string>\n    <string name=\"module_downloading\">모듈 다운로드 중: %s</string>\n    <string name=\"profile_selinux_domain\">도메인</string>\n    <string name=\"launch_app\">실행</string>\n    <string name=\"failed_to_update_sepolicy\">%s 앱에 대한 SELinux 규칙 업데이트 실패</string>\n    <string name=\"save_log\">로그 저장</string>\n    <string name=\"module_changelog\">업데이트 내역</string>\n    <string name=\"enable_web_debugging_summary\">WebUI 디버깅에 사용 가능, 필요한 경우에만 활성화하세요.</string>\n    <string name=\"flashing\">플래싱 중</string>\n    <string name=\"selected_lkm\">선택된 LKM: %s</string>\n    <string name=\"select_file_tip\">%1$s 파티션 이미지가 권장됩니다</string>\n    <string name=\"select_kmi\">KMI 선택</string>\n    <string name=\"install_next\">다음</string>\n    <string name=\"settings_uninstall_permanent_message\">완전하고 영구적으로 KernelSU(루트 권한 및 모든 모듈)를 제거합니다.</string>\n    <string name=\"enable_web_debugging\">WebView 디버깅</string>\n    <string name=\"require_kernel_version\">현재 KernelSU %1$d 버전은 매니저가 정상 작동하기에 너무 낮습니다. %2$d 버전 이상으로 업그레이드하세요!</string>\n    <string name=\"action\">액션</string>\n    <string name=\"settings_uninstall_temporary\">임시 제거</string>\n    <string name=\"open\">열기</string>\n    <string name=\"install_inactive_slot_warning\">재부팅 후 기기가 **강제로** 비활성 슬롯으로 부팅합니다!\\nOTA가 완료된 후에만 이 옵션을 사용하세요.\\n계속할까요?</string>\n    <string name=\"flash_success\">플래싱 성공</string>\n    <string name=\"flash_failed\">플래싱 실패</string>\n    <string name=\"settings_uninstall\">제거</string>\n    <string name=\"settings_uninstall_permanent\">영구 제거</string>\n    <string name=\"settings_uninstall_temporary_message\">임시적으로 KernelSU를 제거하고, 다음 재부팅 이후 기존 상태로 복구합니다.</string>\n    <string name=\"settings_profile_template\">앱 프로필 템플릿</string>\n    <string name=\"settings_profile_template_summary\">로컬 및 온라인의 앱 프로필 템플릿을 관리합니다.</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"app_profile_template_id_invalid\">잘못된 템플릿 ID</string>\n    <string name=\"app_profile_template_name\">이름</string>\n    <string name=\"app_profile_template_description\">설명</string>\n    <string name=\"app_profile_template_save\">저장</string>\n    <string name=\"app_profile_template_delete\">삭제</string>\n    <string name=\"app_profile_template_id_exist\">템플릿 ID가 이미 존재합니다!</string>\n    <string name=\"app_profile_import_export\">불러오기/내보내기</string>\n    <string name=\"app_profile_import_from_clipboard\">클립보드에서 불러오기</string>\n    <string name=\"app_profile_export_to_clipboard\">클립보드로 내보내기</string>\n    <string name=\"app_profile_template_import_success\">불러오기 성공</string>\n    <string name=\"app_profile_template_save_failed\">템플릿 저장 실패</string>\n    <string name=\"app_profile_template_import_empty\">클립보드가 비어 있습니다!</string>\n    <string name=\"grant_root_failed\">루트 권한 부여 실패!</string>\n    <string name=\"app_profile_template_create\">템플릿 생성</string>\n    <string name=\"app_profile_template_edit\">템플릿 편집</string>\n    <string name=\"app_profile_template_view\">템플릿 보기</string>\n    <string name=\"app_profile_template_export_empty\">내보낼 로컬 템플릿을 찾을 수 없습니다!</string>\n    <string name=\"select_file\">파일 선택</string>\n    <string name=\"direct_install\">직접 설치 (권장)</string>\n    <string name=\"install_inactive_slot\">비활성 슬롯에 설치 (OTA 이후)</string>\n    <string name=\"settings_restore_stock_image\">제조사 이미지로 복구</string>\n    <string name=\"settings_restore_stock_image_message\">제조사 이미지로 복구 (백업 존재 시), OTA 이전에 사용합니다. KernelSU를 제거해야 하는 경우, \\\"영구 삭제\\\"를 사용하세요.</string>\n    <string name=\"settings_check_update\">업데이트 확인</string>\n    <string name=\"settings_check_update_summary\">앱 실행 시 자동으로 업데이트를 확인합니다.</string>\n    <string name=\"log_saved\">로그 저장됨</string>\n    <string name=\"module_sort_enabled_first\">활성화 우선</string>\n    <string name=\"module_sort_action_first\">액션 우선</string>\n    <string name=\"module_install_prompt_with_name\">다음 모듈이 설치됩니다: %1$s</string>\n    <string name=\"confirm\">확인</string>\n    <string name=\"su_not_allowed\">%s에 슈퍼유저 권한을 부여할 수 없습니다</string>\n    <string name=\"settings_module_check_update\">모듈 업데이트 확인</string>\n    <string name=\"install_upload_lkm_file\">로컬 LKM 파일 사용</string>\n    <string name=\"install_only_support_ko_file\">.ko 파일만 지원됩니다</string>\n    <string name=\"processing\">처리 중…</string>\n    <string name=\"refresh_pulling\">당겨서 새로 고침</string>\n    <string name=\"refresh_release\">놓아서 새로 고침</string>\n    <string name=\"refresh_refresh\">새로 고침 중…</string>\n    <string name=\"refresh_complete\">새로 고침 성공</string>\n    <string name=\"install_select_partition\">파티션 선택</string>\n    <string name=\"module_repos\">리포지토리</string>\n    <string name=\"module_repos_sort_name\">이름 (A → Z)</string>\n    <string name=\"module_repos_source_code\">소스 코드</string>\n    <string name=\"metamodule_uninstall_confirm\">정말 %s 모듈을 제거할까요? 이 동작은 모든 모듈에 영향을 미치며, 메타모듈에서 제공하는 일부 기능(마운트 등)은 더 이상 작동하지 않습니다.</string>\n    <string name=\"home_gki_warning\">v3.0.0 부터, GKI 동작 모드는 테스트 환경에서만 사용됩니다. 일상적인 사용은 권장하지 않으며, 이미지 파일은 더 이상 제공되지 않습니다.</string>\n    <string name=\"app_profile_affects_following_apps\">다음 앱에 영향</string>\n    <string name=\"feature_status_unsupported_summary\">커널이 이 기능을 지원하지 않습니다.</string>\n    <string name=\"feature_status_managed_summary\">이 기능은 모듈에 의해 관리됩니다.</string>\n    <string name=\"undo\">되돌리기</string>\n    <string name=\"module_undo_uninstall_success\">%s 제거 취소 성공</string>\n    <string name=\"module_undo_uninstall_failed\">제거 취소 실패: %s</string>\n    <string name=\"group_contains_apps\">%d 앱을 포함</string>\n    <string name=\"settings_theme\">테마</string>\n    <string name=\"settings_theme_summary\">앱 테마 모드를 선택합니다.</string>\n    <string name=\"settings_theme_mode_system\">시스템 설정에 따름</string>\n    <string name=\"settings_theme_mode_light\">라이트</string>\n    <string name=\"settings_theme_mode_dark\">다크</string>\n    <string name=\"settings_key_color\">주 색상</string>\n    <string name=\"settings_key_color_default\">기본값</string>\n    <string name=\"color_blue\">파랑</string>\n    <string name=\"color_red\">빨강</string>\n    <string name=\"color_green\">초록</string>\n    <string name=\"color_purple\">보라</string>\n    <string name=\"color_orange\">주황</string>\n    <string name=\"color_teal\">청록</string>\n    <string name=\"color_pink\">분홍</string>\n    <string name=\"color_brown\">갈색</string>\n    <string name=\"network_offline\">네트워크에 연결되지 않음</string>\n    <string name=\"network_retry\">재시도</string>\n    <string name=\"tab_readme\">README</string>\n    <string name=\"tab_releases\">릴리즈</string>\n    <string name=\"tab_info\">정보</string>\n    <string name=\"safe_mode_module_disabled\">안전 모드에서 모듈 설치가 비활성화됨</string>\n    <string name=\"settings_sucompat\">기존 su 명령</string>\n    <string name=\"settings_sucompat_summary\">/system/bin/su를 통한 루트 권한 획득 허용 (새 프로세스 전용)</string>\n    <string name=\"settings_kernel_umount\">모듈 언마운트</string>\n    <string name=\"settings_kernel_umount_summary\">앱 프로필에 따라 커널에서 모듈을 언마운트합니다</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-lt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_fingerprint\">Pirštų atspaudas</string>\n    <string name=\"selinux_status_disabled\">Išjungta</string>\n    <string name=\"selinux_status_enforcing\">Priverstinas</string>\n    <string name=\"selinux_status_unknown\">Nežinomas</string>\n    <string name=\"superuser\">Supernaudotojai</string>\n    <string name=\"module_failed_to_enable\">Nepavyko įjungti modulio: %s</string>\n    <string name=\"module_failed_to_disable\">Nepavyko išjungti modulio: %s</string>\n    <string name=\"selinux_status_permissive\">Leistinas</string>\n    <string name=\"module_empty\">Nėra įdiegtų modulių</string>\n    <string name=\"module\">Moduliai</string>\n    <string name=\"reboot_userspace\">Perkrovimas neišjungus</string>\n    <string name=\"reboot_recovery\">Perkrauti į atkūrimo rėžimą</string>\n    <string name=\"reboot_bootloader\">Perkrauti į įkrovos tvarkyklę</string>\n    <string name=\"reboot_download\">Perkrauti į atsisiuntimo rėžimą</string>\n    <string name=\"about\">Apie</string>\n    <string name=\"module_uninstall_failed\">Nepavyko išdiegti: %s</string>\n    <string name=\"module_uninstall_success\">%s išdiegtas</string>\n    <string name=\"module_version\">Versija</string>\n    <string name=\"module_author\">Autorius</string>\n    <string name=\"show_system_apps\">Rodyti sistemos programas</string>\n    <string name=\"send_log\">Siųsti žurnalą</string>\n    <string name=\"reboot\">Paleisti iš naujo</string>\n    <string name=\"safe_mode\">Saugus rėžimas</string>\n    <string name=\"reboot_to_apply\">Paleiskite iš naujo, kad įsigaliotų</string>\n    <string name=\"module_magisk_conflict\">Moduliai yra išjungti, nes jie konfliktuoja su Magisk\\'s!</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_learn_kernelsu\">Sužinokite apie KernelSU</string>\n    <string name=\"home_click_to_learn_kernelsu\">Sužinokite, kaip įdiegti KernelSU ir naudoti modulius</string>\n    <string name=\"about_source_code\">Peržiūrėkite šaltinio kodą %1$s<br/>Prisijunkite prie mūsų %2$s kanalo</string>\n    <string name=\"profile_default\">Numatytas</string>\n    <string name=\"profile_template\">Šablonas</string>\n    <string name=\"profile_custom\">Pasirinktinis</string>\n    <string name=\"profile_name\">Profilio pavadinimas</string>\n    <string name=\"profile_namespace\">Prijungti vardų erdvę</string>\n    <string name=\"profile_namespace_inherited\">Paveldėtas</string>\n    <string name=\"profile_namespace_global\">Globalus</string>\n    <string name=\"profile_namespace_individual\">Individualus</string>\n    <string name=\"profile_groups\">Grupės</string>\n    <string name=\"profile_capabilities\">Galimybės</string>\n    <string name=\"profile_selinux_context\">SELinux kontekstas</string>\n    <string name=\"profile_umount_modules\">Atjungti modulius</string>\n    <string name=\"settings_umount_modules_default\">Atjungti modulius pagal numatytuosius parametrus</string>\n    <string name=\"profile_umount_modules_summary\">Įjungus šią parinktį, KernelSU galės atkurti visus modulių modifikuotus failus šiai programai.</string>\n    <string name=\"profile_selinux_domain\">Domenas</string>\n    <string name=\"profile_selinux_rules\">Taisyklės</string>\n    <string name=\"module_update\">Atnaujinti</string>\n    <string name=\"module_downloading\">Atsisiunčiamas modulis: %s</string>\n    <string name=\"module_start_downloading\">Pradedamas atsisiuntimas: %s</string>\n    <string name=\"new_version_available\">Nauja versija: %s pasiekiama, spustelėkite norėdami atsinaujinti</string>\n    <string name=\"launch_app\">Paleisti</string>\n    <string name=\"force_stop_app\">Priversti sustoti</string>\n    <string name=\"restart_app\">Perkrauti</string>\n    <string name=\"failed_to_update_sepolicy\">Nepavyko atnaujinti SELinux taisyklių: %s</string>\n    <string name=\"home\">Namai</string>\n    <string name=\"home_not_installed\">Neįdiegta</string>\n    <string name=\"home_unsupported_reason\">KernelSU dabar palaiko tik GKI branduolius</string>\n    <string name=\"home_click_to_install\">Spustelėkite norėdami įdiegti</string>\n    <string name=\"home_working\">Veikia</string>\n    <string name=\"home_working_version\">Versija: %d</string>\n    <string name=\"home_unsupported\">Nepalaikoma</string>\n    <string name=\"home_manager_version\">Tvarkyklės versija</string>\n    <string name=\"home_kernel\">Branduolys</string>\n    <string name=\"home_selinux_status\">SELinux statusas</string>\n    <string name=\"uninstall\">Išdiegti</string>\n    <string name=\"module_install\">Įdiegti</string>\n    <string name=\"install\">Įdiegti</string>\n    <string name=\"settings\">Parametrai</string>\n    <string name=\"reboot_edl\">Perkrauti į EDL</string>\n    <string name=\"module_uninstall_confirm\">Ar tikrai norite išdiegti modulį %s\\?</string>\n    <string name=\"home_support_title\">Paremkite mus</string>\n    <string name=\"home_support_content\">KernelSU yra ir visada bus nemokamas ir atvirojo kodo. Tačiau galite parodyti, kad jums rūpi, paaukodami mums.</string>\n    <string name=\"failed_to_update_app_profile\">Nepavyko atnaujinti programos profilio %s</string>\n    <string name=\"settings_umount_modules_default_summary\">Visuotinė numatytoji „Modulių atjungimo“ reikšmė programų profiliuose. Jei įjungta, ji pašalins visus sistemos modulio pakeitimus programoms, kurios neturi profilio.</string>\n    <string name=\"module_changelog\">Keitimų žurnalas</string>\n    <string name=\"require_kernel_version\">Ši KernelSU versija %1$d yra per žema, kad šis vadybininkas galėtų tinkamai funkcionuoti. Prašome atsinaujinti į versiją %2$d ar aukščiau!</string>\n    <string name=\"save_log\">Saglabāt Žurnālus</string>\n    <string name=\"settings_sucompat\">Nukreipti su dvejetainį failą</string>\n    <string name=\"settings_sucompat_summary\">Nukreipti /system/bin/su į ksud programoms, kurioms suteiktas Superuser leidimas programos profilyje; veiksminga tik naujiems procesams.</string>\n    <string name=\"settings_kernel_umount\">Branduolio atjungimas</string>\n    <string name=\"settings_kernel_umount_summary\">Branduolio lygio atjungimo elgsena, valdoma KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-lv/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"profile_umount_modules_summary\">Iespējojot šo opciju, KernelSU varēs atjaunot visus moduļos šīs lietojumprogrammas modificētos failus.</string>\n    <string name=\"failed_to_update_sepolicy\">Neizdevās atjaunināt SELinux noteikumus: %s</string>\n    <string name=\"settings_profile_template_summary\">Pārvaldiet vietējo un tiešsaistes lietotņu profila veidni</string>\n    <string name=\"app_profile_template_id_invalid\">Nederīgs veidnes id</string>\n    <string name=\"app_profile_template_id_exist\">veidnes id jau pastāv!</string>\n    <string name=\"app_profile_export_to_clipboard\">Eksportēt starpliktuvē</string>\n    <string name=\"app_profile_import_from_clipboard\">Importēt no starpliktuves</string>\n    <string name=\"app_profile_template_import_success\">Importēts veiksmīgi</string>\n    <string name=\"home\">Sākums</string>\n    <string name=\"home_not_installed\">Nav uzstādīts</string>\n    <string name=\"home_click_to_install\">Noklikšķiniet, lai uzstādītu</string>\n    <string name=\"home_working\">Darbojas</string>\n    <string name=\"home_working_version\">Versija: %d</string>\n    <string name=\"home_unsupported\">Neatbalstīts</string>\n    <string name=\"home_unsupported_reason\">KernelSU pagaidām atbalsta tikai GKI kodolus</string>\n    <string name=\"home_kernel\">Kodols</string>\n    <string name=\"home_manager_version\">Pārvaldnieka versija</string>\n    <string name=\"home_fingerprint\">Pirkstu nospiedums</string>\n    <string name=\"home_selinux_status\">SELinux statuss</string>\n    <string name=\"selinux_status_enforcing\">Piespiests</string>\n    <string name=\"selinux_status_disabled\">Atspējots</string>\n    <string name=\"selinux_status_unknown\">Nezināms</string>\n    <string name=\"superuser\">Superlietotājs</string>\n    <string name=\"module_failed_to_disable\">Neizdevās atspējot moduli: %s</string>\n    <string name=\"module_empty\">Nav uzstādīts neviens modulis</string>\n    <string name=\"module\">Moduļi</string>\n    <string name=\"uninstall\">Noņemt</string>\n    <string name=\"install\">Uzstādīšana</string>\n    <string name=\"reboot\">Restartēt</string>\n    <string name=\"settings\">Iestatījumi</string>\n    <string name=\"reboot_userspace\">Ātri restartēt</string>\n    <string name=\"reboot_bootloader\">Restartēt uz Sāknēšanas režīmu</string>\n    <string name=\"reboot_recovery\">Restartēt uz Atkopšanas režīmu</string>\n    <string name=\"reboot_download\">Restartēt uz Lejupielādes režīmu</string>\n    <string name=\"reboot_edl\">Restartēt uz EDL režīmu</string>\n    <string name=\"about\">Par lietotni</string>\n    <string name=\"module_uninstall_success\">%s noņemts</string>\n    <string name=\"module_uninstall_failed\">Neizdevās noņemt: %s</string>\n    <string name=\"module_author\">Autors</string>\n    <string name=\"show_system_apps\">Rādīt sistēmas lietotnes</string>\n    <string name=\"send_log\">Sūtīt žurnālus</string>\n    <string name=\"reboot_to_apply\">Restartējiet, lai stātos spēkā</string>\n    <string name=\"home_learn_kernelsu\">Uzzināt par KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Uzzināt, kā instalēt KernelSU un izmantot moduļus</string>\n    <string name=\"home_support_title\">Atbalsti mūs</string>\n    <string name=\"about_source_code\">Skatiet avota kodu vietnē %1$s<br/> Pievienojies mūsu %2$s kanālam</string>\n    <string name=\"profile_default\">Noklusējums</string>\n    <string name=\"profile_template\">Veidne</string>\n    <string name=\"profile_custom\">Pielāgots</string>\n    <string name=\"profile_name\">Profila vārds</string>\n    <string name=\"profile_namespace\">Mount nosaukumvieta</string>\n    <string name=\"profile_namespace_individual\">Individuāls</string>\n    <string name=\"profile_capabilities\">Iespējas</string>\n    <string name=\"profile_selinux_context\">SELinux konteksts</string>\n    <string name=\"profile_umount_modules\">Atvienot moduļus</string>\n    <string name=\"failed_to_update_app_profile\">Neizdevās atjaunināt lietotnes profilu %s</string>\n    <string name=\"settings_umount_modules_default\">Pēc noklusējuma atvienot moduļus</string>\n    <string name=\"settings_umount_modules_default_summary\">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.</string>\n    <string name=\"profile_selinux_domain\">Domēns</string>\n    <string name=\"profile_selinux_rules\">Noteikumi</string>\n    <string name=\"module_update\">Atjaunināt</string>\n    <string name=\"module_downloading\">Lejupielādē moduli: %s</string>\n    <string name=\"module_start_downloading\">Sākt lejupielādi: %s</string>\n    <string name=\"new_version_available\">Jaunā versija: %s ir pieejama, noklikšķiniet, lai atjauninātu</string>\n    <string name=\"launch_app\">Palaist</string>\n    <string name=\"force_stop_app\">Piespiedu apstāšana</string>\n    <string name=\"restart_app\">Restartēt aplikāciju</string>\n    <string name=\"module_changelog\">Izmaiņu žurnāls</string>\n    <string name=\"settings_profile_template\">Lietotnes profila veidne</string>\n    <string name=\"app_profile_template_create\">Izveidot veidni</string>\n    <string name=\"app_profile_template_edit\">Rediģēt veidni</string>\n    <string name=\"app_profile_template_id\">id</string>\n    <string name=\"app_profile_template_name\">Vārds</string>\n    <string name=\"app_profile_template_description\">Apraksts</string>\n    <string name=\"app_profile_template_save\">Saglabāt</string>\n    <string name=\"app_profile_template_delete\">Dzēst</string>\n    <string name=\"app_profile_template_view\">Skatīt veidni</string>\n    <string name=\"app_profile_import_export\">Importēt/Eksportēt</string>\n    <string name=\"app_profile_template_export_empty\">Nevar atrast vietējo eksportējamo veidni!</string>\n    <string name=\"app_profile_template_save_failed\">Neizdevās saglabāt veidni</string>\n    <string name=\"app_profile_template_import_empty\">Starpliktuve ir tukša!</string>\n    <string name=\"selinux_status_permissive\">Visatļautība</string>\n    <string name=\"module_failed_to_enable\">Neizdevās iespējot moduli: %s</string>\n    <string name=\"module_install\">Uzstādīt</string>\n    <string name=\"module_uninstall_confirm\">Vai tiešām vēlaties noņemt %s moduli?</string>\n    <string name=\"module_version\">Versija</string>\n    <string name=\"safe_mode\">Drošais režīms</string>\n    <string name=\"module_magisk_conflict\">Moduļi nav pieejami dēļ konflikta ar Magisk!</string>\n    <string name=\"home_support_content\">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.</string>\n    <string name=\"profile_groups\">Grupas</string>\n    <string name=\"profile_namespace_global\">Globāli</string>\n    <string name=\"require_kernel_version\">Pašreizējā KernelSU versija %1$d ir pārāk zema, lai pārvaldnieks darbotos pareizi. Lūdzu, atjauniniet uz versiju %2$d vai jaunāku!</string>\n    <string name=\"enable_web_debugging\">Iespējot WebView atkļūdošanu</string>\n    <string name=\"select_file_tip\">Ieteicams %1$s nodalījuma attēls</string>\n    <string name=\"install_next\">Nākamais</string>\n    <string name=\"profile_namespace_inherited\">Mantots</string>\n    <string name=\"select_file\">Izvēlieties failu</string>\n    <string name=\"install_inactive_slot\">Instalēt neaktīvajā slotā (pēc OTA)</string>\n    <string name=\"install_inactive_slot_warning\">Pēc restartēšanas jūsu ierīce tiks **PIESPIESTI** palaista pašreizējā neaktīvajā slotā!\n\\nIzmantojiet šo opciju tikai pēc OTA pabeigšanas\n\\nTurpināt?</string>\n    <string name=\"direct_install\">Tiešā instalēšana (Ieteicams)</string>\n    <string name=\"settings_uninstall\">Atinstalēt</string>\n    <string name=\"settings_uninstall_temporary\">Pagaidu atinstalēšana</string>\n    <string name=\"settings_restore_stock_image\">Atjaunot oriģinālo attēlu</string>\n    <string name=\"settings_uninstall_temporary_message\">Īslaicīgi atinstalēt KernelSU, pēc nākamās restartēšanas atjaunot sākotnējo stāvokli.</string>\n    <string name=\"settings_uninstall_permanent_message\">KernelSU (saknes un visu moduļu) pilnīga atinstalēšana.</string>\n    <string name=\"settings_restore_stock_image_message\">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\\\".</string>\n    <string name=\"selected_lkm\">Izvēlētais lkm: %s</string>\n    <string name=\"grant_root_failed\">Neizdevās piešķirt sakni!</string>\n    <string name=\"open\">Atvērt</string>\n    <string name=\"settings_check_update\">Pārbaudīt atjauninājumus</string>\n    <string name=\"settings_check_update_summary\">Automātiski pārbaudīt atjauninājumus atverot aplikāciju</string>\n    <string name=\"enable_web_debugging_summary\">Var izmantot WebUI atkļūdošanai, lūdzu, izmantot tikai tad, kad tas ir nepieciešams.</string>\n    <string name=\"select_kmi\">Izvēlieties KMI</string>\n    <string name=\"settings_uninstall_permanent\">Neatgriezeniski atinstalēt</string>\n    <string name=\"flashing\">Instalē</string>\n    <string name=\"flash_success\">Instalēts veiksmīgi</string>\n    <string name=\"flash_failed\">Instalēšana neizdevās</string>\n    <string name=\"save_log\">Išsaugoti Žurnalus</string>\n    <string name=\"module_sort_enabled_first\">Kārtot (Iespējotie augšgalā)</string>\n    <string name=\"confirm\">Apstiprināt</string>\n    <string name=\"module_install_prompt_with_name\">Tiks uzstādīti šādi moduļi : %1$s</string>\n    <string name=\"settings_sucompat\">Pārvirzīt su bināro failu</string>\n    <string name=\"settings_sucompat_summary\">Pārvirzīt /system/bin/su uz ksud lietotnēm, kurām piešķirta Superuser atļauja lietotnes profilā; efektīva tikai jauniem procesiem.</string>\n    <string name=\"settings_kernel_umount\">Kodola atvienošana</string>\n    <string name=\"settings_kernel_umount_summary\">Kodola līmeņa atvienošanas uzvedība, ko kontrolē KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-mr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_not_installed\">इंस्टॉल केले नाही</string>\n    <string name=\"home\">होम</string>\n    <string name=\"home_click_to_install\">इंस्टॉल साठी क्लिक करा</string>\n    <string name=\"home_working\">कार्यरत</string>\n    <string name=\"home_working_version\">आवृत्ती: %d</string>\n    <string name=\"home_unsupported\">असमर्थित</string>\n    <string name=\"home_unsupported_reason\">KernelSU आता फक्त GKI कर्नलचे समर्थन करते</string>\n    <string name=\"home_kernel\">कर्नल</string>\n    <string name=\"home_fingerprint\">फिंगरप्रिंट</string>\n    <string name=\"home_manager_version\">व्यवस्थापक आवृत्ती</string>\n    <string name=\"home_selinux_status\">SELinux स्थिती</string>\n    <string name=\"selinux_status_disabled\">अक्षम</string>\n    <string name=\"selinux_status_enforcing\">एनफोर्सिंग</string>\n    <string name=\"selinux_status_permissive\">परमिसिव</string>\n    <string name=\"selinux_status_unknown\">अज्ञात</string>\n    <string name=\"install\">स्थापित करा</string>\n    <string name=\"module_empty\">कोणतेही मॉड्यूल स्थापित केलेले नाही</string>\n    <string name=\"reboot\">रीबूट करा</string>\n    <string name=\"superuser\">सुपरयुझर</string>\n    <string name=\"module_failed_to_enable\">मॉड्यूल सक्षम करण्यात अयशस्वी: %s</string>\n    <string name=\"uninstall\">विस्थापित करा</string>\n    <string name=\"module_failed_to_disable\">मॉड्यूल अक्षम करण्यात अयशस्वी: %s</string>\n    <string name=\"module\">मॉड्यूल</string>\n    <string name=\"module_install\">स्थापित करा</string>\n    <string name=\"settings\">सेटिंग्ज</string>\n    <string name=\"reboot_userspace\">सॉफ्ट रीबूट</string>\n    <string name=\"about\">बद्दल</string>\n    <string name=\"reboot_edl\">EDL वर रीबूट करा</string>\n    <string name=\"module_uninstall_confirm\">तुमची खात्री आहे की तुम्ही मॉड्यूल %s विस्थापित करू इच्छिता\\?</string>\n    <string name=\"module_uninstall_failed\">विस्थापित करण्यात अयशस्वी: %s</string>\n    <string name=\"show_system_apps\">सिस्टम अॅप्स दाखवा</string>\n    <string name=\"reboot_bootloader\">बूटलोडरवर रीबूट करा</string>\n    <string name=\"module_uninstall_success\">%s विस्थापित</string>\n    <string name=\"module_version\">आवृत्ती</string>\n    <string name=\"module_author\">लेखक</string>\n    <string name=\"reboot_recovery\">रिकवरी मध्ये रिबुट करा</string>\n    <string name=\"reboot_download\">डाउनलोड करण्यासाठी रीबूट करा</string>\n    <string name=\"send_log\">लॉग पाठवा</string>\n    <string name=\"safe_mode\">सुरक्षित मोड</string>\n    <string name=\"reboot_to_apply\">प्रभावी होण्यासाठी रीबूट करा</string>\n    <string name=\"home_learn_kernelsu\">KernelSU शिका</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"module_magisk_conflict\">मॉड्यूल अक्षम केले आहेत कारण ते Magisk च्या विरोधाभास आहे!</string>\n    <string name=\"home_click_to_learn_kernelsu\">KernelSU कसे स्थापित करायचे आणि मॉड्यूल कसे वापरायचे ते शिका</string>\n    <string name=\"home_support_content\">KernelSU विनामूल्य आणि मुक्त स्रोत आहे, आणि नेहमीच असेल. तथापि, देणगी देऊन तुम्ही आम्हाला दाखवू शकता की तुमची काळजी आहे.</string>\n    <string name=\"home_support_title\">आम्हाला पाठिंबा द्या</string>\n    <string name=\"profile_custom\">कस्टम</string>\n    <string name=\"profile_namespace\">माउंट नेमस्पेस</string>\n    <string name=\"profile_default\">डीफॉल्ट</string>\n    <string name=\"profile_template\">साचा</string>\n    <string name=\"profile_namespace_individual\">वैयक्तिक</string>\n    <string name=\"profile_capabilities\">क्षमता</string>\n    <string name=\"about_source_code\">%1$s वर स्रोत कोड पहा<br/>आमच्या %2$s चॅनेलमध्ये सामील व्हा</string>\n    <string name=\"profile_name\">प्रोफाइल नाव</string>\n    <string name=\"profile_namespace_inherited\">इनहेरीटेड</string>\n    <string name=\"profile_namespace_global\">जागतिक</string>\n    <string name=\"profile_groups\">गट</string>\n    <string name=\"profile_selinux_context\">SELinux संदर्भ</string>\n    <string name=\"profile_umount_modules\">उमाउंट मॉड्यूल्स</string>\n    <string name=\"failed_to_update_app_profile\">%s साठी अॅप प्रोफाइल अपडेट करण्यात अयशस्वी</string>\n    <string name=\"settings_umount_modules_default\">डीफॉल्टनुसार मॉड्यूल्स उमाउंट करा</string>\n    <string name=\"settings_umount_modules_default_summary\">अॅप प्रोफाइलमधील \\\"उमाउंट मॉड्यूल्स\\\" साठी जागतिक डीफॉल्ट मूल्य. सक्षम असल्यास, ते प्रोफाइल सेट नसलेल्या ॲप्लिकेशनचे सिस्टममधील सर्व मॉड्यूल बदल काढून टाकेल.</string>\n    <string name=\"profile_umount_modules_summary\">हा पर्याय सक्षम केल्याने KernelSU ला या ऍप्लिकेशनसाठी मॉड्यूल्सद्वारे कोणत्याही सुधारित फाइल्स पुनर्संचयित करण्यास अनुमती मिळेल.</string>\n    <string name=\"failed_to_update_sepolicy\">यासाठी SELinux नियम अपडेट करण्यात अयशस्वी: %s</string>\n    <string name=\"profile_selinux_rules\">नियम</string>\n    <string name=\"module_update\">अपडेट करा</string>\n    <string name=\"profile_selinux_domain\">डोमेन</string>\n    <string name=\"module_downloading\">मॉड्यूल डाउनलोड करत आहे: %s</string>\n    <string name=\"module_start_downloading\">डाउनलोड करणे सुरू करा: %s</string>\n    <string name=\"new_version_available\">नवीन आवृत्ती: %s उपलब्ध आहे, डाउनलोड करण्यासाठी क्लिक करा</string>\n    <string name=\"force_stop_app\">सक्तीने थांबा</string>\n    <string name=\"launch_app\">लाँच करा</string>\n    <string name=\"restart_app\">पुन्हा सुरू करा</string>\n    <string name=\"save_log\">लॉग जतन करा</string>\n    <string name=\"settings_sucompat\">su बाइनरी पुन्हा मार्गस्थ करा</string>\n    <string name=\"settings_sucompat_summary\">ॲप प्रोफाईलमध्ये सुपरयुझर परवानगी दिलेल्या ॲप्ससाठी /system/bin/su ला ksud वर पुनर्निर्देशित करा; फक्त नवीन प्रक्रियांसाठी प्रभावी.</string>\n    <string name=\"settings_kernel_umount\">कर्नल अनमाउंट</string>\n    <string name=\"settings_kernel_umount_summary\">KernelSU द्वारे नियंत्रित कर्नल-स्तरीय अनमाउंट वर्तन</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-ms/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"selinux_status_unknown\">Tidak Diketahui</string>\n    <string name=\"selinux_status_disabled\">Lumpuhkan</string>\n    <string name=\"selinux_status_permissive\">Permisif</string>\n    <string name=\"reboot_download\">Reboot ke Download</string>\n    <string name=\"module_failed_to_enable\">Modul tidak berjaya diaktifkan: %s</string>\n    <string name=\"reboot_edl\">Reboot ke EDL</string>\n    <string name=\"selinux_status_enforcing\">Enforcing</string>\n    <string name=\"home_fingerprint\">Cap Jari</string>\n    <string name=\"reboot_recovery\">Reboot ke Recovery</string>\n    <string name=\"reboot_userspace\">Soft Reboot</string>\n    <string name=\"uninstall\">Padam</string>\n    <string name=\"module_install\">Pasang</string>\n    <string name=\"home_click_to_install\">Tekan untuk memasang</string>\n    <string name=\"module\">Modul</string>\n    <string name=\"about\">Tentang</string>\n    <string name=\"home_working_version\">Versi: %d</string>\n    <string name=\"reboot\">Reboot</string>\n    <string name=\"home_unsupported_reason\">KernelSU ketika ini hanya menyokong kernel GKI</string>\n    <string name=\"home_selinux_status\">Status SELinux</string>\n    <string name=\"home_unsupported\">Tidak Disokong</string>\n    <string name=\"home\">Layar Utama</string>\n    <string name=\"module_uninstall_confirm\">Apakah anda pasti ingin membuang modul %s\\?</string>\n    <string name=\"superuser\">Superuser</string>\n    <string name=\"settings\">Tetapan</string>\n    <string name=\"home_working\">Berjalan</string>\n    <string name=\"module_failed_to_disable\">Gagal mematikan modul: %s</string>\n    <string name=\"module_empty\">Tiada modul dipasang</string>\n    <string name=\"install\">Pasang</string>\n    <string name=\"home_kernel\">Kernel</string>\n    <string name=\"home_not_installed\">Tidak terpasang</string>\n    <string name=\"reboot_bootloader\">Reboot ke Bootloader</string>\n    <string name=\"home_manager_version\">Versi manager</string>\n    <string name=\"save_log\">Simpan Log</string>\n    <string name=\"settings_sucompat\">Ubaḥ hala binari su</string>\n    <string name=\"settings_sucompat_summary\">Ubaḥ hala /system/bin/su ke ksud untuk aplikasi yang diberikan kebenaran Superuser dalam Profil Aplikasi; berkesan untuk proses baharu sahaja.</string>\n    <string name=\"settings_kernel_umount\">Umount kernel</string>\n    <string name=\"settings_kernel_umount_summary\">Tingkah laku umount peringkat kernel dikawal oleh KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-my/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"settings_sucompat\">su binary လမ်းကြောင်းပြောင်းမည်</string>\n    <string name=\"settings_sucompat_summary\">အက်ပ်ပရိုဖိုင်တွင် Superuser ခွင့်ပြုချက် ပေးထားသော အက်ပ်များအတွက် /system/bin/su ကို ksud သို့ လမ်းကြောင်းပြောင်းမည်; လုပ်ငန်းစဉ်အသစ်များအတွက်သာ ထိရောက်မှုရှိသည်။</string>\n    <string name=\"settings_kernel_umount\">Kernel ဖြုတ်မည်</string>\n    <string name=\"settings_kernel_umount_summary\">KernelSU မှ ထိန်းချုပ်ထားသော Kernel အဆင့် ဖြုတ်မည့် အပြုအမူ</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-night/themes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Theme.KernelSU\" parent=\"android:Theme.Material.NoActionBar\">\n        <item name=\"android:windowLayoutInDisplayCutoutMode\">shortEdges</item>\n    </style>\n\n    <style name=\"Theme.KernelSU.WebUI\" parent=\"Theme.KernelSU\" />\n\n</resources>"
  },
  {
    "path": "manager/app/src/main/res/values-nl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Home</string>\n    <string name=\"home_not_installed\">Niet geïnstalleerd</string>\n    <string name=\"home_click_to_install\">Klik om te installeren</string>\n    <string name=\"home_working\">Werkend</string>\n    <string name=\"home_working_version\">Versie: %d</string>\n    <string name=\"home_unsupported\">Niet ondersteund</string>\n    <string name=\"home_unsupported_reason\">KernelSU ondersteunt alleen GKI kernels</string>\n    <string name=\"home_kernel\">Kernel version</string>\n    <string name=\"home_manager_version\">Manager versie</string>\n    <string name=\"home_fingerprint\">Fingerprint</string>\n    <string name=\"home_selinux_status\">SELinux status</string>\n    <string name=\"selinux_status_disabled\">Uitgeschakeld</string>\n    <string name=\"selinux_status_enforcing\">Afgedwongen</string>\n    <string name=\"selinux_status_permissive\">Permissief</string>\n    <string name=\"selinux_status_unknown\">Niet gekend</string>\n    <string name=\"superuser\">Supergebruiker</string>\n    <string name=\"module_failed_to_enable\">Mislukt om module in te schakelen: %s</string>\n    <string name=\"module_failed_to_disable\">Mislukt om module uit te schakelen: %s</string>\n    <string name=\"module_empty\">Geen module geïnstalleerde</string>\n    <string name=\"module\">Module</string>\n    <string name=\"uninstall\">Verwijderen</string>\n    <string name=\"module_install\">Installeren</string>\n    <string name=\"install\">Installeren</string>\n    <string name=\"reboot\">Herstart</string>\n    <string name=\"settings\">Instellingen</string>\n    <string name=\"reboot_userspace\">Soft herstart</string>\n    <string name=\"reboot_recovery\">Herstart naar Recovery</string>\n    <string name=\"reboot_bootloader\">Herstart naar Bootloader</string>\n    <string name=\"reboot_download\">Herstart om te downloaden</string>\n    <string name=\"reboot_edl\">Herstart naar EDL</string>\n    <string name=\"about\">Over</string>\n    <string name=\"module_uninstall_confirm\">Zeker van het verwijderen van module %s?</string>\n    <string name=\"module_uninstall_success\">%s verwijderd</string>\n    <string name=\"module_uninstall_failed\">Mislukt om te verwijderen: %s</string>\n    <string name=\"module_version\">Versie</string>\n    <string name=\"module_author\">Auteur</string>\n    <string name=\"show_system_apps\">Toon systeem apps</string>\n    <string name=\"send_log\">Verzenden Logs</string>\n    <string name=\"safe_mode\">Safe mode</string>\n    <string name=\"reboot_to_apply\">Herstart om effect te hebben</string>\n    <string name=\"module_magisk_conflict\">Modules zijn niet beschikbaar vanwege een conflict met Magisk!</string>\n    <string name=\"home_learn_kernelsu\">Leer KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Leer hoe KernelSU te installeren en modules te gebruiken</string>\n    <string name=\"home_support_title\">Ondersteun ons</string>\n    <string name=\"home_support_content\">KernelSU is en blijft gratis en open source. U kunt ons echter laten zien dat u ons steunt door een donatie te doen.</string>\n    <string name=\"about_source_code\"><![CDATA[Bekijk source code op %1$s<br/>Vervoeg ons %2$s kanaal]]></string>\n    <string name=\"profile\" translatable=\"false\">App profiel</string>\n    <string name=\"profile_default\">Standaard</string>\n    <string name=\"profile_template\">Sjabloon</string>\n    <string name=\"profile_custom\">Aangepast</string>\n    <string name=\"profile_name\">Profiel naam</string>\n    <string name=\"profile_namespace\">Koppel naamruimte</string>\n    <string name=\"profile_namespace_inherited\">Overgenomen</string>\n    <string name=\"profile_namespace_global\">Globaal</string>\n    <string name=\"profile_namespace_individual\">Individuëel</string>\n    <string name=\"profile_groups\">Groepen</string>\n    <string name=\"profile_capabilities\">Mogelijkheden</string>\n    <string name=\"profile_selinux_context\">SELinux context</string>\n    <string name=\"profile_umount_modules\">Ontkoppel modules</string>\n    <string name=\"failed_to_update_app_profile\">Mislukt om App Profiel te updaten voor %s</string>\n    <string name=\"settings_umount_modules_default\">Ontkoppel standaard de modules</string>\n    <string name=\"settings_umount_modules_default_summary\">De globale standaardwaarde voor \\\"Umount modules\\\" in App Profile. Als dit is ingeschakeld, worden alle modulewijzigingen in het systeem verwijderd voor apps waarvoor geen profiel is ingesteld.</string>\n    <string name=\"profile_umount_modules_summary\">Met deze optie ingeschakeld zal KernelSU toelaten om alle gewijzigde bestanden door de modules voor deze app te herstellen.</string>\n    <string name=\"profile_selinux_domain\">Domein</string>\n    <string name=\"profile_selinux_rules\">Regels</string>\n    <string name=\"module_update\">Update</string>\n    <string name=\"module_downloading\">Downloaden van module: %s</string>\n    <string name=\"new_version_available\">Nieuwe versie %s is beschikbaar,klik om te upgraden!</string>\n    <string name=\"launch_app\">Start</string>\n    <string name=\"force_stop_app\">Forceer stop</string>\n    <string name=\"restart_app\">Herstart</string>\n    <string name=\"module_start_downloading\">Begin met downloaden: %s</string>\n    <string name=\"failed_to_update_sepolicy\">Kan SELinux-regels niet bijwerken voor %s</string>\n    <string name=\"require_kernel_version\">De huidige KernelSU-versie %1$d is te laag voor de manager om goed te werken. Upgrade naar versie %2$d of hoger!</string>\n    <string name=\"module_changelog\">wijzigings logboek</string>\n    <string name=\"settings_profile_template\">App-profiel sjabloon</string>\n    <string name=\"app_profile_template_create\">Maken sjabloon</string>\n    <string name=\"app_profile_template_edit\">Bewerkin sjabloon</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"app_profile_template_id_invalid\">Ongeldige sjabloon ID</string>\n    <string name=\"app_profile_template_name\">Naam</string>\n    <string name=\"app_profile_template_save\">Redde</string>\n    <string name=\"app_profile_template_view\">Bekijken sjabloon</string>\n    <string name=\"app_profile_template_description\">Beschrijving</string>\n    <string name=\"settings_profile_template_summary\">Beheer lokale en online sjabloon van app-profiel.</string>\n    <string name=\"app_profile_template_delete\">Verwijderen</string>\n    <string name=\"app_profile_template_id_exist\">Sjabloon ID bestaat al!</string>\n    <string name=\"app_profile_template_save_failed\">Mislukt naar opslaan sjabloon</string>\n    <string name=\"app_profile_template_import_empty\">Klembord is leeg!</string>\n    <string name=\"app_profile_import_export\">Importeren/Exporteren</string>\n    <string name=\"app_profile_import_from_clipboard\">Importeren vanaf klembord</string>\n    <string name=\"app_profile_export_to_clipboard\">Exporteren naar klembord</string>\n    <string name=\"settings_check_update\">Controleer for updates</string>\n    <string name=\"enable_web_debugging\">WebView foutopsporing</string>\n    <string name=\"enable_web_debugging_summary\">Kan worden gebruikt om WebUI te debuggen. Schakel dit alleen in als dat nodig is.</string>\n    <string name=\"app_profile_template_export_empty\">Kan niet geen lokale sjabloon vinden om te exporteren!</string>\n    <string name=\"app_profile_template_import_success\">Succesvol geïmporteerd</string>\n    <string name=\"open\">Open</string>\n    <string name=\"settings_check_update_summary\">Controleer automatisch op updates bij het openen van de app.</string>\n    <string name=\"direct_install\">Directe installatie (aanbevolen)</string>\n    <string name=\"select_file\">Selecteer een bestand</string>\n    <string name=\"grant_root_failed\">Kan geen root verlenen!</string>\n    <string name=\"select_file_tip\">%1$s partitie-image wordt aanbevolen</string>\n    <string name=\"install_next\">Naast</string>\n    <string name=\"install_inactive_slot_warning\">Uw apparaat wordt **GEFORCEERD** om na een herstart op te starten naar het huidige inactieve slot!\n\\nGebruik deze optie alleen nadat OTA is voltooid.\n\\nDoorgaan?</string>\n    <string name=\"install_inactive_slot\">Installeren in inactief slot (na OTA)</string>\n    <string name=\"select_kmi\">KMI selecteren</string>\n    <string name=\"settings_uninstall\">Desinstalleren</string>\n    <string name=\"settings_uninstall_temporary\">Tijdelijk verwijderen</string>\n    <string name=\"settings_uninstall_permanent\">Permanent verwijderen</string>\n    <string name=\"settings_restore_stock_image\">Herstel stockafbeelding</string>\n    <string name=\"settings_uninstall_temporary_message\">Verwijder KernelSU tijdelijk en herstel het naar de oorspronkelijke staat na de volgende herstart.</string>\n    <string name=\"settings_uninstall_permanent_message\">Het verwijderen van KernelSU (root en alle modules) volledig en permanent.</string>\n    <string name=\"settings_restore_stock_image_message\">Herstel de standaard fabrieksimage (als er een back-up bestaat), die normaal gesproken vóór OTA wordt gebruikt. Als u KernelSU moet verwijderen, gebruikt u permanent verwijderen.</string>\n    <string name=\"flashing\">Knipperen</string>\n    <string name=\"save_log\">Logboeken Opslaan</string>\n    <string name=\"flash_success\">Flash-succes</string>\n    <string name=\"flash_failed\">Flash is mislukt</string>\n    <string name=\"selected_lkm\">Geselecteerde LKM: %s</string>\n    <string name=\"action\">Actie</string>\n    <string name=\"log_saved\">Logs opgeslagen</string>\n    <string name=\"module_sort_enabled_first\">Sorteren (eerst ingeschakeld)</string>\n    <string name=\"module_sort_action_first\">Sorteren (actie eerst)</string>\n    <string name=\"module_install_prompt_with_name\">De volgende modules worden geïnstalleerd: %1$s</string>\n    <string name=\"su_not_allowed\">Kan geen Superuser-toegang verlenen aan %s</string>\n    <string name=\"confirm\">Bevestigen</string>\n    <string name=\"settings_module_check_update\">Controleer op module-updates</string>\n    <string name=\"install_upload_lkm_file\">Gebruik lokaal LKM-bestand</string>\n    <string name=\"install_only_support_ko_file\">Alleen .ko bestanden worden ondersteund</string>\n    <string name=\"settings_sucompat\">su binary omleiden</string>\n    <string name=\"settings_sucompat_summary\">Stelt apps met Superuser-toestemming in App Profiel in staat om een superuser-shell te verkrijgen door /system/bin/su uit te voeren; alleen effectief voor nieuwe processen.</string>\n    <string name=\"settings_kernel_umount\">Kernel ontkoppelen</string>\n    <string name=\"settings_kernel_umount_summary\">Kernel-niveau ontkoppelgedrag beheerd door KernelSU</string>\n    <string name=\"processing\">Verwerken…</string>\n    <string name=\"refresh_pulling\">Trek omlaag om te vernieuwen</string>\n    <string name=\"refresh_release\">Vrijgeven om te vernieuwen</string>\n    <string name=\"refresh_refresh\">Verfrissend…</string>\n    <string name=\"refresh_complete\">Succesvol vernieuwd</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-pl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\" translatable=\"false\">KernelSU</string>\n    <string name=\"home\">Strona główna</string>\n    <string name=\"home_not_installed\">Nie zainstalowano</string>\n    <string name=\"home_click_to_install\">Kliknij, aby zainstalować</string>\n    <string name=\"home_working\">Działa</string>\n    <string name=\"home_working_version\">Wersja: %d</string>\n    <string name=\"home_unsupported\">Nieobsługiwany</string>\n    <string name=\"home_unsupported_reason\">KernelSU obsługuje obecnie tylko jądra GKI.</string>\n    <string name=\"home_kernel\">Wersja jądra</string>\n    <string name=\"home_manager_version\">Wersja menedżera</string>\n    <string name=\"home_fingerprint\">Odcisk</string>\n    <string name=\"home_selinux_status\">Status SELinux</string>\n    <string name=\"selinux_status_disabled\">Wyłączony</string>\n    <string name=\"selinux_status_enforcing\">Enforcing</string>\n    <string name=\"selinux_status_permissive\">Dozwolony</string>\n    <string name=\"selinux_status_unknown\">Nieznany</string>\n    <string name=\"superuser\">Superużytkownik</string>\n    <string name=\"module_failed_to_enable\">Nie udało się włączyć modułu: %s</string>\n    <string name=\"module_failed_to_disable\">Nie udało się wyłączyć modułu: %s</string>\n    <string name=\"module_empty\">Brak zainstalowanych modułów</string>\n    <string name=\"module\">Moduły</string>\n    <string name=\"uninstall\">Odinstaluj</string>\n    <string name=\"module_install\">Instaluj</string>\n    <string name=\"install\">Instaluj</string>\n    <string name=\"reboot\">Uruchom ponownie</string>\n    <string name=\"settings\">Ustawienia</string>\n    <string name=\"reboot_userspace\">Miękki restart</string>\n    <string name=\"reboot_recovery\">Restart do trybu Recovery</string>\n    <string name=\"reboot_bootloader\">Restart do trybu Bootloader</string>\n    <string name=\"reboot_download\">Restart do trybu Download</string>\n    <string name=\"reboot_edl\">Restart do trybu EDL</string>\n    <string name=\"about\">Informacje</string>\n    <string name=\"module_uninstall_confirm\">Czy na pewno chcesz odinstalować moduł %s?</string>\n    <string name=\"module_uninstall_success\">Odinstalowano %s</string>\n    <string name=\"module_uninstall_failed\">Nie udało się odinstalować: %s</string>\n    <string name=\"module_version\">Wersja</string>\n    <string name=\"module_author\">Autor</string>\n    <string name=\"show_system_apps\">Pokaż aplikacje systemowe</string>\n    <string name=\"send_log\">Wyślij logi</string>\n    <string name=\"safe_mode\">Tryb bezpieczny</string>\n    <string name=\"reboot_to_apply\">Uruchom ponownie, aby zastosować zmiany</string>\n    <string name=\"module_magisk_conflict\">Moduły są niedostępne z powodu konfliktu z Magiskiem!</string>\n    <string name=\"home_learn_kernelsu\">Poznaj KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Dowiedz się jak zainstalować KernelSU i jak korzystać z modułów.</string>\n    <string name=\"home_support_title\">Wesprzyj nas</string>\n    <string name=\"home_support_content\">KernelSU jest i zawsze będzie darmowy oraz otwarty. Niemniej jednak możesz nam pokazać, że Ci zależy, wysyłając darowiznę.</string>\n    <string name=\"about_source_code\"><![CDATA[Przejrzyj kod źródłowy na %1$s<br/>Dołącz do kanału %2$s]]></string>\n    <string name=\"profile\" translatable=\"false\">Profil aplikacji</string>\n    <string name=\"profile_default\">Domyślny</string>\n    <string name=\"profile_template\">Szablon</string>\n    <string name=\"profile_custom\">Własny</string>\n    <string name=\"profile_name\">Nazwa profilu</string>\n    <string name=\"profile_namespace\">Przestrzeń nazw montowania</string>\n    <string name=\"profile_namespace_inherited\">Odziedziczona</string>\n    <string name=\"profile_namespace_global\">Globalna</string>\n    <string name=\"profile_namespace_individual\">Indywidualna</string>\n    <string name=\"profile_groups\">Grupy</string>\n    <string name=\"profile_capabilities\">Uprawnienia</string>\n    <string name=\"profile_selinux_context\">Kontekst SELinux</string>\n    <string name=\"profile_umount_modules\">Odmontuj moduły</string>\n    <string name=\"failed_to_update_app_profile\">Nie udało się zaktualizować profilu aplikacji dla %s</string>\n    <string name=\"settings_umount_modules_default\">Domyślnie odmontowuj moduły</string>\n    <string name=\"settings_umount_modules_default_summary\">Globalna wartość domyślna opcji \\\"Odmontuj moduły\\\" w profilu aplikacji. Jeśli jest włączona, wycofuje wszystkie modyfikacje dokonane przez moduły dla aplikacji, które nie mają ustawionego profilu.</string>\n    <string name=\"profile_umount_modules_summary\">Włączenie tej opcji umożliwi KernelSU przywrócenie wszelkich zmodyfikowanych plików przez moduły dla tej aplikacji.</string>\n    <string name=\"profile_selinux_domain\">Domena</string>\n    <string name=\"profile_selinux_rules\">Reguły</string>\n    <string name=\"module_update\">Zaktualizuj</string>\n    <string name=\"module_downloading\">Pobieranie modułu: %s</string>\n    <string name=\"module_start_downloading\">Rozpocznij pobieranie: %s</string>\n    <string name=\"new_version_available\">Nowa wersja %s jest dostępna. Kliknij, aby zaktualizować!</string>\n    <string name=\"launch_app\">Uruchom</string>\n    <string name=\"force_stop_app\">Wymuś zatrzymanie</string>\n    <string name=\"restart_app\">Restartuj</string>\n    <string name=\"failed_to_update_sepolicy\">Nie udało się zaktualizować reguł SELinux dla %s</string>\n    <string name=\"require_kernel_version\">Obecna wersja KernelSU %1$d jest zbyt stara, aby menedżer działał poprawnie. Prosimy o aktualizację do wersji %2$d lub nowszej!</string>\n    <string name=\"module_changelog\">Dziennik zmian</string>\n    <string name=\"enable_web_debugging\">Debugowanie WebView</string>\n    <string name=\"enable_web_debugging_summary\">Może być użyte do debugowania WebUI. Włącz tylko w razie potrzeby.</string>\n    <string name=\"select_file_tip\">Obraz partycji %1$s jest zalecany</string>\n    <string name=\"select_kmi\">Wybierz KMI</string>\n    <string name=\"install_next\">Dalej</string>\n    <string name=\"direct_install\">Instalacja bezpośrednia (zalecane)</string>\n    <string name=\"select_file\">Wybierz plik</string>\n    <string name=\"install_inactive_slot\">Zainstaluj do nieaktywnego slotu (po aktualizcji OTA)</string>\n    <string name=\"install_inactive_slot_warning\">Po ponownym uruchomieniu Twoje urządzenie zostanie **ZMUSZONE** do uruchomia się z obecnie nieaktywnego slotu!\n\\nUżyj tej opcji dopiero po zakończeniu aktualizacji OTA.\n\\nCzy chcesz kontynuować?</string>\n    <string name=\"app_profile_template_create\">Stwórz szablon</string>\n    <string name=\"app_profile_template_edit\">Edytuj szablon</string>\n    <string name=\"app_profile_template_name\">Nazwa</string>\n    <string name=\"app_profile_template_description\">Opis</string>\n    <string name=\"app_profile_template_save\">Zapisz</string>\n    <string name=\"app_profile_template_delete\">Usuń</string>\n    <string name=\"app_profile_import_export\">Importuj/Eksportuj</string>\n    <string name=\"app_profile_import_from_clipboard\">Importuj ze schowka</string>\n    <string name=\"app_profile_export_to_clipboard\">Eksportuj do schowka</string>\n    <string name=\"app_profile_template_export_empty\">Nie można znaleźć lokalnego szablonu do eksportu!</string>\n    <string name=\"app_profile_template_import_success\">Zaimportowano pomyślnie</string>\n    <string name=\"app_profile_template_save_failed\">Nie udało się zapisać szablonu</string>\n    <string name=\"app_profile_template_import_empty\">Schowek jest pusty!</string>\n    <string name=\"settings_profile_template_summary\">Zarządzaj lokalnym i internetowym szablonem profilu aplikacji.</string>\n    <string name=\"app_profile_template_view\">Zobacz szablon</string>\n    <string name=\"app_profile_template_id_invalid\">Błędny identyfikator szablonu</string>\n    <string name=\"settings_profile_template\">Szablon profilu aplikacji</string>\n    <string name=\"app_profile_template_id\">Identyfikator</string>\n    <string name=\"app_profile_template_id_exist\">Szablon o takim identyfikatorze już istnieje!</string>\n    <string name=\"grant_root_failed\">Nie udało się przyznać roota!</string>\n    <string name=\"open\">Otwórz</string>\n    <string name=\"settings_check_update\">Wyszukaj aktualizacje</string>\n    <string name=\"settings_check_update_summary\">Wyszukuj aktualizacje automatycznie przy otwieraniu aplikacji.</string>\n    <string name=\"settings_uninstall_permanent\">Odinstaluj zupełnie</string>\n    <string name=\"settings_restore_stock_image\">Przywróć obraz fabryczny</string>\n    <string name=\"settings_uninstall_temporary\">Odinstaluj tymczasowo</string>\n    <string name=\"settings_uninstall\">Odinstaluj</string>\n    <string name=\"settings_uninstall_temporary_message\">Tymczasowo odinstaluj KernelSU, przywróć do oryginalnego stanu po następnym ponownym uruchomieniu.</string>\n    <string name=\"settings_uninstall_permanent_message\">Całkowite i trwałe odinstalowanie KernelSU (root i wszystkich modułów).</string>\n    <string name=\"settings_restore_stock_image_message\">Przywróć obraz fabryczny (jeśli istnieje kopia zapasowa), zwykle używany przed OTA; jeśli chcesz odinstalować KernelSU, użyj opcji \\\"Odinstaluj całkowicie\\\".</string>\n    <string name=\"flashing\">Flashowanie</string>\n    <string name=\"flash_success\">Flashowanie ukończone pomyślnie</string>\n    <string name=\"flash_failed\">Flashowanie nieudane</string>\n    <string name=\"selected_lkm\">Wybrano LKM: %s</string>\n    <string name=\"save_log\">Zapisz dzienniki</string>\n    <string name=\"action\">Akcja</string>\n    <string name=\"log_saved\">Dzienniki zapisane</string>\n    <string name=\"module_sort_action_first\">Działania najpierw</string>\n    <string name=\"module_sort_enabled_first\">Włączone najpierw</string>\n    <string name=\"confirm\">Potwierdź</string>\n    <string name=\"module_install_prompt_with_name\">Zainstalowane zostaną następujące moduły: %1$s</string>\n    <string name=\"su_not_allowed\">Nie można przyznać dostępu superużytkownika do %s</string>\n    <string name=\"install_upload_lkm_file\">Użyj lokalnego pliku LKM</string>\n    <string name=\"install_only_support_ko_file\">Obsługiwane są wyłącznie pliki .ko</string>\n    <string name=\"processing\">Przetwarzanie…</string>\n    <string name=\"refresh_pulling\">Przeciągnij w dół, aby odświeżyć</string>\n    <string name=\"refresh_release\">Uwolnij, aby odświeżyć</string>\n    <string name=\"refresh_refresh\">Odświeżanie…</string>\n    <string name=\"refresh_complete\">Odświeżono pomyślnie</string>\n    <string name=\"settings_module_check_update\">Sprawdź aktualizację modułów</string>\n    <string name=\"settings_sucompat\">Przekieruj binarkę su</string>\n    <string name=\"settings_sucompat_summary\">Pozwala aplikacjom z uprawnieniami Superużytkownika w profilu aplikacji uzyskać shell superużytkownika przez wykonanie /system/bin/su; skuteczne tylko dla nowych procesów.</string>\n    <string name=\"settings_kernel_umount\">Odmontowanie jądra</string>\n    <string name=\"settings_kernel_umount_summary\">Zachowanie odmontowania na poziomie jądra kontrolowane przez KernelSU</string>\n    <string name=\"app_profile_affects_following_apps\">Wpływa na następujące aplikacje</string>\n    <string name=\"install_select_partition\">Wybierz partycję</string>\n    <string name=\"group_contains_apps\">Zawiera %d aplikacji</string>\n    <string name=\"metamodule_uninstall_confirm\">Czy na pewno chcesz odinstalować moduł %s? Ta czynność wpłynie na wszystkie moduły, a niektóre funkcje zapewniane przez moduł meta (takie jak montowanie) przestaną działać.</string>\n    <string name=\"undo\">Cofnij</string>\n    <string name=\"module_undo_uninstall_success\">Pomyślnie anulowano odinstalowywanie %s</string>\n    <string name=\"module_undo_uninstall_failed\">Nie udało się cofnąć dezinstalacji: %s</string>\n    <string name=\"feature_status_unsupported_summary\">Jądro nie obsługuje tej funkcji.</string>\n    <string name=\"feature_status_managed_summary\">Ta funkcja jest zarządzana przez moduł.</string>\n    <string name=\"settings_theme\">Motyw</string>\n    <string name=\"settings_theme_summary\">Wybierz tryb motywu aplikacji.</string>\n    <string name=\"settings_theme_mode_system\">Taki jak w systemie</string>\n    <string name=\"settings_theme_mode_light\">Jasny</string>\n    <string name=\"settings_theme_mode_dark\">Ciemny</string>\n    <string name=\"settings_key_color\">Kolor przycisków</string>\n    <string name=\"settings_key_color_default\">Domyślny</string>\n    <string name=\"color_blue\">Niebieski</string>\n    <string name=\"color_red\">Czerwony</string>\n    <string name=\"color_green\">Zielony</string>\n    <string name=\"color_purple\">Purpurowy</string>\n    <string name=\"color_orange\">Pomarańczowy</string>\n    <string name=\"color_teal\">Turkusowy</string>\n    <string name=\"color_pink\">Różowy</string>\n    <string name=\"color_brown\">Brązowy</string>\n    <string name=\"module_repos\">Repozytoria</string>\n    <string name=\"module_repos_sort_name\">Nazwa (A → Z)</string>\n    <string name=\"module_repos_source_code\">Kod źródłowy</string>\n    <string name=\"home_gki_warning\">Począwszy od wersji 3.0.0, tryb pracy GKI będzie używany wyłącznie w środowiskach testowych. Nie zalecamy jego stosowania w codziennym użytkowaniu, a pliki obrazów nie będą już udostępniane.</string>\n    <string name=\"network_offline\">Brak połączenia z siecią</string>\n    <string name=\"network_retry\">Ponów</string>\n    <string name=\"tab_readme\">README</string>\n    <string name=\"tab_releases\">Wydania</string>\n    <string name=\"tab_info\">Informacje</string>\n    <string name=\"safe_mode_module_disabled\">Instalacja modułów jest wyłączona w trybie awaryjnym</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-pt/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_not_installed\">Não instalado</string>\n    <string name=\"home\">Início</string>\n    <string name=\"home_click_to_install\">Clique para instalar</string>\n    <string name=\"home_working\">Funcionando</string>\n    <string name=\"home_working_version\">Versão: %d</string>\n    <string name=\"home_kernel\">Kernel</string>\n    <string name=\"install\">Instalar</string>\n    <string name=\"home_unsupported\">Sem suporte</string>\n    <string name=\"home_unsupported_reason\">KernelSU suporta apenas kernels GKI agora</string>\n    <string name=\"home_selinux_status\">Status do SELinux</string>\n    <string name=\"home_manager_version\">Versão do aplicativo</string>\n    <string name=\"module_failed_to_disable\">Falha ao desativar o módulo: %s</string>\n    <string name=\"home_fingerprint\">Impressão digital</string>\n    <string name=\"selinux_status_disabled\">Desabilitado</string>\n    <string name=\"selinux_status_enforcing\">Impondo</string>\n    <string name=\"selinux_status_permissive\">Permissivo</string>\n    <string name=\"selinux_status_unknown\">Desconhecido</string>\n    <string name=\"superuser\">Super Usuário</string>\n    <string name=\"module_failed_to_enable\">Falha ao ativar o módulo: %s</string>\n    <string name=\"module_empty\">Nenhum módulo instalado</string>\n    <string name=\"uninstall\">Desinstalar</string>\n    <string name=\"module\">Modulos</string>\n    <string name=\"reboot\">Reiniciar</string>\n    <string name=\"module_install\">Instalar</string>\n    <string name=\"reboot_userspace\">Reinicialização Suave</string>\n    <string name=\"settings\">Configurações</string>\n    <string name=\"reboot_bootloader\">Reinicializar modo Bootloader</string>\n    <string name=\"reboot_recovery\">Reiniciar modo recuperação</string>\n    <string name=\"module_uninstall_failed\">Falha ao desinstalar: %s</string>\n    <string name=\"module_version\">Versão</string>\n    <string name=\"module_author\">Autor</string>\n    <string name=\"reboot_download\">Reiniciar para baixar</string>\n    <string name=\"reboot_edl\">Reiniciar em EDL</string>\n    <string name=\"module_uninstall_confirm\">Tem certeza de que deseja desinstalar o módulo %s\\?</string>\n    <string name=\"about\">Sobre</string>\n    <string name=\"send_log\">Enviar log</string>\n    <string name=\"module_uninstall_success\">%s desinstalado</string>\n    <string name=\"show_system_apps\">Mostrar aplicativos do sistema</string>\n    <string name=\"home_click_to_learn_kernelsu\">Aprenda a instalar o KernelSU e usar os módulos</string>\n    <string name=\"home_support_content\">O KernelSU é, e sempre será, gratuito e de código aberto. No entanto, você pode nos mostrar que se importa fazendo uma doação.</string>\n    <string name=\"about_source_code\">Veja o código-fonte em %1$s<br/>\nJunte-se ao nosso canal %2$s</string>\n    <string name=\"profile_namespace_individual\">Individual</string>\n    <string name=\"profile_namespace_global\">Global</string>\n    <string name=\"profile_namespace_inherited\">Herdado</string>\n    <string name=\"profile_default\">Padrão</string>\n    <string name=\"profile_template\">Modelo</string>\n    <string name=\"profile_custom\">Personalizado</string>\n    <string name=\"profile_name\">Nome do perfil</string>\n    <string name=\"profile_namespace\">Montar namespace</string>\n    <string name=\"safe_mode\">Modo de segurança</string>\n    <string name=\"reboot_to_apply\">Reinicie para entrar em vigor</string>\n    <string name=\"home_learn_kernelsu\">Aprender KernelSU</string>\n    <string name=\"module_magisk_conflict\">Os módulos estão desativados porque estão em conflito com os do Magisk!</string>\n    <string name=\"home_support_title\">Apoie-nos</string>\n    <string name=\"profile_groups\">Grupos</string>\n    <string name=\"profile_capabilities\">Capacidades</string>\n    <string name=\"profile_selinux_context\">contexto SELinux</string>\n    <string name=\"profile_selinux_domain\">Domínio</string>\n    <string name=\"module_update\">Atualização</string>\n    <string name=\"profile_umount_modules\">Desativar modulos</string>\n    <string name=\"failed_to_update_app_profile\">Falha ao atualizar o perfil do aplicativo para %s</string>\n    <string name=\"settings_umount_modules_default\">Módulos desativados por padrão</string>\n    <string name=\"settings_umount_modules_default_summary\">O valor padrão global para \\\"Módulos Umount\\\" em Perfis de Aplicativos. Se ativado, removerá todas as modificações de módulo do sistema para aplicativos que não possuem um Perfil definido.</string>\n    <string name=\"profile_selinux_rules\">Regras</string>\n    <string name=\"profile_umount_modules_summary\">Ativar esta opção permitirá que o KernelSU restaure quaisquer arquivos modificados pelos módulos para este aplicativo.</string>\n    <string name=\"module_start_downloading\">Iniciar o download: %s</string>\n    <string name=\"module_downloading\">Baixando módulo: %s</string>\n    <string name=\"failed_to_update_sepolicy\">Falha ao atualizar as regras do SELinux para: %s</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"restart_app\">Reiniciar</string>\n    <string name=\"launch_app\">Lançar</string>\n    <string name=\"force_stop_app\">Forçar parada</string>\n    <string name=\"new_version_available\">Nova versão: %s está disponível, clique para baixar</string>\n    <string name=\"require_kernel_version\">A versão atual do KernelSU %1$d é muito baixa para o gerenciador funcionar corretamente. Atualize para a versão %2$d ou superior!</string>\n    <string name=\"save_log\">Salvar Registros</string>\n    <string name=\"settings_sucompat\">Redirecionar binário su</string>\n    <string name=\"settings_sucompat_summary\">Permite que apps com permissão de Superutilizador no Perfil da App obtenham um shell de superutilizador executando /system/bin/su; eficaz apenas para novos processos.</string>\n    <string name=\"settings_kernel_umount\">Desmontar kernel</string>\n    <string name=\"settings_kernel_umount_summary\">Comportamento de desmontagem ao nível do kernel controlado pelo KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-pt-rBR/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Início</string>\n    <string name=\"home_not_installed\">Não instalado</string>\n    <string name=\"home_click_to_install\">Clique para instalar</string>\n    <string name=\"home_working\">Em execução</string>\n    <string name=\"home_working_version\">Versão: %d</string>\n    <string name=\"home_unsupported\">Sem suporte</string>\n    <string name=\"home_unsupported_reason\">KernelSU suporta apenas kernels GKI agora</string>\n    <string name=\"home_kernel\">Versão do kernel</string>\n    <string name=\"home_manager_version\">Versão do gerenciador</string>\n    <string name=\"home_fingerprint\">Impressão digital</string>\n    <string name=\"home_selinux_status\">Status do SELinux</string>\n    <string name=\"selinux_status_disabled\">Desativado</string>\n    <string name=\"selinux_status_enforcing\">Impondo</string>\n    <string name=\"selinux_status_permissive\">Permissivo</string>\n    <string name=\"selinux_status_unknown\">Desconhecido</string>\n    <string name=\"superuser\">SuperUsuário</string>\n    <string name=\"module_failed_to_enable\">Não foi possível ativar o módulo %s</string>\n    <string name=\"module_failed_to_disable\">Não foi possível desativar o módulo %s</string>\n    <string name=\"module_empty\">Nenhum módulo instalado</string>\n    <string name=\"module\">Módulo</string>\n    <string name=\"uninstall\">Desinstalar</string>\n    <string name=\"module_install\">Instalar</string>\n    <string name=\"install\">Instalar</string>\n    <string name=\"reboot\">Reiniciar</string>\n    <string name=\"settings\">Configurações</string>\n    <string name=\"reboot_userspace\">Reinicialização suave</string>\n    <string name=\"reboot_recovery\">Reiniciar em modo Recovery</string>\n    <string name=\"reboot_bootloader\">Reiniciar em modo Bootloader</string>\n    <string name=\"reboot_download\">Reiniciar em modo Download</string>\n    <string name=\"reboot_edl\">Reiniciar em modo EDL</string>\n    <string name=\"about\">Sobre</string>\n    <string name=\"module_uninstall_confirm\">Tem certeza que deseja desinstalar o módulo %s?</string>\n    <string name=\"module_uninstall_success\">%s desinstalado</string>\n    <string name=\"module_uninstall_failed\">Não foi possível desinstalar %s</string>\n    <string name=\"module_version\">Versão</string>\n    <string name=\"module_author\">Autor</string>\n    <string name=\"show_system_apps\">Mostrar apps do sistema</string>\n    <string name=\"send_log\">Reportar registros</string>\n    <string name=\"safe_mode\">Modo de segurança</string>\n    <string name=\"reboot_to_apply\">Reinicie para entrar em vigor</string>\n    <string name=\"module_magisk_conflict\">Os módulos estão indisponíveis devido a um conflito com Magisk!</string>\n    <string name=\"home_learn_kernelsu\">Saiba mais sobre o KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/pt_BR/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Saiba como instalar o KernelSU e usar os módulos</string>\n    <string name=\"home_support_title\">Apoie-nos</string>\n    <string name=\"home_support_content\">KernelSU sempre foi e sempre será, gratuito e de código aberto. No entanto, você pode nos ajudar enviando uma pequena doação.</string>\n    <string name=\"about_source_code\"><![CDATA[Veja o código-fonte no %1$s<br/>Junte-se ao nosso canal do %2$s]]></string>\n    <string name=\"profile\" translatable=\"false\">Perfil do Aplicativo</string>\n    <string name=\"profile_default\">Padrão</string>\n    <string name=\"profile_template\">Modelo</string>\n    <string name=\"profile_custom\">Personalizado</string>\n    <string name=\"profile_name\">Nome do perfil</string>\n    <string name=\"profile_namespace\">Montar namespace</string>\n    <string name=\"profile_namespace_inherited\">Herdado</string>\n    <string name=\"profile_namespace_global\">Global</string>\n    <string name=\"profile_namespace_individual\">Individual</string>\n    <string name=\"profile_groups\">Grupos</string>\n    <string name=\"profile_capabilities\">Capacidades</string>\n    <string name=\"profile_selinux_context\">Contexto do SELinux</string>\n    <string name=\"profile_umount_modules\">Desmontar módulos</string>\n    <string name=\"failed_to_update_app_profile\">Falha ao atualizar o Perfil do Aplicativo para %s</string>\n    <string name=\"settings_umount_modules_default\">Desmontar módulos por padrão</string>\n    <string name=\"settings_umount_modules_default_summary\">O valor padrão global para \\\"Desmontar módulos\\\" em Perfil do Aplicativo. Se ativado, ele removerá todas as modificações do módulo no sistema para apps que não possuem um perfil definido.</string>\n    <string name=\"profile_umount_modules_summary\">Ativar esta opção permitirá que o KernelSU restaure quaisquer arquivos modificados pelos módulos para este app.</string>\n    <string name=\"profile_selinux_domain\">Domínio</string>\n    <string name=\"profile_selinux_rules\">Regras</string>\n    <string name=\"module_update\">Atualizar</string>\n    <string name=\"module_downloading\">Baixando módulo %s</string>\n    <string name=\"module_start_downloading\">Começando a baixar %s</string>\n    <string name=\"new_version_available\">Nova versão %s está disponível, clique para atualizar.</string>\n    <string name=\"launch_app\">Iniciar</string>\n    <string name=\"force_stop_app\">Forçar parada</string>\n    <string name=\"restart_app\">Reiniciar</string>\n    <string name=\"failed_to_update_sepolicy\">Falha ao atualizar as regras do SELinux para %s</string>\n    <string name=\"require_kernel_version\">A versão atual do KernelSU %1$d é muito baixa para o gerenciador funcionar corretamente. Por favor, atualize para a versão %2$d ou superior!</string>\n    <string name=\"module_changelog\">Registro de alterações</string>\n    <string name=\"app_profile_template_import_success\">Importado com sucesso</string>\n    <string name=\"app_profile_export_to_clipboard\">Exportar para a área de transferência</string>\n    <string name=\"app_profile_template_export_empty\">Não foi possível encontrar o modelo local para exportar!</string>\n    <string name=\"app_profile_template_id_exist\">O ID do modelo já existe!</string>\n    <string name=\"app_profile_import_from_clipboard\">Importar da área de transferência</string>\n    <string name=\"app_profile_template_name\">Nome</string>\n    <string name=\"app_profile_template_id_invalid\">ID do modelo inválido</string>\n    <string name=\"app_profile_template_create\">Criar modelo</string>\n    <string name=\"app_profile_import_export\">Importar/Exportar</string>\n    <string name=\"app_profile_template_save_failed\">Falha ao salvar o modelo</string>\n    <string name=\"app_profile_template_edit\">Editar modelo</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"settings_profile_template\">Modelo do Perfil do Aplicativo</string>\n    <string name=\"app_profile_template_description\">Descrição</string>\n    <string name=\"app_profile_template_save\">Salvar</string>\n    <string name=\"settings_profile_template_summary\">Gerencie o modelo local e online do Perfil do Aplicativo</string>\n    <string name=\"app_profile_template_delete\">Excluir</string>\n    <string name=\"app_profile_template_import_empty\">A área de transferência está vazia!</string>\n    <string name=\"app_profile_template_view\">Ver modelo</string>\n    <string name=\"settings_check_update\">Verificar por atualizações</string>\n    <string name=\"settings_check_update_summary\">Verifique automaticamente se há atualizações ao abrir o app</string>\n    <string name=\"grant_root_failed\">Falha ao conceder acesso root!</string>\n    <string name=\"open\">Abrir</string>\n    <string name=\"enable_web_debugging\">Ativar depuração do WebView</string>\n    <string name=\"enable_web_debugging_summary\">Pode ser usado para depurar o WebUI. Por favor, ative somente quando necessário.</string>\n    <string name=\"select_file\">Selecione um arquivo</string>\n    <string name=\"direct_install\">Instalação direta (recomendada)</string>\n    <string name=\"install_inactive_slot\">Instalar no slot inativo (após o OTA)</string>\n    <string name=\"install_inactive_slot_warning\">Seu dispositivo será **FORÇADO** a inicializar no slot inativo atual após uma reinicialização!\n\\nSó use esta opção após a conclusão do OTA.\n\\nDeseja continuar?</string>\n    <string name=\"install_next\">Próximo</string>\n    <string name=\"select_file_tip\">A imagem da partição %1$s é recomendada</string>\n    <string name=\"select_kmi\">Selecionar KMI</string>\n    <string name=\"settings_uninstall\">Desinstalar</string>\n    <string name=\"settings_uninstall_temporary\">Desinstalar temporariamente</string>\n    <string name=\"settings_uninstall_permanent\">Desinstalar permanentemente</string>\n    <string name=\"settings_restore_stock_image\">Restaurar imagem de fábrica</string>\n    <string name=\"settings_restore_stock_image_message\">Restaure a imagem de fábrica (se existir um backup), geralmente usada antes do OTA. Se você precisar desinstalar o KernelSU, use \\\"Desinstalar permanentemente\\\".</string>\n    <string name=\"settings_uninstall_temporary_message\">Desinstale temporariamente o KernelSU e restaure ao estado original após a próxima reinicialização.</string>\n    <string name=\"settings_uninstall_permanent_message\">Desinstale o KernelSU (root e todos os módulos) completamente e permanentemente.</string>\n    <string name=\"selected_lkm\">LKM selecionado: %s</string>\n    <string name=\"flash_failed\">Flash falhou</string>\n    <string name=\"flashing\">Flashando</string>\n    <string name=\"flash_success\">Flash bem-sucedido</string>\n    <string name=\"save_log\">Salvar registros</string>\n    <string name=\"action\">Ação</string>\n    <string name=\"log_saved\">Registros salvos</string>\n    <string name=\"module_sort_action_first\">Ordenar (Ação primeiro)</string>\n    <string name=\"module_sort_enabled_first\">Ordenar (Ativado primeiro)</string>\n    <string name=\"confirm\">Confirmar</string>\n    <string name=\"su_not_allowed\">Não foi possível conceder acesso de SuperUsuário a %s</string>\n    <string name=\"module_install_prompt_with_name\">Os seguintes módulos serão instalados: %1$s</string>\n    <string name=\"settings_sucompat\">Redirecionar binário su</string>\n    <string name=\"settings_sucompat_summary\">Permite que aplicativos com permissão de Superusuário no Perfil do App obtenham um shell de superusuário executando /system/bin/su; efetivo apenas para novos processos.</string>\n    <string name=\"settings_kernel_umount\">Desmontar kernel</string>\n    <string name=\"settings_kernel_umount_summary\">Comportamento de desmontagem no nível do kernel controlado pelo KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-ro/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Acasă</string>\n    <string name=\"home_not_installed\">Nu este instalat</string>\n    <string name=\"home_click_to_install\">Click pentru a instala</string>\n    <string name=\"home_working\">Funcționează</string>\n    <string name=\"home_working_version\">Versiune: %d</string>\n    <string name=\"home_unsupported\">Necompatibil</string>\n    <string name=\"home_unsupported_reason\">KernelSU suportă doar nuclee GKI acum</string>\n    <string name=\"home_kernel\">Nucleu</string>\n    <string name=\"home_manager_version\">Versiune Manager</string>\n    <string name=\"home_fingerprint\">Amprentă</string>\n    <string name=\"home_selinux_status\">Stare SELinux</string>\n    <string name=\"selinux_status_disabled\">Dezactivat</string>\n    <string name=\"selinux_status_enforcing\">Obligatoriu</string>\n    <string name=\"selinux_status_permissive\">Permisiv</string>\n    <string name=\"selinux_status_unknown\">Necunoscut</string>\n    <string name=\"superuser\">Super-Utilizator</string>\n    <string name=\"module_failed_to_enable\">Activarea modulului %s a eșuat</string>\n    <string name=\"module_failed_to_disable\">Dezactivarea modulului %s a eșuat</string>\n    <string name=\"module_empty\">Niciun modul instalat</string>\n    <string name=\"module\">Module</string>\n    <string name=\"uninstall\">Dezinstalează</string>\n    <string name=\"module_install\">Instalează</string>\n    <string name=\"install\">Instalează</string>\n    <string name=\"reboot\">Repornește</string>\n    <string name=\"settings\">Setări</string>\n    <string name=\"reboot_userspace\">Repornire rapidă</string>\n    <string name=\"reboot_recovery\">Repornire în Recuperare</string>\n    <string name=\"reboot_bootloader\">Repornire în Bootloader</string>\n    <string name=\"reboot_download\">Repornire în Download</string>\n    <string name=\"reboot_edl\">Repornire în EDL</string>\n    <string name=\"about\">Despre</string>\n    <string name=\"module_uninstall_confirm\">Sigur dorești să dezinstalezi modulul %s?</string>\n    <string name=\"module_uninstall_success\">%s dezinstalat</string>\n    <string name=\"module_uninstall_failed\">Dezinstalare eșuată: %s</string>\n    <string name=\"module_version\">Versiune</string>\n    <string name=\"module_author\">Autor</string>\n    <string name=\"show_system_apps\">Arată aplicațiile de sistem</string>\n    <string name=\"send_log\">Raportează jurnal</string>\n    <string name=\"safe_mode\">Mod sigur</string>\n    <string name=\"reboot_to_apply\">Repornește pentru ca modificările să intre în vigoare</string>\n    <string name=\"module_magisk_conflict\">Modulele sunt dezactivate deoarece sunt în conflict cu cele ale Magisk-ului!</string>\n    <string name=\"home_learn_kernelsu\">Află mai multe despre KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Află cum să instalezi KernelSU și să utilizezi module</string>\n    <string name=\"home_support_title\">Suport</string>\n    <string name=\"home_support_content\">KernelSU este, și va fi întotdeauna, gratuit și cu codul sursă deschis. Cu toate acestea, ne poți arăta că îți pasă făcând o donație.</string>\n    <string name=\"about_source_code\"><![CDATA[Vezi codul sursă la %1$s<br/>Alătură-te canalului nostru %2$s]]></string>\n    <string name=\"profile_default\">Implicit</string>\n    <string name=\"profile_template\">Șablon</string>\n    <string name=\"profile_custom\">Personalizat</string>\n    <string name=\"profile_name\">Nume profil</string>\n    <string name=\"profile_namespace\">Montare spațiu de nume</string>\n    <string name=\"profile_namespace_inherited\">Moștenit</string>\n    <string name=\"profile_namespace_global\">Global</string>\n    <string name=\"profile_namespace_individual\">Individual</string>\n    <string name=\"profile_groups\">Grupuri</string>\n    <string name=\"profile_capabilities\">Capabilități</string>\n    <string name=\"profile_selinux_context\">Context SELinux</string>\n    <string name=\"profile_umount_modules\">Module u-montate</string>\n    <string name=\"failed_to_update_app_profile\">Nu s-a putut actualiza profilul aplicației pentru %s</string>\n    <string name=\"settings_umount_modules_default\">U-montează modulele în mod implicit</string>\n    <string name=\"settings_umount_modules_default_summary\">Valoarea implicită globală pentru „Module u-montate” în Profilurile aplicațiilor. Dacă este activat, va elimina toate modificările modulelor aduse sistemului pentru aplicațiile care nu au un profil setat.</string>\n    <string name=\"profile_umount_modules_summary\">Activarea acestei opțiuni va permite KernelSU să restaureze orice fișiere modificate de către modulele pentru această aplicație.</string>\n    <string name=\"profile_selinux_domain\">Domeniu</string>\n    <string name=\"profile_selinux_rules\">Reguli</string>\n    <string name=\"module_update\">Actualizează</string>\n    <string name=\"module_downloading\">Se descarcă modulul: %s</string>\n    <string name=\"module_start_downloading\">Începe descărcarea: %s</string>\n    <string name=\"new_version_available\">Versiune nouă: %s este disponibilă, clic pentru a actualiza</string>\n    <string name=\"failed_to_update_sepolicy\">Nu s-au reușit actualizările regulilor SELinux pentru: %s</string>\n    <string name=\"launch_app\">Lansare</string>\n    <string name=\"force_stop_app\">Oprire forțată</string>\n    <string name=\"restart_app\">Repornește</string>\n    <string name=\"require_kernel_version\">Versiunea actuală a KernelSU %1$d este prea mică pentru ca managerul să funcționeze corect. Actualizează la versiunea %2$d sau o versiune superioară!</string>\n    <string name=\"module_changelog\">Jurnalul modificărilor</string>\n    <string name=\"app_profile_template_import_success\">Importat cu succes</string>\n    <string name=\"app_profile_export_to_clipboard\">Export în clipboard</string>\n    <string name=\"app_profile_template_export_empty\">Nu există șabloane locale de exportat!</string>\n    <string name=\"app_profile_template_id_exist\">ID-ul șablonului există deja!</string>\n    <string name=\"app_profile_import_from_clipboard\">Import din clipboard</string>\n    <string name=\"app_profile_template_name\">Nume</string>\n    <string name=\"app_profile_template_id_invalid\">ID șablon nevalid</string>\n    <string name=\"app_profile_template_create\">Creează un șablon</string>\n    <string name=\"app_profile_import_export\">Import/Export</string>\n    <string name=\"app_profile_template_save_failed\">Nu s-a salvat șablonul</string>\n    <string name=\"app_profile_template_edit\">Editează șablonul</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"settings_profile_template\">Șablon de profil al aplicației</string>\n    <string name=\"app_profile_template_description\">Descriere</string>\n    <string name=\"app_profile_template_save\">Salvează</string>\n    <string name=\"settings_profile_template_summary\">Gestionează șablonul local și online al Profilului aplicației</string>\n    <string name=\"app_profile_template_delete\">Șterge</string>\n    <string name=\"app_profile_template_import_empty\">Clipboard-ul este gol!</string>\n    <string name=\"app_profile_template_view\">Vizualizare șablon</string>\n    <string name=\"settings_check_update\">Verifică actualizarea</string>\n    <string name=\"settings_check_update_summary\">Se verifică automat actualizările când deschizi aplicația</string>\n    <string name=\"enable_web_debugging\">Activează depanarea WebView</string>\n    <string name=\"enable_web_debugging_summary\">Poate fi folosit pentru a depana WebUI, activează numai când este necesar.</string>\n    <string name=\"grant_root_failed\">Nu s-a acordat acces root!</string>\n    <string name=\"open\">Deschide</string>\n    <string name=\"select_file_tip\">Se recomandă imaginea partiției %1$s</string>\n    <string name=\"install_next\">Înainte</string>\n    <string name=\"install_inactive_slot_warning\">Dispozitivul va fi **FORȚAT** să pornească în slot-ul inactiv curent după o repornire!\n\\nFolosește această opțiune numai după finisarea OTA.\n\\nContinui?</string>\n    <string name=\"select_kmi\">Selectează KMI</string>\n    <string name=\"direct_install\">Instalare directă (recomandat)</string>\n    <string name=\"select_file\">Selectează un fișier</string>\n    <string name=\"install_inactive_slot\">Instalează într-un slot inactiv (după OTA)</string>\n    <string name=\"settings_uninstall\">Dezinstalează</string>\n    <string name=\"settings_restore_stock_image\">Restaurare imagine stoc</string>\n    <string name=\"settings_uninstall_temporary_message\">Dezinstalează temporar KernelSU, se revine la starea inițială după următoarea repornire.</string>\n    <string name=\"selected_lkm\">Lkm selectat: %s</string>\n    <string name=\"settings_uninstall_temporary\">Dezinstalează temporar</string>\n    <string name=\"settings_uninstall_permanent\">Dezinstalează definitiv</string>\n    <string name=\"settings_uninstall_permanent_message\">Dezinstalare KernelSU (Root și toate modulele) complet și permanent.</string>\n    <string name=\"settings_restore_stock_image_message\">Restaurează imaginea stoc din fabrică (dacă există o copie de rezervă), utilizată de obicei înainte de OTA; dacă trebuie să dezinstalezi KernelSU, utilizează „Dezinstalare permanentă”.</string>\n    <string name=\"flashing\">Instalare</string>\n    <string name=\"flash_success\">Instalare reușită</string>\n    <string name=\"flash_failed\">Instalarea a eșuat</string>\n    <string name=\"save_log\">Salvează Jurnale</string>\n    <string name=\"settings_sucompat\">Redirecționare binar su</string>\n    <string name=\"settings_sucompat_summary\">Permite aplicațiilor cu permisiune de Superutilizator în Profilul Aplicației să obțină un shell de superutilizator executând /system/bin/su; eficient doar pentru procesele noi.</string>\n    <string name=\"settings_kernel_umount\">Demontare kernel</string>\n    <string name=\"settings_kernel_umount_summary\">Comportament de demontare la nivel de kernel controlat de KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-ru/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Главная</string>\n    <string name=\"home_not_installed\">Не установлен</string>\n    <string name=\"home_click_to_install\">Нажмите, чтобы установить</string>\n    <string name=\"home_working\">Работает</string>\n    <string name=\"home_working_version\">Версия: %d</string>\n    <!--Don't translate this string!-->\n    <string name=\"home_unsupported\">Не поддерживается</string>\n    <string name=\"home_unsupported_reason\">Теперь KernelSU поддерживает только ядра GKI, однако вы всё ещё можете пропатчить образ для GKI-устройств.</string>\n    <string name=\"home_kernel\">Версия ядра</string>\n    <string name=\"home_manager_version\">Версия менеджера</string>\n    <string name=\"home_fingerprint\">Подпись</string>\n    <string name=\"home_selinux_status\">Состояние SELinux</string>\n    <string name=\"selinux_status_disabled\">Выключен</string>\n    <string name=\"selinux_status_enforcing\">Принудительный</string>\n    <string name=\"selinux_status_permissive\">Разрешающий</string>\n    <string name=\"selinux_status_unknown\">Неизвестно</string>\n    <string name=\"superuser\">Суперюзеры</string>\n    <!--Don't translate this string!-->\n    <string name=\"module_failed_to_enable\">Не удалось включить модуль %s</string>\n    <string name=\"module_failed_to_disable\">Не удалось отключить модуль %s</string>\n    <string name=\"module_empty\">Нет установленных модулей</string>\n    <string name=\"module\">Модули</string>\n    <string name=\"uninstall\">Удалить</string>\n    <string name=\"module_install\">Установить</string>\n    <string name=\"install\">Установка</string>\n    <string name=\"reboot\">Перезагрузить</string>\n    <string name=\"settings\">Настройки</string>\n    <string name=\"reboot_userspace\">Мягкая перезагрузка</string>\n    <string name=\"reboot_recovery\">Перезагрузить в Recovery</string>\n    <string name=\"reboot_bootloader\">Перезагрузить в Bootloader</string>\n    <string name=\"reboot_download\">Перезагрузить в Download</string>\n    <string name=\"reboot_edl\">Перезагрузить в EDL</string>\n    <string name=\"about\">О приложении</string>\n    <string name=\"module_uninstall_confirm\">Вы уверены, что хотите удалить модуль %s?</string>\n    <string name=\"module_uninstall_success\">%s удалён</string>\n    <string name=\"module_uninstall_failed\">Не удалось удалить %s</string>\n    <string name=\"module_version\">Версия</string>\n    <string name=\"module_author\">Автор</string>\n    <string name=\"show_system_apps\">Показать системные приложения</string>\n    <string name=\"send_log\">Отправить логи</string>\n    <string name=\"safe_mode\">Безопасный режим</string>\n    <string name=\"reboot_to_apply\">Перезагрузите, чтобы изменения вступили в силу</string>\n    <string name=\"module_magisk_conflict\">Модули недоступны из-за конфликта с Magisk!</string>\n    <string name=\"home_learn_kernelsu\">Узнайте о KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/ru_RU/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Узнайте, как установить KernelSU и использовать модули.</string>\n    <string name=\"home_support_title\">Поддержите нас</string>\n    <string name=\"home_support_content\">KernelSU был и всегда будет бесплатным и открытым проектом. Однако Вы всегда можете поддержать нас, отправив небольшое пожертвование.</string>\n    <string name=\"about_source_code\"><![CDATA[Посмотреть исходный код на %1$s<br/>Присоединяйтесь к нашему %2$s каналу]]></string>\n    <string name=\"profile\" translatable=\"false\">App Profile</string>\n    <!--Don't translate this string!-->\n    <string name=\"profile_default\">По умолчанию</string>\n    <string name=\"profile_template\">Шаблон</string>\n    <string name=\"profile_custom\">Пользовательский</string>\n    <string name=\"profile_name\">Название профиля</string>\n    <string name=\"profile_namespace\">Монтировать пространство имен</string>\n    <string name=\"profile_namespace_inherited\">Унаследованный</string>\n    <string name=\"profile_namespace_global\">Глобальный</string>\n    <string name=\"profile_namespace_individual\">Индивидуальный</string>\n    <string name=\"profile_groups\">Группы</string>\n    <string name=\"profile_capabilities\">Возможности</string>\n    <string name=\"profile_selinux_context\">Контекст SELinux</string>\n    <string name=\"profile_umount_modules\">Размонтировать модули</string>\n    <string name=\"failed_to_update_app_profile\">Не удалось обновить App Profile для %s</string>\n    <string name=\"settings_umount_modules_default\">Размонтировать модули по умолчанию</string>\n    <string name=\"settings_umount_modules_default_summary\">Глобальное значение по умолчанию для \\\"Размонтировать модули\\\" в App Profile. При включении будут удалены все модификации модулей в системе для приложений, у которых не задан Profile.</string>\n    <string name=\"profile_umount_modules_summary\">Включение этой опции позволит KernelSU восстанавливать любые изменённые модулями файлы для данного приложения.</string>\n    <string name=\"profile_selinux_domain\">Домен</string>\n    <string name=\"profile_selinux_rules\">Правила</string>\n    <string name=\"module_update\">Обновить</string>\n    <string name=\"module_downloading\">Скачивание модуля: %s</string>\n    <string name=\"module_start_downloading\">Начало скачивания: %s</string>\n    <string name=\"new_version_available\">Новая версия: %s доступна, нажмите чтобы скачать!</string>\n    <string name=\"force_stop_app\">Остановить принудительно</string>\n    <string name=\"failed_to_update_sepolicy\">Не удалось обновить правила SELinux для %s</string>\n    <string name=\"launch_app\">Запустить</string>\n    <string name=\"restart_app\">Перезапустить</string>\n    <string name=\"require_kernel_version\">Текущая версия KernelSU %1$d слишком низкая для правильной работы менеджера. Пожалуйста, обновите до версии %2$d или выше!</string>\n    <string name=\"module_changelog\">Список изменений</string>\n    <string name=\"app_profile_template_import_success\">Успешный импорт</string>\n    <string name=\"app_profile_export_to_clipboard\">Экспортировать в буфер обмена</string>\n    <string name=\"app_profile_template_export_empty\">Нет локальных шаблонов для экспорта!</string>\n    <string name=\"app_profile_template_id_exist\">Шаблон с таким ID уже существует!</string>\n    <string name=\"app_profile_import_from_clipboard\">Импортировать из буфера обмена</string>\n    <string name=\"app_profile_template_name\">Название</string>\n    <string name=\"app_profile_template_id_invalid\">Неверный ID шаблона</string>\n    <string name=\"app_profile_template_create\">Создать шаблон</string>\n    <string name=\"app_profile_import_export\">Импорт/Экспорт</string>\n    <string name=\"app_profile_template_save_failed\">Не удалось сохранить шаблон</string>\n    <string name=\"app_profile_template_edit\">Редактирование шаблона</string>\n    <string name=\"app_profile_template_id\">Идентификационный номер</string>\n    <string name=\"settings_profile_template\">Шаблон профиля приложения</string>\n    <string name=\"app_profile_template_description\">Описание</string>\n    <string name=\"app_profile_template_save\">Сохранить</string>\n    <string name=\"settings_profile_template_summary\">Управление локальным и онлайн-шаблоном профиля приложения.</string>\n    <string name=\"app_profile_template_delete\">Удалить</string>\n    <string name=\"app_profile_template_import_empty\">Буфер обмена пуст!</string>\n    <string name=\"app_profile_template_view\">Просмотр шаблона</string>\n    <string name=\"settings_check_update\">Проверять наличие обновлений</string>\n    <string name=\"settings_check_update_summary\">Автоматическая проверка обновлений при открытии приложения.</string>\n    <string name=\"grant_root_failed\">Не удалось выдать root!</string>\n    <string name=\"open\">Открыть</string>\n    <string name=\"enable_web_debugging\">Отладка WebView</string>\n    <string name=\"enable_web_debugging_summary\">Используется для отладки WebUI. Пожалуйста, включайте только при необходимости.</string>\n    <string name=\"direct_install\">Прямая установка (Рекомендуется)</string>\n    <string name=\"install_inactive_slot\">Установка в неактивный слот (После OTA)</string>\n    <string name=\"install_next\">Далее</string>\n    <string name=\"select_file\">Выбрать файл</string>\n    <string name=\"install_inactive_slot_warning\">Ваше устройство будет **ПРИНУДИТЕЛЬНО** загружено в текущий неактивный слот после перезагрузки!\n\\n Используйте эту опцию только после завершения OTA.\n\\n Продолжить?</string>\n    <string name=\"select_kmi\">Выбрать KMI</string>\n    <string name=\"select_file_tip\">%1$s образ раздела рекомендуется</string>\n    <string name=\"settings_uninstall_temporary\">Удалить на время</string>\n    <string name=\"settings_uninstall_permanent_message\">Удалить KernelSU (root и все модули) полностью.</string>\n    <string name=\"settings_uninstall_permanent\">Удалить полностью</string>\n    <string name=\"settings_uninstall_temporary_message\">Временно удалить KernelSU, восстановить исходное состояние после следующей перезагрузки.</string>\n    <string name=\"settings_uninstall\">Удалить</string>\n    <string name=\"settings_restore_stock_image\">Восстановить сток образ</string>\n    <string name=\"settings_restore_stock_image_message\">Восстановить исходный заводской образ (если существует резервная копия), обычно используется перед OTA; если вам нужно удалить KernelSU, используйте «Удалить полностью».</string>\n    <string name=\"flash_success\">Установка выполнена</string>\n    <string name=\"flashing\">Установка</string>\n    <string name=\"flash_failed\">Установка не выполнена</string>\n    <string name=\"selected_lkm\">Выбран LKM: %s</string>\n    <string name=\"save_log\">Сохранить логи</string>\n    <string name=\"action\">Действие</string>\n    <string name=\"log_saved\">Логи сохранены</string>\n    <string name=\"module_sort_action_first\">Сначала с действием</string>\n    <string name=\"module_sort_enabled_first\">Сначала включённые</string>\n    <string name=\"module_install_prompt_with_name\">Будут установлены следующие модули: %1$s</string>\n    <string name=\"confirm\">Подтвердить</string>\n    <string name=\"su_not_allowed\">Не удалось предоставить права Суперпользователя к %s</string>\n    <string name=\"refresh_complete\">Обновлено успешно</string>\n    <string name=\"refresh_refresh\">Обновление…</string>\n    <string name=\"refresh_release\">Отпустите чтобы обновить</string>\n    <string name=\"refresh_pulling\">Потяните вниз чтобы обновить</string>\n    <string name=\"processing\">Обработка…</string>\n    <string name=\"install_upload_lkm_file\">Использовать локальный LKM файл</string>\n    <string name=\"install_only_support_ko_file\">Поддерживаются только .ko файлы</string>\n    <string name=\"settings_sucompat\">Классическая команда su</string>\n    <string name=\"settings_sucompat_summary\">Разрешить root-доступ через /system/bin/su в новых процессах.</string>\n    <string name=\"settings_kernel_umount\">Размонтирование модулей</string>\n    <string name=\"settings_kernel_umount_summary\">Размонтировать модули в ядре для профиля приложения</string>\n    <string name=\"settings_module_check_update\">Проверять обновления модулей</string>\n    <string name=\"install_select_partition\">Выбрать раздел</string>\n    <string name=\"app_profile_affects_following_apps\">Влияет на следующие приложения</string>\n    <string name=\"group_contains_apps\">Содержит %d приложений</string>\n    <string name=\"metamodule_uninstall_confirm\">Вы уверены, что хотите удалить модуль %s? Это действие повлияет на все модули, и некоторые функции метамодуля (например, монтирование) перестанут работать.</string>\n    <string name=\"undo\">Отменить</string>\n    <string name=\"module_undo_uninstall_failed\">не удалось отменить удаление: %s</string>\n    <string name=\"module_undo_uninstall_success\">Удаление %s успешно отменено</string>\n    <string name=\"settings_theme\">Тема</string>\n    <string name=\"settings_theme_summary\">Выберите режим темы приложения.</string>\n    <string name=\"settings_theme_mode_system\">Как в системе</string>\n    <string name=\"feature_status_managed_summary\">Эта функция управляется модулем.</string>\n    <string name=\"feature_status_unsupported_summary\">Ядро не поддерживает эту функцию.</string>\n    <string name=\"settings_key_color\">Ключевой цвет</string>\n    <string name=\"settings_key_color_default\">По умолчанию</string>\n    <string name=\"color_blue\">Синий</string>\n    <string name=\"color_red\">Красный</string>\n    <string name=\"color_green\">Зеленый</string>\n    <string name=\"color_purple\">Фиолетовый</string>\n    <string name=\"color_orange\">Оранжевый</string>\n    <string name=\"color_teal\">Бирюзовый</string>\n    <string name=\"color_pink\">Розовый</string>\n    <string name=\"color_brown\">Коричневый</string>\n    <string name=\"settings_theme_mode_light\">Светлая</string>\n    <string name=\"settings_theme_mode_dark\">Тёмная</string>\n    <string name=\"module_repos\">Репозиторий</string>\n    <string name=\"module_repos_sort_name\">Имя (А → Я)</string>\n    <string name=\"module_repos_source_code\">Исходный код</string>\n    <string name=\"home_gki_warning\">Начиная с версии 3.0.0, режим работы GKI будет использоваться только в тестовых средах. Мы не рекомендуем его для ежедневного использования, а image файлы больше предоставляться не будут.</string>\n    <string name=\"network_offline\">Нет подключения к сети</string>\n    <string name=\"network_retry\">Повторить</string>\n    <string name=\"tab_readme\">README</string>\n    <string name=\"tab_releases\">Релизы</string>\n    <string name=\"tab_info\">Инфо</string>\n    <string name=\"safe_mode_module_disabled\">В безопасном режиме установка модулей отключена</string>\n    <string name=\"module_action_success\">Действие модуля выполнено успешно.</string>\n    <string name=\"settings_mode_enable_by_default\">Включена (По умолчанию)</string>\n    <string name=\"settings_mode_disable_until_reboot\">Отключить до перезагрузки</string>\n    <string name=\"settings_mode_disable_always\">Всегда отключена</string>\n    <string name=\"module_shortcut_title\">Создать ярлык</string>\n    <string name=\"module_shortcut_name_label\">Название ярлыка</string>\n    <string name=\"module_shortcut_icon_pick\">Выбрать пользовательскую иконку</string>\n    <string name=\"module_shortcut_not_supported\">Лаунчер не поддерживает ярлыки на рабочем столе.</string>\n    <string name=\"module_shortcut_created\">Ярлык создан на рабочем столе.</string>\n    <string name=\"module_shortcut_updated\">Ярлык обновлен.</string>\n    <string name=\"module_shortcut_delete\">Удалить ярлык</string>\n    <string name=\"module_shortcut_permission_tip_xiaomi\">Пожалуйста, включите разрешение «Ярлыки на рабочем столе» для этого приложения в настройках Xiaomi.</string>\n    <string name=\"module_shortcut_permission_tip_oppo\">Пожалуйста, включите разрешение «Ярлыки на рабочем столе» для этого приложения в настройках OPPO.</string>\n    <string name=\"module_shortcut_permission_tip_default\">Если создание ярлыка не удалось, пожалуйста, включите разрешение на создание ярлыков на рабочем столе для этого приложения в системных настройках.</string>\n    <string name=\"no_such_module\">Модуль %s не существует</string>\n    <string name=\"module_unavailable\">Модуль %s отключен, обновляется или ожидает удаления</string>\n    <string name=\"select_file_tip_nogki\">Пожалуйста, выберите файл образа GKI устройства, который вы хотите пропатчить</string>\n    <string name=\"current_kmi\">Версия KMI этого устройства: %s</string>\n    <string name=\"current_device_kmi\">KMI этого устройства</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-sl/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_click_to_install\">Klikni za namestitev</string>\n    <string name=\"home_working\">V obdelavi</string>\n    <string name=\"home_working_version\">Verzija: %d</string>\n    <string name=\"home_unsupported_reason\">KernelSU podpira samo GKI kernele</string>\n    <string name=\"home_kernel\">Kernel</string>\n    <string name=\"home_manager_version\">Verzija upravitelja</string>\n    <string name=\"home_fingerprint\">Prstni odtis</string>\n    <string name=\"home_selinux_status\">SELinux status</string>\n    <string name=\"selinux_status_disabled\">Onemogočeno</string>\n    <string name=\"selinux_status_unknown\">Neznano</string>\n    <string name=\"module_failed_to_disable\">Napaka pri onemogočanju modula: %s</string>\n    <string name=\"module_empty\">Ni nameščenih modulov</string>\n    <string name=\"module\">Modul</string>\n    <string name=\"uninstall\">Odmesti</string>\n    <string name=\"module_install\">Namesti</string>\n    <string name=\"install\">Namesti</string>\n    <string name=\"reboot_userspace\">Mehki ponovni zagon</string>\n    <string name=\"reboot_recovery\">Ponovni zagon v Recovery</string>\n    <string name=\"reboot_bootloader\">Ponovni zagon v Bootloader</string>\n    <string name=\"reboot_edl\">Ponovni zagon v EDL</string>\n    <string name=\"module_uninstall_confirm\">Ste prepričani, da želite odstraniti modul %s?</string>\n    <string name=\"module_uninstall_success\">%s je odmeščen</string>\n    <string name=\"module_author\">Avtor</string>\n    <string name=\"send_log\">Prijavite dnevnik</string>\n    <string name=\"home_learn_kernelsu\">Naučite se KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Naučite se, kako namestiti KernelSU in uporabiti module</string>\n    <string name=\"about_source_code\">Glej odprto kodo na %1$s<br/>Pridružite se našem %2$s kanalu</string>\n    <string name=\"profile_default\">Privzeto</string>\n    <string name=\"profile_template\">Predloga</string>\n    <string name=\"profile_namespace\">Imenski prostor vmestitve</string>\n    <string name=\"profile_namespace_inherited\">Podedovano</string>\n    <string name=\"profile_namespace_global\">Globalno</string>\n    <string name=\"profile_namespace_individual\">Pozameznik</string>\n    <string name=\"profile_capabilities\">Zmožnosti</string>\n    <string name=\"profile_umount_modules\">Izvrzi module</string>\n    <string name=\"settings_umount_modules_default\">Po privzetem izvrzi module</string>\n    <string name=\"profile_selinux_domain\">Domena</string>\n    <string name=\"module_update\">Posodobitev</string>\n    <string name=\"module_downloading\">Nalaganje modula: %s</string>\n    <string name=\"launch_app\">Zaženi</string>\n    <string name=\"restart_app\">Ponovni zagon</string>\n    <string name=\"module_changelog\">Dnevnik sprememb</string>\n    <string name=\"settings_profile_template\">Predloga za aplikacijski profil</string>\n    <string name=\"home\">Domov</string>\n    <string name=\"home_unsupported\">Ne podpira</string>\n    <string name=\"superuser\">SuperUporabnik</string>\n    <string name=\"module_failed_to_enable\">Napaka pri omogočanju modula: %s</string>\n    <string name=\"reboot\">Znova zaženi</string>\n    <string name=\"settings\">Nastavitve</string>\n    <string name=\"reboot_download\">Ponovni zagon v Download</string>\n    <string name=\"about\">O nas</string>\n    <string name=\"module_version\">Verzija</string>\n    <string name=\"module_uninstall_failed\">Napaka pri odmeščanju: %s</string>\n    <string name=\"safe_mode\">Varni način</string>\n    <string name=\"reboot_to_apply\">Za uveljavitev je potreben ponovni zagon</string>\n    <string name=\"show_system_apps\">Prikaz sistemskih aplikacij</string>\n    <string name=\"module_magisk_conflict\">Moduli so onemogočeni, ker so v konfliktu z Magiskovimi!</string>\n    <string name=\"home_support_title\">Podprite nas</string>\n    <string name=\"profile_custom\">Po meri</string>\n    <string name=\"profile_name\">Ime profila</string>\n    <string name=\"profile_groups\">Skupine</string>\n    <string name=\"profile_selinux_context\">SELinux kontekst</string>\n    <string name=\"home_support_content\">KernelSU je, in bo vedno brezplačen in odprtokoden. Kljub temu, nam lahko z donacijo pokažete, da vam je mar.</string>\n    <string name=\"failed_to_update_app_profile\">Napaka pri posodobitvi aplikacijskega profila za %s</string>\n    <string name=\"require_kernel_version\">Za pravilno funkionalnost upravitelja je trenutna KernelSU verzija %1$d prenizka. Potrebna je nadgradnja na verzijo %2$d ali več!</string>\n    <string name=\"settings_umount_modules_default_summary\">Globalno privzeta vrednost za \\\"Izvrzi module\\\" v aplikacijskih profilih. Če je omogočena, bo to odstranilo vse sistemske modifikacije modulov za aplikacije, ki nimajo nastavljenega profila.</string>\n    <string name=\"profile_umount_modules_summary\">Omogočanje te opcije bo dovolilo KernelSU, da obnovi vse zaradi modulov spremenjene datoteke za to aplikacijo.</string>\n    <string name=\"force_stop_app\">Prisilna ustavitev</string>\n    <string name=\"profile_selinux_rules\">Pravila</string>\n    <string name=\"module_start_downloading\">Začni z nalaganjem: %s</string>\n    <string name=\"new_version_available\">Na voljo je nova verzija: %s, kliknite za nadgradnjo</string>\n    <string name=\"failed_to_update_sepolicy\">Napaka pri posodobitvi SELinux pravil za: %s</string>\n    <string name=\"home_not_installed\">Ni nameščeno</string>\n    <string name=\"selinux_status_enforcing\">Enforcing</string>\n    <string name=\"selinux_status_permissive\">Permissive</string>\n    <string name=\"app_profile_template_create\">Ustvari predlogo</string>\n    <string name=\"app_profile_template_edit\">Uredi predlogo</string>\n    <string name=\"app_profile_template_id_invalid\">Neveljaven id predloge</string>\n    <string name=\"app_profile_template_description\">Opis</string>\n    <string name=\"app_profile_template_save\">Shrani</string>\n    <string name=\"app_profile_template_delete\">Odstrani</string>\n    <string name=\"app_profile_template_id_exist\">id predloge že obstaja!</string>\n    <string name=\"app_profile_import_from_clipboard\">Uvoz iz odložišča</string>\n    <string name=\"app_profile_export_to_clipboard\">Izvoz v odložišče</string>\n    <string name=\"app_profile_template_export_empty\">Lokalna predloga za izvoz ni bila najdena!</string>\n    <string name=\"app_profile_template_save_failed\">Napaka pri shranjevanju predloge</string>\n    <string name=\"app_profile_template_import_empty\">Odložišče je prazno!</string>\n    <string name=\"settings_profile_template_summary\">Upravljaj z lokalnimi in spletnimi predlogami za aplikacijski profil</string>\n    <string name=\"app_profile_template_id\">id</string>\n    <string name=\"app_profile_template_name\">Ime</string>\n    <string name=\"app_profile_template_view\">Ogled predloge</string>\n    <string name=\"app_profile_template_import_success\">Uvoz uspešen</string>\n    <string name=\"app_profile_import_export\">Uvoz/Izvoz</string>\n    <string name=\"save_log\">Shrani Dnevnike</string>\n    <string name=\"settings_sucompat\">Preusmeri su binarno datoteko</string>\n    <string name=\"settings_sucompat_summary\">Preusmeri /system/bin/su na ksud za aplikacije z dovoljenjem Superuporabnika v profilu aplikacije; velja samo za nove procese.</string>\n    <string name=\"settings_kernel_umount\">Jedrno odklapljanje</string>\n    <string name=\"settings_kernel_umount_summary\">Vedenje odklapljanja na ravni jedra, ki ga nadzoruje KernelSU</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-sr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home_click_to_install\">Додирните да бисте инсталирали</string>\n    <string name=\"home\">Почетна</string>\n    <string name=\"home_not_installed\">Није инсталирано</string>\n    <string name=\"home_working_version\">Верзија: %d</string>\n    <string name=\"home_working\">Ради</string>\n    <string name=\"save_log\">Сачувај Дневнике</string>\n    <string name=\"settings_sucompat\">Preusmeri su binarni fajl</string>\n    <string name=\"settings_sucompat_summary\">Preusmeri /system/bin/su na ksud za aplikacije sa dozvolom Superkorisnika u Profilu aplikacije; efektivno samo za nove procese.</string>\n    <string name=\"settings_kernel_umount\">Kernel umount</string>\n    <string name=\"settings_kernel_umount_summary\">Ponašanje umount-a na nivou kernela kontrolisano od strane KernelSU-a</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-te/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"selinux_status_unknown\">తెలియదు</string>\n    <string name=\"module_failed_to_enable\">మాడ్యూల్‌ని ప్రారంభించడంలో విఫలమైంది: %s</string>\n    <string name=\"force_stop_app\">బలవంతంగా ఆపడం</string>\n    <string name=\"restart_app\">పునఃప్రారంభించండి</string>\n    <string name=\"module\">మాడ్యూల్</string>\n    <string name=\"about\">గురించి</string>\n    <string name=\"home_selinux_status\">SELinux స్థితి</string>\n    <string name=\"home\">హోమ్</string>\n    <string name=\"superuser\">సూపర్యూజర్</string>\n    <string name=\"module_failed_to_disable\">మాడ్యూల్‌ని నిలిపివేయడంలో విఫలమైంది: %s</string>\n    <string name=\"module_empty\">మాడ్యూల్ ఏదీ ఇన్‌స్టాల్ చేయబడలేదు</string>\n    <string name=\"home_not_installed\">ఇన్‌స్టాల్ చేయలేదు</string>\n    <string name=\"home_click_to_install\">ఇన్‌స్టాల్ చేయడానికి క్లిక్ చేయండి</string>\n    <string name=\"home_working\">పని చేస్తోంది</string>\n    <string name=\"home_working_version\">వెర్షన్: %d</string>\n    <string name=\"save_log\">లాగ్‌లు సేవ్ చేయండి</string>\n    <string name=\"settings_sucompat\">su బైనరీని మళ్ళించండి</string>\n    <string name=\"settings_sucompat_summary\">యాప్ ప్రొఫైల్‌లో సూపర్‌యూజర్ అనుమతి ఉన్న యాప్‌ల కోసం /system/bin/su ని ksud కి మళ్ళించండి; కొత్త ప్రాసెస్‌లకు మాత్రమే పని చేస్తుంది.</string>\n    <string name=\"settings_kernel_umount\">కెర్నల్ అన్‌మౌంట్</string>\n    <string name=\"settings_kernel_umount_summary\">KernelSU ద్వారా నియంత్రించబడే కెర్నల్-స్థాయి అన్‌మౌంట్ ప్రవర్తన</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-th/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">หน้าหลัก</string>\n    <string name=\"home_not_installed\">ยังไม่ได้ติดตั้ง</string>\n    <string name=\"home_click_to_install\">กดเพื่อติดตั้ง</string>\n    <string name=\"home_working\">กำลังทำงาน</string>\n    <string name=\"home_working_version\">เวอร์ชัน: %d</string>\n    <string name=\"home_manager_version\">เวอร์ชันตัวจัดการ</string>\n    <string name=\"home_unsupported\">ไม่รองรับ</string>\n    <string name=\"selinux_status_enforcing\">Enforcing</string>\n    <string name=\"reboot_recovery\">รีบูตเข้าสู่โหมดกู้คืน</string>\n    <string name=\"reboot_userspace\">ซอฟต์รีบูต</string>\n    <string name=\"home_unsupported_reason\">KernelSU รองรับเคอร์เนลประเภท GKI เท่านั้น</string>\n    <string name=\"home_kernel\">เวอร์ชันเคอร์เนล</string>\n    <string name=\"selinux_status_disabled\">ปิดใช้งาน</string>\n    <string name=\"home_fingerprint\">ลายนิ้วมือ</string>\n    <string name=\"home_selinux_status\">สถานะ SELinux</string>\n    <string name=\"selinux_status_permissive\">Permissive</string>\n    <string name=\"selinux_status_unknown\">ไม่ทราบ</string>\n    <string name=\"superuser\">สิทธิ์ผู้ใช้ขั้นสูง</string>\n    <string name=\"module_failed_to_enable\">ล้มเหลวในการเปิดใช้งานโมดูล %s</string>\n    <string name=\"module_failed_to_disable\">ล้มเหลวในการปิดใช้งานโมดูล: %s</string>\n    <string name=\"module_empty\">ไม่มีโมดูลที่ติดตั้ง</string>\n    <string name=\"module\">โมดูล</string>\n    <string name=\"uninstall\">ถอนการติดตั้ง</string>\n    <string name=\"settings\">ตั้งค่า</string>\n    <string name=\"module_install\">ติดตั้ง</string>\n    <string name=\"install\">ติดตั้ง</string>\n    <string name=\"reboot\">รีบูต</string>\n    <string name=\"reboot_bootloader\">รีบูตเข้าสู่โหมด Bootloader</string>\n    <string name=\"about\">เกี่ยวกับ</string>\n    <string name=\"reboot_download\">รีบูตเข้าสู่โหมด Download</string>\n    <string name=\"reboot_edl\">รีบูตเข้าสู่โหมด EDL</string>\n    <string name=\"module_uninstall_success\">%s ถอนการติดตั้งสำเร็จ</string>\n    <string name=\"module_uninstall_failed\">ถอนการติดตั้ง %s ล้มเหลว</string>\n    <string name=\"module_uninstall_confirm\">คุณแน่ใจว่าจะถอนการติดตั้งโมดูล %s หรือไม่\\?</string>\n    <string name=\"module_author\">ผู้สร้าง</string>\n    <string name=\"module_version\">เวอร์ชัน</string>\n    <string name=\"show_system_apps\">แสดงแอประบบ</string>\n    <string name=\"send_log\">ส่ง logs</string>\n    <string name=\"safe_mode\">โหมดปลอดภัย</string>\n    <string name=\"reboot_to_apply\">รีบูตเพื่อให้มีผล</string>\n    <string name=\"module_magisk_conflict\">โมดูลไม่สามารถใช้งานได้ เนื่องจากขัดแย้งกับ Magisk!</string>\n    <string name=\"home_learn_kernelsu\">เรียนรู้เกี่ยวกับ KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">เรียนรู้วิธีการติดตั้ง KernelSU และวิธีใช้งานโมดูลต่าง ๆ</string>\n    <string name=\"home_support_title\">สนับสนุนพวกเรา</string>\n    <string name=\"home_support_content\">KernelSU จะเป็นโอเพ่นซอร์สฟรีเสมอ อย่างไรก็ตาม คุณสามารถแสดงความห่วงใยได้ด้วยการบริจาค</string>\n    <string name=\"about_source_code\"><![CDATA[ดูซอร์สโค้ดได้ที่ %1$s<br/>เข้าร่วม %2$s channel]]></string>\n    <string name=\"profile_custom\">กำหนดเอง</string>\n    <string name=\"profile_default\">ค่าเริ่มต้น</string>\n    <string name=\"profile_template\">เทมเพลต</string>\n    <string name=\"profile_name\">ชื่อโปรไฟล์</string>\n    <string name=\"profile_namespace\">Mount เนมสเปซ</string>\n    <string name=\"profile_namespace_global\">ทั่วไป</string>\n    <string name=\"profile_namespace_inherited\">สืบทอด</string>\n    <string name=\"profile_namespace_individual\">ส่วนบุคคล</string>\n    <string name=\"profile_groups\">หมวดหมู่</string>\n    <string name=\"profile_capabilities\">ความสามารถของแอป</string>\n    <string name=\"profile_umount_modules_summary\">การเปิดใช้งานตัวเลือกนี้จะทำให้ KernelSU สามารถกู้คืนไฟล์ที่แก้ไขโดยโมดูลสำหรับแอปนี้ได้</string>\n    <string name=\"profile_selinux_context\">บริบท SELinux</string>\n    <string name=\"profile_umount_modules\">Umount โมดูล</string>\n    <string name=\"failed_to_update_app_profile\">ไม่สามารถอัปเดตโปรไฟล์แอปสำหรับ %s ได้</string>\n    <string name=\"settings_umount_modules_default\">Umount โมดูลตามค่าเริ่มต้น</string>\n    <string name=\"profile_selinux_domain\">โดเมน</string>\n    <string name=\"module_update\">อัปเดต</string>\n    <string name=\"profile_selinux_rules\">กฎ</string>\n    <string name=\"module_downloading\">กำลังดาวน์โหลดโมดูล: %s</string>\n    <string name=\"module_start_downloading\">กำลังเริ่มดาวน์โหลด: %s</string>\n    <string name=\"new_version_available\">เวอร์ชัน %s พร้อมใช้งาน คลิกเพื่ออัปเกรด !</string>\n    <string name=\"force_stop_app\">บังคับหยุด</string>\n    <string name=\"restart_app\">รีสตาร์ท</string>\n    <string name=\"settings_umount_modules_default_summary\">หากเปิดใช้งานค่าเริ่มต้นโดยทั่วไปสำหรับ \\\"Umount โมดูล\\\" ในโปรไฟล์แอป จะเป็นการลบการแก้ไขโมดูลทั้งหมดในระบบสำหรับแอปพลิเคชันที่ไม่มีการตั้งค่าโปรไฟล์</string>\n    <string name=\"launch_app\">เปิด</string>\n    <string name=\"failed_to_update_sepolicy\">ไม่สามารถอัปเดตกฎ SElinux สำหรับ %s</string>\n    <string name=\"require_kernel_version\">KernelSU เวอร์ชัน %1$d ต่ำเกินไป ทำให้ตัวจัดการไม่สามารถทำงานได้อย่างถูกต้อง โปรดอัปเกรดเป็นเวอร์ชัน %2$d หรือสูงกว่า!</string>\n    <string name=\"module_changelog\">บันทึกการเปลี่ยนแปลง</string>\n    <string name=\"app_profile_template_import_success\">นำเข้าเสร็จสิ้น</string>\n    <string name=\"app_profile_export_to_clipboard\">ส่งออกไปยังคลิปบอร์ด</string>\n    <string name=\"app_profile_template_export_empty\">ไม่พบเทมเพลตในเครื่องที่จะส่งออก!</string>\n    <string name=\"app_profile_template_id_exist\">มีไอดีเทมเพลตนี้อยู่แล้ว!</string>\n    <string name=\"app_profile_import_from_clipboard\">นำเข้าจากคลิปบอร์ด</string>\n    <string name=\"app_profile_template_name\">ชื่อ</string>\n    <string name=\"app_profile_template_id_invalid\">ไอดีเทมเพลตไม่ถูกต้อง</string>\n    <string name=\"app_profile_template_create\">สร้างเทมเพลต</string>\n    <string name=\"app_profile_import_export\">นำเข้า/ส่งออก</string>\n    <string name=\"app_profile_template_save_failed\">ไม่สามารถบันทึกเทมเพลต</string>\n    <string name=\"app_profile_template_edit\">แก้ไขเทมเพลต</string>\n    <string name=\"app_profile_template_id\">ไอดี</string>\n    <string name=\"settings_profile_template\">เทมเพลตโปรไฟล์แอป</string>\n    <string name=\"app_profile_template_description\">คำอธิบาย</string>\n    <string name=\"app_profile_template_save\">บันทึก</string>\n    <string name=\"settings_profile_template_summary\">จัดการเทมเพลตโปรไฟล์แอปในเครื่องและเทมเพลตออนไลน์</string>\n    <string name=\"app_profile_template_delete\">ลบ</string>\n    <string name=\"app_profile_template_import_empty\">คลิปบอร์ดว่างเปล่า!</string>\n    <string name=\"app_profile_template_view\">ดูเทมเพลต</string>\n    <string name=\"open\">เปิด</string>\n    <string name=\"grant_root_failed\">ไม่สามารถให้สิทธิ์รูทได้!</string>\n    <string name=\"settings_check_update\">ตรวจสอบการอัปเดต</string>\n    <string name=\"settings_check_update_summary\">ตรวจสอบการอัปเดตโดยอัตโนมัติเมื่อเปิดแอป</string>\n    <string name=\"enable_web_debugging\">การแก้ไขข้อบกพร่อง WebView</string>\n    <string name=\"select_kmi\">เลือก KMI</string>\n    <string name=\"install_next\">ต่อไป</string>\n    <string name=\"select_file\">เลือกไฟล์</string>\n    <string name=\"install_inactive_slot\">ติดตั้งลงในสล็อตที่ไม่ใช้งาน (หลังจาก OTA)</string>\n    <string name=\"direct_install\">ติดตั้งโดยตรง (แนะนำ)</string>\n    <string name=\"select_file_tip\">แนะนำให้ใช้อิมเมจพาร์ติชัน %1$s</string>\n    <string name=\"enable_web_debugging_summary\">ใช้เพื่อดีบัก WebUI เท่านั้น โปรดเปิดใช้งานเมื่อจำเป็น</string>\n    <string name=\"install_inactive_slot_warning\">อุปกรณ์ของคุณจะถูก **บังคับ** ให้บูตไปยังสล็อตที่ไม่ได้ใช้งานหลังจากรีบูต!\n\\nโปรดใช้ตัวเลือกนี้หลังจาก OTA เสร็จแล้วเท่านั้น\n\\nดำเนินการต่อหรือไม่?</string>\n    <string name=\"settings_uninstall_permanent\">ถอนการติดตั้งถาวร</string>\n    <string name=\"settings_restore_stock_image\">เรียกคืนอิมเมจดั้งเดิม</string>\n    <string name=\"settings_uninstall_temporary_message\">ถอนการติดตั้ง KernelSU ชั่วคราว จะคืนค่าเป็นสถานะดั้งเดิมหลังจากรีบูตในครั้งถัดไป</string>\n    <string name=\"flashing\">กำลังแฟลช</string>\n    <string name=\"flash_success\">แฟลชสำเร็จ</string>\n    <string name=\"flash_failed\">แฟลชล้มเหลว</string>\n    <string name=\"selected_lkm\">เลือก LKM: %s</string>\n    <string name=\"settings_uninstall\">ถอนการติดตั้ง</string>\n    <string name=\"settings_uninstall_temporary\">ถอนการติดตั้งชั่วคราว</string>\n    <string name=\"settings_uninstall_permanent_message\">การถอนการติดตั้ง KernelSU (การรูทและโมดูลทั้งหมด) อย่างสมบูรณ์</string>\n    <string name=\"settings_restore_stock_image_message\">คืนค่าโรงงานอิมเมจดั้งเดิม (หากมีข้อมูลสำรอง) ส่วนใหญ่มักใช้ก่อนทำการ OTA ซึ่งหากคุณต้องการถอนการติดตั้ง KernelSU โปรดใช้ \\\"ถอนการติดตั้งถาวร\\\"</string>\n    <string name=\"save_log\">บันทึก logs</string>\n    <string name=\"action\">คำสั่ง</string>\n    <string name=\"log_saved\">บันทึก logs แล้ว</string>\n    <string name=\"module_sort_action_first\">รันคำสั่งขึ้นก่อน</string>\n    <string name=\"module_sort_enabled_first\">เปิดใช้งานขึ้นก่อน</string>\n    <string name=\"module_install_prompt_with_name\">โมดูล %1$s จะถูกติดตั้ง</string>\n    <string name=\"confirm\">ยืนยัน</string>\n    <string name=\"su_not_allowed\">ไม่สามารถให้สิทธิ์ผู้ใช้ขั้นสูงกับ %s ได้</string>\n    <string name=\"settings_module_check_update\">ตรวจสอบการอัปเดตโมดูล</string>\n    <string name=\"install_upload_lkm_file\">ใช้ไฟล์ LKM ในเครื่อง</string>\n    <string name=\"install_only_support_ko_file\">รองรับเฉพาะไฟล์ .ko เท่านั้น</string>\n    <string name=\"settings_sucompat\">เปลี่ยนเส้นทางไบนารี su</string>\n    <string name=\"settings_sucompat_summary\">อนุญาตให้แอปที่ได้รับอนุญาต Superuser ในโปรไฟล์แอปได้รับเชล superuser โดยการเรียกใช้ /system/bin/su; มีผลเฉพาะกับกระบวนการใหม่เท่านั้น</string>\n    <string name=\"settings_kernel_umount\">การเลิกเมานต์เคอร์เนล</string>\n    <string name=\"settings_kernel_umount_summary\">พฤติกรรมการเลิกเมานต์ระดับเคอร์เนลที่ควบคุมโดย KernelSU</string>\n    <string name=\"processing\">กำลังประมวลผล…</string>\n    <string name=\"refresh_pulling\">ดึงลงเพื่อรีเฟรช</string>\n    <string name=\"refresh_release\">ปล่อยเพื่อรีเฟรช</string>\n    <string name=\"refresh_refresh\">กำลังรีเฟรช…</string>\n    <string name=\"refresh_complete\">รีเฟรชสำเร็จ</string>\n    <string name=\"install_select_partition\">เลือกพาร์ทิชัน</string>\n    <string name=\"app_profile_affects_following_apps\">ส่งผลต่อแอปดังต่อไปนี้</string>\n    <string name=\"group_contains_apps\">มีแอป %d รายการ</string>\n    <string name=\"metamodule_uninstall_confirm\">คุณแน่ใจหรือไม่ว่าต้องการถอนการติดตั้งโมดูล %s? การดำเนินการนี้จะส่งผลต่อโมดูลทั้งหมด และฟีเจอร์บางอย่างที่เมตาโมดูลจัดเตรียมไว้ (เช่น การ mount) จะไม่ทำงานอีกต่อไป</string>\n    <string name=\"undo\">เลิกทำ</string>\n    <string name=\"module_undo_uninstall_success\">ยกเลิกการถอนการติดตั้ง %s สำเร็จ</string>\n    <string name=\"module_undo_uninstall_failed\">ไม่สามารถยกเลิกการถอนการติดตั้ง %s ได้</string>\n    <string name=\"settings_theme\">ธีม</string>\n    <string name=\"settings_theme_summary\">เลือกธีมของแอป</string>\n    <string name=\"settings_theme_mode_system\">ตามระบบ</string>\n    <string name=\"settings_theme_mode_light\">สว่าง</string>\n    <string name=\"settings_theme_mode_dark\">มืด</string>\n    <string name=\"settings_key_color\">สีหลัก</string>\n    <string name=\"settings_key_color_default\">ค่าเริ่มต้น</string>\n    <string name=\"color_blue\">สีน้ำเงิน</string>\n    <string name=\"color_red\">สีแดง</string>\n    <string name=\"color_green\">สีเขียว</string>\n    <string name=\"color_purple\">สีม่วง</string>\n    <string name=\"color_orange\">สีส้ม</string>\n    <string name=\"color_teal\">สีเขียวอมน้ำเงิน</string>\n    <string name=\"color_pink\">สีชมพู</string>\n    <string name=\"color_brown\">สีน้ำตาล</string>\n    <string name=\"feature_status_unsupported_summary\">เคอร์เนลของคุณไม่รองรับฟีเจอร์นี้</string>\n    <string name=\"feature_status_managed_summary\">คุณสมบัตินี้ถูกจัดการโดยโมดูล</string>\n    <string name=\"module_repos\">พื้นที่จัดเก็บโมดูล</string>\n    <string name=\"module_repos_sort_name\">เรียงตามชื่อ (A → Z)</string>\n    <string name=\"module_repos_source_code\">ซอร์สโค้ด</string>\n    <string name=\"home_gki_warning\">ตั้งแต่เวอร์ชัน 3.0.0 เป็นต้นไป โหมด GKI จะถูกใช้สำหรับการทดสอบเท่านั้น เราไม่แนะนำให้ใช้โหมดนี้ในชีวิตประจำวัน และจะไม่มีไฟล์อิมเมจให้ดาวน์โหลดอีกต่อไป</string>\n    <string name=\"network_offline\">ไม่ได้เชื่อมต่อกับเครือข่าย</string>\n    <string name=\"network_retry\">ลองอีกครั้ง</string>\n    <string name=\"tab_readme\">โปรดอ่าน</string>\n    <string name=\"tab_info\">ข้อมูล</string>\n    <string name=\"tab_releases\">รายการที่เผยแพร่</string>\n    <string name=\"safe_mode_module_disabled\">ไม่สามารถติดตั้งโมดูลได้ในโหมดปลอดภัย</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-tr/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"app_name\" translatable=\"false\">KernelSU</string>\n    <string name=\"home\">Ana Sayfa</string>\n    <string name=\"home_not_installed\">Kurulmadı</string>\n    <string name=\"home_click_to_install\">Kurmak için tıklayın</string>\n    <string name=\"home_working\">Çalışıyor</string>\n    <string name=\"home_working_version\">Sürüm: %d</string>\n    <string name=\"home_unsupported\">Desteklenmiyor</string>\n    <string name=\"home_unsupported_reason\">KernelSU şimdilik sadece GKI çekirdeklerini destekliyor</string>\n    <string name=\"home_kernel\">Çekirdek Versiyonu</string>\n    <string name=\"home_manager_version\">Yönetici sürümü</string>\n    <string name=\"home_fingerprint\">Parmak izi</string>\n    <string name=\"home_selinux_status\">SELinux durumu</string>\n    <string name=\"selinux_status_disabled\">Devre dışı</string>\n    <string name=\"selinux_status_enforcing\">Etkin (Enforcing)</string>\n    <string name=\"selinux_status_permissive\">Serbest (Permissive)</string>\n    <string name=\"selinux_status_unknown\">Bilinmiyor</string>\n    <string name=\"superuser\">Süper kullanıcı</string>\n    <string name=\"module_failed_to_enable\">Modül etkinleştirilemedi: %s</string>\n    <string name=\"module_failed_to_disable\">Modül devre dışı bırakılamadı: %s</string>\n    <string name=\"module_empty\">Kurulu modül yok</string>\n    <string name=\"module\">Modül</string>\n    <string name=\"uninstall\">Kaldır</string>\n    <string name=\"module_install\">Kur</string>\n    <string name=\"install\">Kur</string>\n    <string name=\"reboot\">Cihazı yeniden başlat</string>\n    <string name=\"settings\">Ayarlar</string>\n    <string name=\"reboot_userspace\">Hızlı yeniden başlat</string>\n    <string name=\"reboot_recovery\">Kurtarma modunda yeniden başlat</string>\n    <string name=\"reboot_bootloader\">Önyükleyici modunda yeniden başlat</string>\n    <string name=\"reboot_download\">İndirme modunda yeniden başlat</string>\n    <string name=\"reboot_edl\">EDL modunda yeniden başlat</string>\n    <string name=\"about\">Hakkında</string>\n    <string name=\"module_uninstall_confirm\">%s modülünü kaldırmak istediğinizden emin misiniz?</string>\n    <string name=\"module_uninstall_success\">%s kaldırıldı</string>\n    <string name=\"module_uninstall_failed\">Kaldırma başarısız: %s</string>\n    <string name=\"module_version\">Sürüm</string>\n    <string name=\"module_author\">Geliştirici</string>\n    <string name=\"show_system_apps\">Sistem uygulamalarını göster</string>\n    <string name=\"send_log\">Günlükleri gönder</string>\n    <string name=\"safe_mode\">Güvenli mod</string>\n    <string name=\"reboot_to_apply\">Değişikliklerin uygulanması için cihazı yeniden başlat</string>\n    <string name=\"module_magisk_conflict\">Magisk ile çakışma nedeniyle modüller kullanılamıyor!</string>\n    <string name=\"home_learn_kernelsu\">KernelSU\\'yu öğrenin</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">KernelSU\\'nun nasıl kurulacağını ve modüllerin nasıl kullanılacağını öğrenin</string>\n    <string name=\"home_support_title\">Bizi Destekleyin</string>\n    <string name=\"home_support_content\">KernelSU ücretsiz ve açık kaynaklı bir yazılımdır ve her zaman öyle kalacaktır. Ancak bağış yaparak bize destek olduğunuzu gösterebilirsiniz.</string>\n    <string name=\"about_source_code\"><![CDATA[%1$s adresinde kaynak kodunu görüntüleyin.<br/>%2$s kanalımıza katılın.]]></string>\n    <string name=\"profile\" translatable=\"false\">Uygulama profili</string>\n    <string name=\"profile_default\">Varsayılan</string>\n    <string name=\"profile_template\">Şablon</string>\n    <string name=\"profile_custom\">Özel</string>\n    <string name=\"profile_name\">Profil adı</string>\n    <string name=\"profile_namespace\">Ad alanını bağla</string>\n    <string name=\"profile_namespace_inherited\">Kalıtsal</string>\n    <string name=\"profile_namespace_global\">Küresel</string>\n    <string name=\"profile_namespace_individual\">Bireysel</string>\n    <string name=\"profile_groups\">Gruplar</string>\n    <string name=\"profile_capabilities\">Yetkinlikler</string>\n    <string name=\"profile_selinux_context\">SELinux içeriği</string>\n    <string name=\"profile_umount_modules\">Modüllerin bağlantısını kes</string>\n    <string name=\"failed_to_update_app_profile\">%s için uygulama profili güncellenemedi</string>\n    <string name=\"require_kernel_version\">Mevcut KernelSU sürümü %1$d, yöneticinin düzgün çalışabilmesi için çok düşük. Lütfen %2$d sürümüne veya daha yüksek bir sürüme güncelleyin!</string>\n    <string name=\"settings_umount_modules_default\">Varsayılan olarak modüllerin bağlantısını kes</string>\n    <string name=\"settings_umount_modules_default_summary\">Uygulama profilindeki \\\"Modüllerin bağlantısını kes\\\" seçeneği için varsayılan değer. Etkinleştirilirse, profil ayarı yapılmamış uygulamalar için modüllerin sistemde yaptığı tüm değişiklikler kaldırılacaktır</string>\n    <string name=\"profile_umount_modules_summary\">Bu seçeneği etkinleştirmek, KernelSU\\'nun bu uygulama için modüller tarafından değiştirilen dosyaları geri yüklemesine izin verir</string>\n    <string name=\"profile_selinux_domain\">İsim alanı</string>\n    <string name=\"profile_selinux_rules\">Kurallar</string>\n    <string name=\"module_update\">Güncelle</string>\n    <string name=\"module_downloading\">Modül indiriliyor: %s</string>\n    <string name=\"module_start_downloading\">İndirme başladı: %s</string>\n    <string name=\"new_version_available\">Yeni sürüm: %s kullanılabilir, güncellemek için tıklayın</string>\n    <string name=\"launch_app\">Uygulamayı başlat</string>\n    <string name=\"force_stop_app\">Uygulamayı durmaya zorla</string>\n    <string name=\"restart_app\">Uygulamayı yeniden başlat</string>\n    <string name=\"failed_to_update_sepolicy\">%s için SELinux kuralları güncellenemedi</string>\n    <string name=\"module_changelog\">Değişiklik geçmişi</string>\n    <string name=\"settings_profile_template\">Uygulama profili şablonu</string>\n    <string name=\"settings_profile_template_summary\">Yerel ve çevrimiçi uygulama profili şablonlarını yönetin</string>\n    <string name=\"app_profile_template_create\">Şablon oluştur</string>\n    <string name=\"app_profile_template_edit\">Şablonu düzenle</string>\n    <string name=\"app_profile_template_id\">Kimlik</string>\n    <string name=\"app_profile_template_id_invalid\">Geçersiz şablon kimliği</string>\n    <string name=\"app_profile_template_name\">İsim</string>\n    <string name=\"app_profile_template_description\">Açıklama</string>\n    <string name=\"app_profile_template_save\">Kaydet</string>\n    <string name=\"app_profile_template_delete\">Sil</string>\n    <string name=\"app_profile_template_view\">Şablonu görüntüle</string>\n    <string name=\"app_profile_template_id_exist\">Şablon kimliği zaten var!</string>\n    <string name=\"app_profile_import_export\">İçe aktar/Dışa aktar</string>\n    <string name=\"app_profile_import_from_clipboard\">Panodan içe aktar</string>\n    <string name=\"app_profile_export_to_clipboard\">Panodan dışa aktar</string>\n    <string name=\"app_profile_template_export_empty\">Dışa aktarmak için yerel şablon bulunamadı!</string>\n    <string name=\"app_profile_template_import_success\">Başarıyla içe aktarıldı</string>\n    <string name=\"app_profile_template_save_failed\">Şablon kaydedilemedi</string>\n    <string name=\"app_profile_template_import_empty\">Pano boş!</string>\n    <string name=\"settings_check_update\">Güncellemeleri denetle</string>\n    <string name=\"settings_check_update_summary\">Uygulamayı açarken güncellemeleri otomatik denetle</string>\n    <string name=\"grant_root_failed\">Root izni verilemedi!</string>\n    <string name=\"open\">Aç</string>\n    <string name=\"enable_web_debugging\">Web görünümü hata ayıklamasını etkinleştir</string>\n    <string name=\"enable_web_debugging_summary\">Web kullanıcı arayüzünde hata ayıklamak için kullanılabilir, lütfen yalnızca gerektiğinde etkinleştirin</string>\n    <string name=\"direct_install\">Doğrudan kur (Tavsiye edilen)</string>\n    <string name=\"select_file\">Bir dosya seçin</string>\n    <string name=\"install_inactive_slot\">Etkin olmayan yuvaya kur (OTA\\'dan sonra)</string>\n    <string name=\"install_inactive_slot_warning\">Cihazınız geçerli etkin olmayan yuvaya **ZORLA** yeniden başlatılacaktır!\n\\nBu seçeneği yalnızca OTA tamamlandıktan sonra kullanın.\n\\nDevam edilsin mi?</string>\n    <string name=\"install_next\">Sonraki</string>\n    <string name=\"select_kmi\">KMI seçin</string>\n    <string name=\"select_file_tip\">%1$s bölüm imajı önerilir</string>\n    <string name=\"settings_uninstall_temporary\">Geçici olarak kaldır</string>\n    <string name=\"settings_uninstall_permanent\">Kalıcı olarak kaldır</string>\n    <string name=\"settings_restore_stock_image\">Stok imajı geri yükle</string>\n    <string name=\"settings_uninstall_temporary_message\">KernelSU\\'yu geçici olarak kaldır, bir sonraki yeniden başlatmadan sonra orijinal durumuna geri yükle</string>\n    <string name=\"settings_uninstall\">Kaldır</string>\n    <string name=\"settings_uninstall_permanent_message\">KernelSU (Root ve tüm modüller) tamamen ve kalıcı olarak kaldırılıyor</string>\n    <string name=\"settings_restore_stock_image_message\">Stok fabrika imajını geri yükler (eğer yedek varsa), genellikle OTA\\'dan önce kullanılır; KernelSU\\'yu kaldırmanız gerekiyorsa, lütfen \\\"Kalıcı olarak kaldır\\\" seçeneğini kullanın</string>\n    <string name=\"flash_success\">Flaşlama başarılı</string>\n    <string name=\"selected_lkm\">Seçili LKM: %s</string>\n    <string name=\"flashing\">Flaşlanıyor</string>\n    <string name=\"flash_failed\">Flaşlama başarısız</string>\n    <string name=\"save_log\">Günlükleri Kaydet</string>\n    <string name=\"action\">Aksiyon</string>\n    <string name=\"log_saved\">Günlükler kaydedildi</string>\n    <string name=\"confirm\">Onayla</string>\n    <string name=\"su_not_allowed\">%s için Superuser erişimi verilemedi</string>\n    <string name=\"module_install_prompt_with_name\">Aşağıdaki modüller yüklenecek: %1$s</string>\n    <string name=\"module_sort_action_first\">Sırala (Action önce)</string>\n    <string name=\"module_sort_enabled_first\">Sırala (Etkin olanlar önce)</string>\n    <string name=\"module_repos\">Depolar</string>\n    <string name=\"module_repos_sort_name\">Ad (A → Z)</string>\n    <string name=\"module_repos_source_code\">Kaynak kodu</string>\n    <string name=\"metamodule_uninstall_confirm\">%s modülünü kaldırmak istediğinizden emin misiniz? Bu eylem tüm modülleri etkileyecek ve metamodül (örneğin bağlama özelliği) tarafından sunulan belli özellikler artık çalışmayacaktır.</string>\n    <string name=\"home_gki_warning\">v3.0.0 sürümünden itibaren GKI çalışma modu yalnızca test ortamlarında kullanılacaktır. Günlük kullanım için tavsiye etmiyoruz ve imaj dosyaları artık sunulmayacaktır.</string>\n    <string name=\"settings_sucompat\">su ikili dosyasını yeniden yönlendir</string>\n    <string name=\"settings_sucompat_summary\">Uygulama Profilinde Süper Kullanıcı izni verilen uygulamaların /system/bin/su yürütülerek süper kullanıcı kabuğu almasına izin verir; yalnızca yeni işlemler için etkilidir.</string>\n    <string name=\"settings_kernel_umount\">Çekirdek bağlantısını kes</string>\n    <string name=\"settings_kernel_umount_summary\">KernelSU tarafından kontrol edilen çekirdek seviyesinde bağlantı kesme davranışı</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-uk/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">Головна</string>\n    <string name=\"home_not_installed\">Не встановлено</string>\n    <string name=\"home_click_to_install\">Натисніть щоб встановити</string>\n    <string name=\"home_working\">Працює</string>\n    <string name=\"home_working_version\">Версія: %d</string>\n    <string name=\"home_unsupported\">Не підтримується</string>\n    <string name=\"home_unsupported_reason\">KernelSU зараз підтримує лише ядра GKI.</string>\n    <string name=\"home_kernel\">Версія ядра</string>\n    <string name=\"home_manager_version\">Версія менеджера</string>\n    <string name=\"home_fingerprint\">Відбиток</string>\n    <string name=\"home_selinux_status\">Статус SELinux</string>\n    <string name=\"selinux_status_disabled\">Вимкнено</string>\n    <string name=\"selinux_status_enforcing\">Примусовий</string>\n    <string name=\"selinux_status_permissive\">Дозвільний</string>\n    <string name=\"selinux_status_unknown\">Невідомо</string>\n    <string name=\"superuser\">Суперкористувач</string>\n    <string name=\"module_failed_to_enable\">Не вдалося ввімкнути модуль: %s</string>\n    <string name=\"module_failed_to_disable\">Не вдалося вимкнути модуль: %s</string>\n    <string name=\"module_empty\">Модуль не встановлено</string>\n    <string name=\"module\">Модулі</string>\n    <string name=\"uninstall\">Видалити</string>\n    <string name=\"module_install\">Встановити</string>\n    <string name=\"install\">Встановити</string>\n    <string name=\"reboot\">Перезавантажити</string>\n    <string name=\"settings\">Налаштування</string>\n    <string name=\"reboot_userspace\">М\\'яке перезавантаження</string>\n    <string name=\"reboot_recovery\">Перезавантажити до Recovery</string>\n    <string name=\"reboot_bootloader\">Перезавантажити до Bootloader</string>\n    <string name=\"reboot_download\">Перезавантажити до Download</string>\n    <string name=\"reboot_edl\">Перезавантажити до EDL</string>\n    <string name=\"about\">Про додаток</string>\n    <string name=\"module_uninstall_confirm\">Ви впевнені, що хочете видалити модуль %s?</string>\n    <string name=\"module_uninstall_success\">%s невстановлено</string>\n    <string name=\"module_uninstall_failed\">Не вдалося видалити: %s</string>\n    <string name=\"module_version\">Версія</string>\n    <string name=\"module_author\">Автор</string>\n    <string name=\"show_system_apps\">Показати системні додатки</string>\n    <string name=\"send_log\">Надіслати журнали</string>\n    <string name=\"safe_mode\">Безпечний режим</string>\n    <string name=\"reboot_to_apply\">Перезавантажте, щоб застосувати</string>\n    <string name=\"module_magisk_conflict\">Модулі недоступні через конфлікт з Magisk!</string>\n    <string name=\"home_learn_kernelsu\">Дізнайтеся про KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">Дізнайтеся, як інсталювати KernelSU і використовувати модулі.</string>\n    <string name=\"home_support_title\">Підтримати нас</string>\n    <string name=\"home_support_content\">KernelSU є і завжди буде безкоштовним програмним забезпеченням з відкритим вихідним кодом. Однак ви можете показати нам, що вам небайдужа наша допомога, зробивши пожертву.</string>\n    <string name=\"about_source_code\"><![CDATA[Переглянути вихідний код на %1$s<br/>Приєднуйтесь до нашого каналу %2$s]]></string>\n    <string name=\"profile\">Профіль додатка</string>\n    <string name=\"profile_default\">Типовий</string>\n    <string name=\"profile_template\">Шаблон</string>\n    <string name=\"profile_custom\">Власний</string>\n    <string name=\"profile_name\">Назва профілю</string>\n    <string name=\"profile_namespace\">Змонтувати простір імен</string>\n    <string name=\"profile_namespace_inherited\">Наслідуваний</string>\n    <string name=\"profile_namespace_global\">Глобальний</string>\n    <string name=\"profile_namespace_individual\">Індивідуальний</string>\n    <string name=\"profile_groups\">Групи</string>\n    <string name=\"profile_capabilities\">Можливості</string>\n    <string name=\"profile_selinux_context\">Контекст SELinux</string>\n    <string name=\"profile_umount_modules\">Розмонтування модулів</string>\n    <string name=\"failed_to_update_app_profile\">Не вдалося оновити профіль додатка для %s</string>\n    <string name=\"settings_umount_modules_default\">Розмонтування модулів за замовчуванням</string>\n    <string name=\"settings_umount_modules_default_summary\">Загальне значення за замовчуванням для \\\"Розмонтувати модулі\\\" у профілях додатків. Якщо ввімкнено, буде видалено всі модифікації модулів у системі для додатків, які не мають встановленого профілю.</string>\n    <string name=\"profile_umount_modules_summary\">Увімкнення цієї опції дозволить KernelSU відновити будь-які змінені файли модулями для цієї програми.</string>\n    <string name=\"profile_selinux_domain\">Домен</string>\n    <string name=\"profile_selinux_rules\">Правила</string>\n    <string name=\"module_update\">Оновити</string>\n    <string name=\"module_downloading\">Завантаження модуля: %s</string>\n    <string name=\"module_start_downloading\">Початок завантаження: %s</string>\n    <string name=\"launch_app\">Запустити</string>\n    <string name=\"force_stop_app\">Примусова зупинка</string>\n    <string name=\"restart_app\">Перезапустити</string>\n    <string name=\"new_version_available\">Доступна нова версія %s, натисніть, щоб оновити!</string>\n    <string name=\"failed_to_update_sepolicy\">Не вдалося оновити правила SELinux для %s</string>\n    <string name=\"module_changelog\">Журнал змін</string>\n    <string name=\"require_kernel_version\">Поточна версія KernelSU %1$d занадто низька для належної роботи менеджера. Будь ласка, оновіть його до версії %2$d або вище!</string>\n    <string name=\"app_profile_template_import_success\">Успішно імпортовано</string>\n    <string name=\"app_profile_export_to_clipboard\">Експортувати в буфер обміну</string>\n    <string name=\"app_profile_template_export_empty\">Не вдається знайти локальний шаблон для експорту!</string>\n    <string name=\"app_profile_template_id_exist\">Ідентифікатор шаблону вже існує!</string>\n    <string name=\"app_profile_import_from_clipboard\">Імпортувати з буферу обміну</string>\n    <string name=\"app_profile_template_name\">Ім\\'я</string>\n    <string name=\"app_profile_template_id_invalid\">Недійсний ідентифікатор шаблону</string>\n    <string name=\"app_profile_template_create\">Створити шаблон</string>\n    <string name=\"app_profile_import_export\">Імпорт/Експорт</string>\n    <string name=\"app_profile_template_save_failed\">Помилка при збереженні шаблону</string>\n    <string name=\"app_profile_template_edit\">Редагувати шаблон</string>\n    <string name=\"app_profile_template_id\">Ідентифікатор</string>\n    <string name=\"settings_profile_template\">Шаблон профілю програми</string>\n    <string name=\"app_profile_template_description\">Опис</string>\n    <string name=\"app_profile_template_save\">Зберегти</string>\n    <string name=\"settings_profile_template_summary\">Керувати локальними та мережевими шаблонами профілів додатків.</string>\n    <string name=\"app_profile_template_delete\">Видалити</string>\n    <string name=\"app_profile_template_import_empty\">Буфер обміну пустий!</string>\n    <string name=\"app_profile_template_view\">Переглянути шаблон</string>\n    <string name=\"enable_web_debugging\">Налагодження WebView</string>\n    <string name=\"select_kmi\">Виберіть KMI</string>\n    <string name=\"install_next\">Далі</string>\n    <string name=\"settings_check_update\">Перевірити наявність оновлень</string>\n    <string name=\"settings_check_update_summary\">Автоматична перевірка оновлень під час відкриття програми.</string>\n    <string name=\"enable_web_debugging_summary\">Можна використовувати для налагодження веб-інтерфейсу. Вмикайте лише за потреби.</string>\n    <string name=\"direct_install\">Пряме встановлення (рекомендовано)</string>\n    <string name=\"select_file\">Виберіть файл</string>\n    <string name=\"install_inactive_slot\">Встановити в неактивний слот (після OTA)</string>\n    <string name=\"install_inactive_slot_warning\">Ваш пристрій буде **ПРИМУСОВО** завантажено в поточний неактивний слот після перезавантаження!\n\\n Використовуйте цю опцію тільки після завершення OTA.\n\\n Продовжити?</string>\n    <string name=\"select_file_tip\">%1$s образ розділу рекомендується</string>\n    <string name=\"grant_root_failed\">Не вдалося отримати root!</string>\n    <string name=\"open\">Відкрити</string>\n    <string name=\"settings_uninstall_temporary\">Тимчасово видалити</string>\n    <string name=\"settings_uninstall_permanent\">Видалити назавжди</string>\n    <string name=\"settings_restore_stock_image\">Відновити стокове зображення</string>\n    <string name=\"settings_uninstall_temporary_message\">Тимчасово видалити KernelSU, відновити початковий стан після наступного перезавантаження.</string>\n    <string name=\"settings_uninstall\">Видалити</string>\n    <string name=\"flashing\">Прошивка</string>\n    <string name=\"flash_success\">Прошивку виконано</string>\n    <string name=\"flash_failed\">Прошивка не виконана</string>\n    <string name=\"selected_lkm\">Вибраний ЛКМ: %s</string>\n    <string name=\"settings_uninstall_permanent_message\">Повне та остаточне видалення KernelSU (root та всіх модулів).</string>\n    <string name=\"settings_restore_stock_image_message\">Відновити стоковий заводський образ (якщо є резервна копія), зазвичай використовується перед OTA; якщо вам потрібно видалити KernelSU, використовуйте \\\"Назавжди видалити\\\".</string>\n    <string name=\"save_log\">Зберегти Журнали</string>\n    <string name=\"module_install_prompt_with_name\">Будуть встановлені такі модулі: %1$s</string>\n    <string name=\"module_sort_action_first\">Дія спочатку</string>\n    <string name=\"module_sort_enabled_first\">Увімкнено першим</string>\n    <string name=\"confirm\">Підтвердити</string>\n    <string name=\"su_not_allowed\">Не вдалося надати доступ суперкористувачу для %s</string>\n    <string name=\"action\">Дія</string>\n    <string name=\"log_saved\">Журнали збережено</string>\n    <string name=\"settings_module_check_update\">Перевірити наявність оновлень модулів</string>\n    <string name=\"install_upload_lkm_file\">Використовувати локальний LKM-файл</string>\n    <string name=\"install_only_support_ko_file\">Підтримуються лише файли .ko</string>\n    <string name=\"settings_sucompat\">Перенаправлення бінарного файлу su</string>\n    <string name=\"settings_sucompat_summary\">Дозволяє програмам з правами Суперкористувача в профілі програми отримати оболонку суперкористувача шляхом виконання /system/bin/su; діє тільки для нових процесів.</string>\n    <string name=\"settings_kernel_umount\">Розмонтування ядра</string>\n    <string name=\"settings_kernel_umount_summary\">Поведінка розмонтування на рівні ядра, контрольована KernelSU</string>\n    <string name=\"processing\">Обробка…</string>\n    <string name=\"refresh_pulling\">Потягніть униз, щоб оновити</string>\n    <string name=\"refresh_release\">Відпустіть для оновлення</string>\n    <string name=\"refresh_refresh\">Оновлення…</string>\n    <string name=\"refresh_complete\">Оновлено успішно</string>\n    <string name=\"install_select_partition\">Виберіть розділ</string>\n    <string name=\"metamodule_uninstall_confirm\">Ви впевнені, що хочете видалити модуль %s? Ця дія вплине на всі модулі, а деякі функції, що надаються метамодулем (наприклад, монтування), більше не працюватимуть.</string>\n    <string name=\"app_profile_affects_following_apps\">Впливає на такі програми</string>\n    <string name=\"group_contains_apps\">Містить %d програм</string>\n    <string name=\"settings_theme\">Тема</string>\n    <string name=\"settings_theme_summary\">Виберіть режим теми програми.</string>\n    <string name=\"settings_theme_mode_system\">Слідкувати за системою</string>\n    <string name=\"settings_theme_mode_light\">Світло</string>\n    <string name=\"settings_theme_mode_dark\">Темний</string>\n    <string name=\"settings_key_color\">Ключовий колір</string>\n    <string name=\"settings_key_color_default\">За замовчуванням</string>\n    <string name=\"color_blue\">Синій</string>\n    <string name=\"color_red\">Червоний</string>\n    <string name=\"color_green\">Зелений</string>\n    <string name=\"color_purple\">Фіолетовий</string>\n    <string name=\"color_orange\">Помаранчевий</string>\n    <string name=\"color_teal\">Бірюзовий</string>\n    <string name=\"color_pink\">Рожевий</string>\n    <string name=\"color_brown\">Коричневий</string>\n    <string name=\"feature_status_unsupported_summary\">Ядро не підтримує цю функцію.</string>\n    <string name=\"feature_status_managed_summary\">Цією функцією керує модуль.</string>\n    <string name=\"module_repos\">Репозиторій модулів</string>\n    <string name=\"module_repos_sort_name\">Ім\\'я (А → Я)</string>\n    <string name=\"module_repos_source_code\">Вихідний код</string>\n    <string name=\"home_gki_warning\">Починаючи з версії 3.0.0, робочий режим GKI використовуватиметься лише в тестових середовищах. Ми не рекомендуємо його для щоденного використання, а файли зображень більше не надаватимуться.</string>\n    <string name=\"undo\">Відмінити</string>\n    <string name=\"module_undo_uninstall_success\">Видалення %s успішно скасовано</string>\n    <string name=\"module_undo_uninstall_failed\">Не вдалося відмінити видалення: %s</string>\n    <string name=\"network_offline\">Не підключено до мережі</string>\n    <string name=\"network_retry\">Повторити спробу</string>\n    <string name=\"tab_readme\">Файл README</string>\n    <string name=\"tab_releases\">Релізи</string>\n    <string name=\"tab_info\">Інформація</string>\n    <string name=\"safe_mode_module_disabled\">Встановлення модулів вимкнено в безпечному режимі</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-vi/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"profile\">Hồ sơ ứng dụng</string>\n    <string name=\"profile_default\">Mặc định</string>\n    <string name=\"profile_template\">Bản mẫu</string>\n    <string name=\"profile_custom\">Tuỳ chỉnh</string>\n    <string name=\"profile_name\">Tên hồ sơ</string>\n    <string name=\"profile_groups\">Nhóm</string>\n    <string name=\"failed_to_update_app_profile\">Cập nhật Hồ sơ ứng dụng cho %s thất bại</string>\n    <string name=\"settings_umount_modules_default\">Umount modules</string>\n    <string name=\"settings_umount_modules_default_summary\">Giá trị mặc định chung cho \\\"Umount modules\\\" trong Hồ sơ ứng dụng. Nếu được bật, mọi thay đổi hệ thống do các module gây ra sẽ bị gỡ bỏ khỏi hệ thống và các ứng dụng chưa thiết lập hồ sơ.</string>\n    <string name=\"profile_umount_modules_summary\">Bật tùy chọn này sẽ cho phép KernelSU khôi phục mọi file đã được các module sửa đổi trong ứng dụng này.</string>\n    <string name=\"module_update\">Cập nhật</string>\n    <string name=\"module_downloading\">Đang tải xuống module: %s</string>\n    <string name=\"module_start_downloading\">Bắt đầu tải xuống: %s</string>\n    <string name=\"new_version_available\">Phiên bản mới %s đã có sẵn, nhấn để cập nhật!</string>\n    <string name=\"home_learn_kernelsu\">Tìm hiểu về KernelSU</string>\n    <string name=\"home_click_to_learn_kernelsu\">Tìm hiểu cách cài đặt KernelSU và sử dụng các module.</string>\n    <string name=\"home_support_title\">Ủng hộ chúng tôi</string>\n    <string name=\"home_support_content\">KernelSU sẽ luôn là miễn phí và mã nguồn mở. Tuy nhiên, bạn có thể cho chúng tôi thấy rằng bạn quan tâm bằng cách quyên góp!</string>\n    <string name=\"about_source_code\"><![CDATA[Xem mã nguồn tại %1$s<br/>Tham gia kênh %2$s của chúng tôi]]></string>\n    <string name=\"module_magisk_conflict\">Các module bị vô hiệu hoá do xung đột với Magisk!</string>\n    <string name=\"module_uninstall_confirm\">Bạn có THẬT SỰ muốn gỡ cài đặt module %s không?</string>\n    <string name=\"send_log\">Gửi nhật ký</string>\n    <string name=\"home\">Trang chủ</string>\n    <string name=\"home_not_installed\">Chưa cài đặt</string>\n    <string name=\"home_click_to_install\">Nhấn để cài đặt</string>\n    <string name=\"home_working\">Đang hoạt động</string>\n    <string name=\"home_working_version\">Phiên bản: %d</string>\n    <string name=\"home_unsupported\">Không được hỗ trợ</string>\n    <string name=\"home_unsupported_reason\">KernelSU hiện tại chỉ hỗ trợ Kernel GKI, nhưng bạn có thể patch file .img của thiết bị GKI mà bạn cần.</string>\n    <string name=\"home_kernel\">Phiên bản Kernel</string>\n    <string name=\"home_manager_version\">Phiên bản Trình quản lý</string>\n    <string name=\"home_fingerprint\">Fingerprint</string>\n    <string name=\"home_selinux_status\">Trạng thái SELinux</string>\n    <string name=\"selinux_status_disabled\">Vô hiệu hoá</string>\n    <string name=\"selinux_status_enforcing\">Enforcing</string>\n    <string name=\"selinux_status_permissive\">Permissive</string>\n    <string name=\"selinux_status_unknown\">Không xác định</string>\n    <string name=\"superuser\">Superuser</string>\n    <string name=\"module_failed_to_enable\">Không thể kích hoạt module: %s</string>\n    <string name=\"module_failed_to_disable\">Không thể vô hiệu hoá module: %s</string>\n    <string name=\"module_empty\">Chưa có module nào được cài đặt</string>\n    <string name=\"module\">Module</string>\n    <string name=\"uninstall\">Gỡ cài đặt</string>\n    <string name=\"module_install\">Cài đặt</string>\n    <string name=\"install\">Cài đặt</string>\n    <string name=\"reboot\">Khởi động lại</string>\n    <string name=\"settings\">Cài đặt</string>\n    <string name=\"reboot_userspace\">Khởi động lại mềm</string>\n    <string name=\"reboot_recovery\">Khởi động lại vào Recovery</string>\n    <string name=\"reboot_bootloader\">Khởi động lại vào Bootloader</string>\n    <string name=\"reboot_download\">Khởi động lại vào Download</string>\n    <string name=\"reboot_edl\">Khởi động lại vào EDL</string>\n    <string name=\"about\">Thông tin</string>\n    <string name=\"module_uninstall_success\">%s đã được gỡ cài đặt</string>\n    <string name=\"module_uninstall_failed\">Gỡ cài đặt thất bại: %s</string>\n    <string name=\"module_version\">Phiên bản</string>\n    <string name=\"module_author\">Tác giả</string>\n    <string name=\"show_system_apps\">Hiển thị các ứng dụng hệ thống</string>\n    <string name=\"safe_mode\">Chế độ an toàn</string>\n    <string name=\"reboot_to_apply\">Khởi động lại để có hiệu lực</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/guide/what-is-kernelsu.html</string>\n    <string name=\"profile_selinux_domain\">Tên miền</string>\n    <string name=\"profile_selinux_rules\">Quy tắc</string>\n    <string name=\"launch_app\">Khởi chạy</string>\n    <string name=\"restart_app\">Khởi động lại</string>\n    <string name=\"profile_namespace\">Không gian tên</string>\n    <string name=\"profile_capabilities\">Tính tương thích</string>\n    <string name=\"failed_to_update_sepolicy\">Cập nhật quy tắc SELinux cho %s thất bại</string>\n    <string name=\"force_stop_app\">Buộc dừng</string>\n    <string name=\"profile_namespace_inherited\">Thừa hưởng</string>\n    <string name=\"profile_namespace_global\">Toàn cục</string>\n    <string name=\"profile_namespace_individual\">Riêng biệt</string>\n    <string name=\"profile_selinux_context\">Bối cảnh SELinux</string>\n    <string name=\"profile_umount_modules\">Umount modules</string>\n    <string name=\"require_kernel_version\">Phiên bản KernelSU hiện tại %1$d quá thấp để Trình quản lý hoạt động bình thường. Vui lòng cập nhật lên phiên bản %2$d hoặc cao hơn!</string>\n    <string name=\"app_profile_template_import_success\">Đã nhập thành công</string>\n    <string name=\"app_profile_export_to_clipboard\">Xuất vào bộ nhớ tạm</string>\n    <string name=\"app_profile_template_export_empty\">Không tìm thấy mẫu cục bộ để xuất!</string>\n    <string name=\"app_profile_template_id_exist\">ID mẫu đã tồn tại!</string>\n    <string name=\"module_changelog\">Nhật ký thay đổi</string>\n    <string name=\"app_profile_import_from_clipboard\">Nhập từ bộ nhớ tạm</string>\n    <string name=\"app_profile_template_name\">Tên</string>\n    <string name=\"app_profile_template_id_invalid\">ID mẫu không hợp lệ</string>\n    <string name=\"app_profile_template_create\">Tạo mẫu</string>\n    <string name=\"app_profile_import_export\">Nhập/Xuất</string>\n    <string name=\"app_profile_template_save_failed\">Lưu mẫu thất bại</string>\n    <string name=\"app_profile_template_edit\">Chỉnh sửa mẫu</string>\n    <string name=\"settings_profile_template\">Mẫu Hồ sơ ứng dụng</string>\n    <string name=\"app_profile_template_description\">Mô tả</string>\n    <string name=\"app_profile_template_save\">Lưu</string>\n    <string name=\"settings_profile_template_summary\">Quản lý mẫu cục bộ và trực tuyến của Hồ sơ ứng dụng.</string>\n    <string name=\"app_profile_template_delete\">Xóa</string>\n    <string name=\"app_profile_template_import_empty\">Bộ nhớ tạm đang trống!</string>\n    <string name=\"app_profile_template_view\">Xem mẫu</string>\n    <string name=\"app_profile_template_id\">ID</string>\n    <string name=\"enable_web_debugging\">Gỡ lỗi WebView</string>\n    <string name=\"enable_web_debugging_summary\">Có thể sử dụng để gỡ lỗi WebUI. Vui lòng chỉ bật khi cần thiết.</string>\n    <string name=\"grant_root_failed\">Cấp quyền root thất bại!</string>\n    <string name=\"settings_check_update\">Kiểm tra cập nhật</string>\n    <string name=\"settings_check_update_summary\">Tự động kiểm tra cập nhật khi mở ứng dụng.</string>\n    <string name=\"open\">Mở</string>\n    <string name=\"install_inactive_slot\">Cài đặt vào phân vùng không hoạt động (Sau OTA)</string>\n    <string name=\"install_inactive_slot_warning\">Thiết bị của bạn sẽ **BUỘC** phải khởi động vào phân vùng không hoạt động hiện tại sau khi khởi động lại!\\nChỉ dùng tùy chọn này khi cập nhật OTA đã hoàn tất!\\nTiếp tục?</string>\n    <string name=\"settings_uninstall_temporary_message\">Gỡ cài đặt tạm thời KernelSU, khôi phục lại trạng thái ban đầu sau lần khởi động lại tiếp theo.</string>\n    <string name=\"select_kmi\">Chọn KMI</string>\n    <string name=\"install_next\">Kế tiếp</string>\n    <string name=\"direct_install\">Cài đặt trực tiếp (Khuyến nghị)</string>\n    <string name=\"select_file\">Chọn file</string>\n    <string name=\"settings_uninstall\">Gỡ cài đặt</string>\n    <string name=\"settings_uninstall_temporary\">Gỡ cài đặt tạm thời</string>\n    <string name=\"settings_uninstall_permanent\">Gỡ cài đặt vĩnh viễn</string>\n    <string name=\"settings_restore_stock_image\">Khôi phục image gốc</string>\n    <string name=\"settings_uninstall_permanent_message\">Gỡ cài đặt KernelSU (root và tất cả các module) sạch hoàn toàn, trả về trạng thái ban đầu.</string>\n    <string name=\"settings_restore_stock_image_message\">Khôi phục lại image gốc (nếu có bản sao lưu), thường được sử dụng trước OTA; nếu bạn cần gỡ hẳn KernelSU, hãy sử dụng \\\"Gỡ cài đặt vĩnh viễn\\\".</string>\n    <string name=\"flashing\">Đang Flash...</string>\n    <string name=\"flash_success\">Flash thành công</string>\n    <string name=\"flash_failed\">Flash thất bại</string>\n    <string name=\"selected_lkm\">LKM đã chọn: %s</string>\n    <string name=\"select_file_tip\">Phân vùng image %1$s được khuyến nghị</string>\n    <string name=\"save_log\">Lưu nhật ký</string>\n    <string name=\"module_sort_action_first\">Khởi chạy trước</string>\n    <string name=\"module_install_prompt_with_name\">Các module sau đây sẽ được cài đặt: %1$s</string>\n    <string name=\"confirm\">Xác nhận</string>\n    <string name=\"module_sort_enabled_first\">Bật trước</string>\n    <string name=\"su_not_allowed\">Không thể cấp quyền Superuser cho %s</string>\n    <string name=\"action\">Khởi chạy</string>\n    <string name=\"log_saved\">Đã lưu nhật ký</string>\n    <string name=\"settings_module_check_update\">Kiểm tra cập nhật module</string>\n    <string name=\"install_upload_lkm_file\">Sử dụng file LKM cục bộ</string>\n    <string name=\"install_only_support_ko_file\">Chỉ hỗ trợ các file .ko</string>\n    <string name=\"settings_sucompat\">Lệnh SU cổ điển</string>\n    <string name=\"settings_sucompat_summary\">Cho phép truy cập root thông qua /system/bin/su, trong các tiến trình mới.</string>\n    <string name=\"settings_kernel_umount\">Umount modules (Kernel)</string>\n    <string name=\"settings_kernel_umount_summary\">Umount modules khỏi Kernel trong Hồ sơ ứng dụng.</string>\n    <string name=\"processing\">Đang xử lý…</string>\n    <string name=\"refresh_pulling\">Kéo xuống để làm mới</string>\n    <string name=\"refresh_release\">Thả để làm mới</string>\n    <string name=\"refresh_refresh\">Đang làm mới…</string>\n    <string name=\"refresh_complete\">Đã làm mới thành công</string>\n    <string name=\"install_select_partition\">Chọn phân vùng</string>\n    <string name=\"app_profile_affects_following_apps\">Ảnh hưởng đến các ứng dụng sau</string>\n    <string name=\"group_contains_apps\">Chứa %d ứng dụng</string>\n    <string name=\"metamodule_uninstall_confirm\">Bạn có chắc chắn muốn gỡ cài đặt module %s không? Thao tác này sẽ ảnh hưởng đến tất cả các module và một số tính năng do siêu module cung cấp (chẳng hạn như mount) sẽ không còn hoạt động nữa.</string>\n    <string name=\"undo\">Hoàn tác</string>\n    <string name=\"module_undo_uninstall_success\">Đã huỷ việc gỡ cài đặt %s thành công</string>\n    <string name=\"module_undo_uninstall_failed\">Hoàn tác gỡ cài đặt: %s thất bại</string>\n    <string name=\"settings_theme\">Chủ đề</string>\n    <string name=\"settings_theme_summary\">Chọn chủ đề ứng dụng.</string>\n    <string name=\"settings_theme_mode_system\">Mặc định theo hệ thống</string>\n    <string name=\"settings_theme_mode_light\">Sáng</string>\n    <string name=\"settings_theme_mode_dark\">Tối</string>\n    <string name=\"settings_key_color_default\">Mặc định</string>\n    <string name=\"color_blue\">Xanh dương</string>\n    <string name=\"color_red\">Đỏ</string>\n    <string name=\"color_green\">Xanh lục</string>\n    <string name=\"color_purple\">Tím</string>\n    <string name=\"color_orange\">Cam</string>\n    <string name=\"color_teal\">Xanh mòng két</string>\n    <string name=\"color_pink\">Hồng</string>\n    <string name=\"color_brown\">Nâu</string>\n    <string name=\"settings_key_color\">Màu nhấn</string>\n    <string name=\"feature_status_managed_summary\">Tính năng này được quản lý bởi một module</string>\n    <string name=\"feature_status_unsupported_summary\">Kernel không hỗ trợ tính năng này</string>\n    <string name=\"module_repos\">Thư viện</string>\n    <string name=\"module_repos_sort_name\">Tên (A → Z)</string>\n    <string name=\"module_repos_source_code\">Mã nguồn</string>\n    <string name=\"home_gki_warning\">Bắt đầu từ v3.0.0, chế độ GKI sẽ chỉ được sử dụng trong môi trường thử nghiệm. Chúng tôi không khuyến nghị sử dụng chế độ này và các file image sẽ không còn được cung cấp nữa.</string>\n    <string name=\"network_offline\">Không có kết nối Internet</string>\n    <string name=\"network_retry\">Thử lại</string>\n    <string name=\"tab_readme\">README</string>\n    <string name=\"tab_releases\">Releases</string>\n    <string name=\"tab_info\">Info</string>\n    <string name=\"safe_mode_module_disabled\">Việc cài đặt module bị vô hiệu hoá ở chế độ an toàn</string>\n    <string name=\"settings_mode_enable_by_default\">Bật (Mặc định)</string>\n    <string name=\"settings_mode_disable_until_reboot\">Tắt cho đến khi Khởi động lại</string>\n    <string name=\"settings_mode_disable_always\">Luôn Tắt</string>\n    <string name=\"module_shortcut_title\">Tạo shortcut</string>\n    <string name=\"module_shortcut_name_label\">Tên shortcut</string>\n    <string name=\"module_shortcut_icon_pick\">Chọn icon tuỳ chỉnh</string>\n    <string name=\"module_shortcut_not_supported\">Launcher không hỗ trợ shortcut.</string>\n    <string name=\"module_shortcut_created\">Shortcut đã được tạo.</string>\n    <string name=\"module_shortcut_updated\">Shortcut đã được cập nhật.</string>\n    <string name=\"module_shortcut_delete\">Xoá shortcut</string>\n    <string name=\"module_shortcut_permission_tip_xiaomi\">Vui lòng bật quyền \\\"Shortcut trên màn hình\\\" cho ứng dụng này trong Cài đặt Xiaomi.</string>\n    <string name=\"module_shortcut_permission_tip_oppo\">Vui lòng bật quyền \\\"Shortcut trên màn hình\\\" cho ứng dụng này trong Cài đặt OPPO.</string>\n    <string name=\"module_shortcut_permission_tip_default\">Nếu việc tạo shortcut thất bại! Vui lòng bật quyền \\\"Shortcut trên màn hình\\\" cho ứng dụng này trong Cài đặt hệ thống.</string>\n    <string name=\"no_such_module\">Module %s không tồn tại</string>\n    <string name=\"module_unavailable\">Module %s đã bị Vô hiệu hoá, Chờ cập nhật hoặc Gỡ cài đặt</string>\n    <string name=\"select_file_tip_nogki\">Vui lòng chọn file .img của thiết bị GKI mà bạn muốn patch</string>\n    <string name=\"current_kmi\">Phiên bản KMI của thiết bị này: %s</string>\n    <string name=\"current_device_kmi\">KMI của thiết bị này</string>\n    <string name=\"module_action_success\">Khởi chạy module đã được thực thi.</string>\n    <string name=\"color_deep_purple\">Tím đậm</string>\n    <string name=\"color_indigo\">Chàm</string>\n    <string name=\"color_cyan\">Xanh lam</string>\n    <string name=\"color_yellow\">Vàng</string>\n    <string name=\"color_amber\">Hổ phách</string>\n    <string name=\"color_blue_grey\">Xám xanh</string>\n    <string name=\"color_sakura\">Hoa anh đào</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-zh-rCN/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">主页</string>\n    <string name=\"home_not_installed\">未安装</string>\n    <string name=\"home_click_to_install\">点击安装</string>\n    <string name=\"home_working\">工作中</string>\n    <string name=\"home_working_version\">版本：%d</string>\n    <string name=\"home_unsupported\">不支持</string>\n    <string name=\"home_unsupported_reason\">KernelSU 现在只支持 GKI 内核，但是你可以为 GKI 设备补丁镜像</string>\n    <string name=\"home_version_mismatch\">管理器版本 (%1$d) 与 KernelSU 驱动版本 (%2$d) 不匹配。</string>\n    <string name=\"home_kernel\">内核版本</string>\n    <string name=\"home_manager_version\">管理器版本</string>\n    <string name=\"home_fingerprint\">系统指纹</string>\n    <string name=\"home_selinux_status\">SELinux 状态</string>\n    <string name=\"selinux_status_disabled\">被禁用</string>\n    <string name=\"selinux_status_enforcing\">强制执行</string>\n    <string name=\"selinux_status_permissive\">宽容模式</string>\n    <string name=\"selinux_status_unknown\">未知</string>\n    <string name=\"superuser\">超级用户</string>\n    <string name=\"module_failed_to_enable\">无法启用模块：%s</string>\n    <string name=\"module_failed_to_disable\">无法禁用模块：%s</string>\n    <string name=\"module_empty\">没有安装模块</string>\n    <string name=\"module\">模块</string>\n    <string name=\"module_sort_action_first\">可执行优先</string>\n    <string name=\"module_sort_enabled_first\">已启用优先</string>\n    <string name=\"module_repos\">模块仓库</string>\n    <string name=\"module_repos_sort_name\">按名称排序</string>\n    <string name=\"module_repos_source_code\">源码</string>\n    <string name=\"uninstall\">卸载</string>\n    <string name=\"module_install\">安装</string>\n    <string name=\"install\">安装</string>\n    <string name=\"reboot\">重启</string>\n    <string name=\"settings\">设置</string>\n    <string name=\"reboot_soft\">软重启</string>\n    <string name=\"reboot_userspace\">用户空间重启</string>\n    <string name=\"reboot_recovery\">重启到 Recovery</string>\n    <string name=\"reboot_bootloader\">重启到 BootLoader</string>\n    <string name=\"reboot_download\">重启到 Download</string>\n    <string name=\"reboot_edl\">重启到 EDL</string>\n    <string name=\"about\">关于</string>\n    <string name=\"module_uninstall_confirm\">确定要卸载模块 %s 吗？</string>\n    <string name=\"metamodule_uninstall_confirm\">您确定要卸载模块 %s 吗？此操作将影响所有模块，并且元模块提供的某些功能（如挂载）将不再工作</string>\n    <string name=\"module_uninstall_success\">%s 已卸载</string>\n    <string name=\"module_uninstall_failed\">卸载失败：%s</string>\n    <string name=\"module_version\">版本</string>\n    <string name=\"module_author\">作者</string>\n    <string name=\"show_system_apps\">显示系统应用</string>\n    <string name=\"send_log\">发送日志</string>\n    <string name=\"safe_mode\">安全模式</string>\n    <string name=\"jailbreak_mode\">越狱模式</string>\n    <string name=\"home_jailbreak\">越狱</string>\n    <string name=\"jailbreak_timeout\">越狱可能失败，请检查日志</string>\n    <string name=\"safe_mode_module_disabled\">处于安全模式下，禁止安装模块</string>\n    <string name=\"reboot_to_apply\">重启生效</string>\n    <string name=\"module_magisk_conflict\">因与 Magisk 有冲突，所有模块不可用！</string>\n    <string name=\"home_learn_kernelsu\">了解 KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/zh_CN/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">了解如何安装 KernelSU 以及如何开发模块</string>\n    <string name=\"home_support_title\">支持开发</string>\n    <string name=\"home_support_content\">KernelSU 将保持免费开源，向开发者捐赠以表示支持</string>\n    <string name=\"home_gki_warning\">自 v3.0.0 起 GKI 工作模式将仅用于测试环境，我们不建议用于日常使用，也不再提供镜像文件</string>\n    <string name=\"about_source_code\"><![CDATA[在 %1$s 查看源码<br/>加入我们的 %2$s 频道<br/>加入我们的 <b><a href=\"https://pd.qq.com/s/8lipl1brp\">QQ</a></b> 频道]]></string>\n    <string name=\"profile_default\">默认</string>\n    <string name=\"profile_template\">模版</string>\n    <string name=\"profile_custom\">自定义</string>\n    <string name=\"profile_name\">名称</string>\n    <string name=\"profile_namespace\">挂载命名空间</string>\n    <string name=\"profile_namespace_inherited\">继承</string>\n    <string name=\"profile_namespace_global\">全局</string>\n    <string name=\"profile_namespace_individual\">独立</string>\n    <string name=\"profile_groups\">组</string>\n    <string name=\"profile_capabilities\">权能</string>\n    <string name=\"profile_selinux_context\">SELinux</string>\n    <string name=\"profile_umount_modules\">卸载模块</string>\n    <string name=\"failed_to_update_app_profile\">为 %s 更新 App Profile 失败</string>\n    <string name=\"require_kernel_version\">当前 KernelSU 版本 %1$d 过低，管理器无法正常工作，请将内核 KernelSU 版本升级至 %2$d 或以上！</string>\n    <string name=\"settings_umount_modules_default\">默认卸载模块</string>\n    <string name=\"settings_umount_modules_default_summary\">App Profile 中「卸载模块」的全局默认值，如果启用，会将未自定义 Profile 的应用移除所有模块针对系统的修改</string>\n    <string name=\"profile_umount_modules_summary\">启用该选项后将允许 KernelSU 为本应用还原被模块修改过的文件</string>\n    <string name=\"profile_selinux_domain\">域</string>\n    <string name=\"profile_selinux_rules\">规则</string>\n    <string name=\"module_update\">更新</string>\n    <string name=\"module_downloading\">正在下载模块：%s</string>\n    <string name=\"module_start_downloading\">开始下载：%s</string>\n    <string name=\"new_version_available\">发现新版本：%s，点击升级！</string>\n    <string name=\"launch_app\">启动</string>\n    <string name=\"force_stop_app\">强制停止</string>\n    <string name=\"restart_app\">重新启动</string>\n    <string name=\"failed_to_update_sepolicy\">为 %s 更新 SELinux 策略失败</string>\n    <string name=\"su_not_allowed\">无法授予 %s 超级用户权限</string>\n    <string name=\"module_changelog\">更新日志</string>\n    <string name=\"settings_profile_template\">App Profile 模版</string>\n    <string name=\"settings_profile_template_summary\">管理本地和在线的 App Profile 模版</string>\n    <string name=\"app_profile_template_create\">创建模版</string>\n    <string name=\"app_profile_template_edit\">编辑模版</string>\n    <string name=\"app_profile_template_id\">模版 ID</string>\n    <string name=\"app_profile_template_id_invalid\">模版 ID 不合法</string>\n    <string name=\"app_profile_template_name\">名字</string>\n    <string name=\"app_profile_template_description\">描述</string>\n    <string name=\"app_profile_template_save\">保存</string>\n    <string name=\"app_profile_template_delete\">删除</string>\n    <string name=\"app_profile_template_view\">查看模版</string>\n    <string name=\"app_profile_template_id_exist\">模版 ID 已存在！</string>\n    <string name=\"app_profile_import_export\">导入/导出</string>\n    <string name=\"app_profile_import_from_clipboard\">从剪切板导入</string>\n    <string name=\"app_profile_export_to_clipboard\">导出到剪切板</string>\n    <string name=\"app_profile_template_export_empty\">没有可以导出的本地模板！</string>\n    <string name=\"app_profile_template_import_success\">导入成功</string>\n    <string name=\"app_profile_template_save_failed\">模版保存失败</string>\n    <string name=\"app_profile_template_import_empty\">剪切板为空！</string>\n    <string name=\"app_profile_affects_following_apps\">影响以下应用</string>\n    <string name=\"settings_check_update\">检查更新</string>\n    <string name=\"settings_check_update_summary\">在应用启动后自动检查是否有最新版</string>\n    <string name=\"settings_enable_predictive_back\">预测性返回手势</string>\n    <string name=\"settings_enable_predictive_back_summary\">启用对预测性返回手势的支持</string>\n    <string name=\"settings_module_check_update\">检查模块更新</string>\n    <string name=\"grant_root_failed\">获取 root 失败！</string>\n    <string name=\"home_pr_build_warning\">当前为 PR 调试构建，请勿在生产环境中使用！</string>\n    <string name=\"home_pr_kernel_warning\">当前内核包含 PR 签名支持，这不是一个生产内核！</string>\n    <string name=\"action\">执行</string>\n    <string name=\"open\">打开</string>\n    <string name=\"enable_web_debugging\">WebView 调试</string>\n    <string name=\"enable_web_debugging_summary\">可用于调试 WebUI，请仅在需要时启用</string>\n    <string name=\"direct_install\">直接安装（推荐）</string>\n    <string name=\"select_file\">选择一个文件</string>\n    <string name=\"install_inactive_slot\">安装到未使用的槽位（OTA 后）</string>\n    <string name=\"install_inactive_slot_warning\">将在重启后强制切换到另一个槽位！注意只能在 OTA 更新完成后的重启之前使用</string>\n    <string name=\"jailbreak_flash_warning\">当前处于**越狱模式**。在**未解锁 Bootloader** 的设备上刷写分区会破坏 AVB（Android 验证启动），可能导致设备**无法开机**。\\n\\n请确保 Bootloader 已解锁后再继续操作！</string>\n    <string name=\"jailbreak_flash_warning_countdown\">继续 (%1$d)</string>\n    <string name=\"install_next\">下一步</string>\n    <string name=\"install_select_partition\">选择分区</string>\n    <string name=\"install_upload_lkm_file\">使用本地 LKM 文件</string>\n    <string name=\"install_only_support_ko_file\">仅支持选择 .ko 文件</string>\n    <string name=\"select_file_tip\">建议选择 %1$s 分区镜像</string>\n    <string name=\"select_kmi\">选择 KMI</string>\n    <string name=\"settings_uninstall\">卸载</string>\n    <string name=\"settings_uninstall_temporary\">临时卸载</string>\n    <string name=\"settings_uninstall_permanent\">永久卸载</string>\n    <string name=\"settings_restore_stock_image\">恢复原厂镜像</string>\n    <string name=\"settings_uninstall_temporary_message\">临时卸载 KernelSU，下次重启后恢复至原始状态</string>\n    <string name=\"settings_uninstall_permanent_message\">完全并永久卸载 KernelSU（Root 权限和所有模块）</string>\n    <string name=\"settings_restore_stock_image_message\">恢复原厂镜像（若存在备份），一般在 OTA 前使用；如果你需要卸载 KernelSU，请使用「永久卸载」</string>\n    <string name=\"flashing\">刷写中</string>\n    <string name=\"flash_success\">刷写完成</string>\n    <string name=\"flash_failed\">刷写失败</string>\n    <string name=\"selected_lkm\">已选择的 LKM：%s</string>\n    <string name=\"save_log\">保存日志</string>\n    <string name=\"log_saved\">日志已保存</string>\n    <string name=\"settings_mode_enable_by_default\">启用（默认）</string>\n    <string name=\"settings_mode_disable_until_reboot\">禁用直到下次重启</string>\n    <string name=\"settings_mode_disable_always\">始终禁用</string>\n    <string name=\"settings_sucompat\">传统 su 命令支持</string>\n    <string name=\"settings_sucompat_summary\">允许通过 /system/bin/su 获取 Root 权限</string>\n    <string name=\"settings_kernel_umount\">内核处理卸载模块</string>\n    <string name=\"settings_kernel_umount_summary\">在内核给需要的应用卸载模块</string>\n    <string name=\"module_install_prompt_with_name\">将安装以下模块：%1$s</string>\n    <string name=\"confirm\">确认</string>\n    <string name=\"processing\">处理中…</string>\n    <string name=\"refresh_pulling\">下拉刷新</string>\n    <string name=\"refresh_release\">松开刷新</string>\n    <string name=\"refresh_refresh\">正在刷新…</string>\n    <string name=\"refresh_complete\">刷新成功</string>\n    <string name=\"undo\">撤销</string>\n    <string name=\"module_undo_uninstall_success\">成功撤销卸载 %s</string>\n    <string name=\"module_undo_uninstall_failed\">撤销卸载 %s 失败</string>\n    <string name=\"group_contains_apps\">包含 %d 个应用</string>\n    <string name=\"settings_theme\">主题设置</string>\n    <string name=\"settings_theme_summary\">自定义更多主题选项</string>\n    <string name=\"settings_theme_mode_system\">跟随系统</string>\n    <string name=\"settings_theme_mode_light\">浅色</string>\n    <string name=\"settings_theme_mode_dark\">深色</string>\n    <string name=\"settings_monet\">启用 Monet 颜色</string>\n    <string name=\"settings_key_color\">强调色</string>\n    <string name=\"settings_key_color_default\">默认</string>\n    <string name=\"settings_ui_mode\">界面风格</string>\n    <string name=\"settings_ui_mode_summary\">选择应用的界面风格</string>\n    <string name=\"settings_page_scale\">界面缩放</string>\n    <string name=\"settings_page_scale_summary\">调整全局显示比例</string>\n    <string name=\"settings_color_spec\">色彩标准</string>\n    <string name=\"settings_color_style\">色彩风格</string>\n    <string name=\"color_blue\">蓝色</string>\n    <string name=\"color_red\">红色</string>\n    <string name=\"color_green\">绿色</string>\n    <string name=\"color_purple\">紫色</string>\n    <string name=\"color_orange\">橙色</string>\n    <string name=\"color_teal\">青绿</string>\n    <string name=\"color_pink\">粉色</string>\n    <string name=\"color_brown\">棕色</string>\n    <string name=\"color_sakura\">樱花</string>\n    <string name=\"color_deep_purple\">深紫</string>\n    <string name=\"color_indigo\">靛青</string>\n    <string name=\"color_cyan\">青色</string>\n    <string name=\"color_yellow\">黄色</string>\n    <string name=\"color_amber\">琥珀</string>\n    <string name=\"color_blue_grey\">灰蓝</string>\n    <string name=\"network_offline\">未连接到网络</string>\n    <string name=\"network_retry\">重试</string>\n    <string name=\"tab_readme\">自述</string>\n    <string name=\"tab_releases\">版本</string>\n    <string name=\"tab_info\">信息</string>\n    <string name=\"feature_status_unsupported_summary\">内核不支持此特性</string>\n    <string name=\"feature_status_managed_summary\">此功能由一个模块管理</string>\n    <string name=\"module_shortcut_title\">创建快捷方式</string>\n    <string name=\"module_shortcut_name_label\">快捷方式名称</string>\n    <string name=\"module_shortcut_icon_pick\">选择自定义图标</string>\n    <string name=\"module_shortcut_not_supported\">启动器不支持桌面快捷方式</string>\n    <string name=\"module_shortcut_created\">已在桌面创建快捷方式。</string>\n    <string name=\"module_shortcut_updated\">快捷方式已更新。</string>\n    <string name=\"module_shortcut_delete\">删除快捷方式</string>\n    <string name=\"module_shortcut_permission_tip_xiaomi\">请在 Xiaomi 设置中为本应用启用「桌面快捷方式」权限</string>\n    <string name=\"module_shortcut_permission_tip_oppo\">请在 OPPO 设置中为本应用启用「创建桌面快捷方式」权限</string>\n    <string name=\"module_shortcut_permission_tip_default\">若创建快捷方式失败，请在系统设置中为本应用启用桌面快捷方式权限</string>\n    <string name=\"no_such_module\">模块 %s 不存在</string>\n    <string name=\"module_unavailable\">模块 %s 被禁用，待更新或待卸载</string>\n    <string name=\"select_file_tip_nogki\">请选择要修补的 GKI 设备镜像文件</string>\n    <string name=\"current_kmi\">本设备 KMI 版本: %s</string>\n    <string name=\"current_device_kmi\">当前设备的 KMI</string>\n    <string name=\"module_action_success\">模块 action 执行成功。</string>\n    <string name=\"settings_enable_blur\">模糊</string>\n    <string name=\"settings_enable_blur_summary\">启用顶栏和底栏的模糊效果</string>\n    <string name=\"settings_floating_bottom_bar\">悬浮底栏</string>\n    <string name=\"settings_floating_bottom_bar_summary\">使用 Apple 风格的悬浮底栏</string>\n    <string name=\"settings_enable_glass\">液态玻璃</string>\n    <string name=\"settings_enable_glass_summary\">启用悬浮底栏的液态玻璃效果</string>\n    <string name=\"show_only_primary_user_apps\">只显示主用户应用</string>\n    <string name=\"settings_auto_jailbreak\">自动越狱</string>\n    <string name=\"settings_auto_jailbreak_summary\">开机检测到 Permissive SELinux 时自动使用 Magica 提权。需要授予本应用自启动权限。</string>\n    <string name=\"allow_shell\">总是给 shell 授予 root 权限</string>\n    <string name=\"allow_shell_summary\">总是允许 adb shell 调用 su ，非必要请勿开启。</string>\n    <string name=\"enable_adb\">启动时强制启用 ADB 调试</string>\n    <string name=\"enable_adb_summary\">强制允许 USB 调试并取消 adb 认证，非必要请勿开启。</string>\n    <string name=\"advanced_options\">高级选项</string>\n    <string name=\"expand\">展开</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-zh-rHK/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">首頁</string>\n    <string name=\"home_not_installed\">未安裝</string>\n    <string name=\"home_click_to_install\">按一下以安裝</string>\n    <string name=\"home_working\">運作中</string>\n    <string name=\"home_working_version\">KernelSU 版本：%d</string>\n    <string name=\"home_unsupported\">不支援</string>\n    <string name=\"home_unsupported_reason\">KernelSU 現在僅支援 GKI 核心</string>\n    <string name=\"home_kernel\">核心</string>\n    <string name=\"home_manager_version\">管理器版本</string>\n    <string name=\"home_fingerprint\">指紋</string>\n    <string name=\"home_selinux_status\">SELinux 狀態</string>\n    <string name=\"selinux_status_disabled\">已停用</string>\n    <string name=\"selinux_status_enforcing\">強制</string>\n    <string name=\"selinux_status_permissive\">寬鬆</string>\n    <string name=\"selinux_status_unknown\">未知</string>\n    <string name=\"superuser\">超級使用者</string>\n    <string name=\"module_failed_to_enable\">無法啟用模組：%s</string>\n    <string name=\"module_failed_to_disable\">無法停用模組：%s</string>\n    <string name=\"module_empty\">尚未安裝模組</string>\n    <string name=\"module\">模組</string>\n    <string name=\"uninstall\">解除安裝</string>\n    <string name=\"module_install\">安裝</string>\n    <string name=\"install\">安裝</string>\n    <string name=\"reboot\">重新啟動</string>\n    <string name=\"settings\">設定</string>\n    <string name=\"reboot_userspace\">軟啟動</string>\n    <string name=\"reboot_recovery\">重新啟動至 Recovery</string>\n    <string name=\"reboot_bootloader\">重新啟動至 Bootloader</string>\n    <string name=\"reboot_download\">重新啟動至 Download</string>\n    <string name=\"reboot_edl\">重新啟動至 EDL</string>\n    <string name=\"about\">關於</string>\n    <string name=\"module_uninstall_confirm\">您確定要解除安裝模組「%s」嗎？</string>\n    <string name=\"module_uninstall_success\">「%s」已解除安裝</string>\n    <string name=\"module_uninstall_failed\">無法解除安裝：%s</string>\n    <string name=\"module_version\">版本</string>\n    <string name=\"module_author\">作者</string>\n    <string name=\"show_system_apps\">顯示系統應用程式</string>\n    <string name=\"send_log\">傳送記錄</string>\n    <string name=\"safe_mode\">安全模式</string>\n    <string name=\"reboot_to_apply\">重新啟動以生效</string>\n    <string name=\"module_magisk_conflict\">模組已停用，因其與 Magisk 的模組存在衝突！</string>\n    <string name=\"home_learn_kernelsu\">深入瞭解 KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/zh_TW/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">瞭解如何安裝 KernelSU 以及如何開發模組</string>\n    <string name=\"home_support_title\">支援開發</string>\n    <string name=\"home_support_content\">KernelSU 將保持免費和開源，您可以考慮向開發人員贊助以表示支持。</string>\n    <string name=\"about_source_code\"><![CDATA[在 %1$s 中檢視原始碼<br/>加入我們的 %2$s 頻道]]></string>\n    <string name=\"profile_default\">預設</string>\n    <string name=\"profile_name\">設定檔名稱</string>\n    <string name=\"profile_template\">範本</string>\n    <string name=\"profile_namespace_inherited\">繼承</string>\n    <string name=\"profile_namespace_global\">全域</string>\n    <string name=\"profile_capabilities\">功能</string>\n    <string name=\"profile_umount_modules\">卸載模組</string>\n    <string name=\"failed_to_update_app_profile\">無法更新 %s 應用程式設定檔</string>\n    <string name=\"profile_selinux_rules\">規則</string>\n    <string name=\"require_kernel_version\">目前 KernelSU 版本 %1$d 過低，管理器無法正常運作。請升級至 %2$d 或更高版本！</string>\n    <string name=\"settings_umount_modules_default_summary\">應用程式設定檔中「解除安裝模組」的全域預設值，如果啟用，將會為沒有設定檔的應用程式移除所有模組針對系統的修改。</string>\n    <string name=\"profile_umount_modules_summary\">啟用此選項將允許 KernelSU 為這個應用程式還原任何被模組修改過的檔案。</string>\n    <string name=\"profile_selinux_domain\">網域</string>\n    <string name=\"module_update\">更新</string>\n    <string name=\"profile_custom\">自訂</string>\n    <string name=\"profile_namespace\">掛載命名空間</string>\n    <string name=\"profile_namespace_individual\">個人</string>\n    <string name=\"profile_groups\">群組</string>\n    <string name=\"profile_selinux_context\">SELinux 環境</string>\n    <string name=\"settings_umount_modules_default\">預設解除安裝模組</string>\n    <string name=\"module_downloading\">正在下載模組：%s</string>\n    <string name=\"module_start_downloading\">開始下載：%s</string>\n    <string name=\"new_version_available\">新版本：%s 已可供使用，按一下以升級</string>\n    <string name=\"launch_app\">啟動</string>\n    <string name=\"force_stop_app\">強制停止</string>\n    <string name=\"restart_app\">重新啟動</string>\n    <string name=\"failed_to_update_sepolicy\">無法為 %s 更新 SELinux 規則</string>\n    <string name=\"module_changelog\">變更記錄</string>\n    <string name=\"app_profile_template_import_success\">成功匯出</string>\n    <string name=\"app_profile_export_to_clipboard\">導出到剪貼板</string>\n    <string name=\"app_profile_template_export_empty\">本地沒有模板可匯出！</string>\n    <string name=\"app_profile_template_id_exist\">模板 ID 已存在！</string>\n    <string name=\"app_profile_import_from_clipboard\">從剪貼簿匯入</string>\n    <string name=\"app_profile_template_name\">名字</string>\n    <string name=\"app_profile_template_id_invalid\">模板 ID 無效</string>\n    <string name=\"app_profile_template_create\">創建模板</string>\n    <string name=\"app_profile_import_export\">匯出 / 匯入</string>\n    <string name=\"app_profile_template_save_failed\">模板儲存失敗</string>\n    <string name=\"app_profile_template_edit\">編輯模板</string>\n    <string name=\"app_profile_template_id\">模板 ID</string>\n    <string name=\"settings_profile_template\">App Profile 模板</string>\n    <string name=\"app_profile_template_description\">描述</string>\n    <string name=\"app_profile_template_save\">儲存</string>\n    <string name=\"settings_profile_template_summary\">管理本地和線上的 App Profile 模板</string>\n    <string name=\"app_profile_template_delete\">刪除</string>\n    <string name=\"app_profile_template_import_empty\">剪貼簿沒有內容！</string>\n    <string name=\"app_profile_template_view\">查看模板</string>\n    <string name=\"enable_web_debugging\">WebView 除錯模式</string>\n    <string name=\"enable_web_debugging_summary\">可用於 WebUI 除錯。請僅在需要時啟用。</string>\n    <string name=\"direct_install\">直接安裝（建議）</string>\n    <string name=\"select_file\">選取檔案</string>\n    <string name=\"install_inactive_slot\">安裝至非活動插槽（OTA 後）</string>\n    <string name=\"install_inactive_slot_warning\">重新啟動後將強制由目前非活動插槽開機！請僅在 OTA 完成後才使用此選項。</string>\n    <string name=\"install_next\">下一個</string>\n    <string name=\"select_kmi\">選取 KMI</string>\n    <string name=\"select_file_tip\">建議使用 %1$s 分割區映像</string>\n    <string name=\"grant_root_failed\">授予root權限失敗！</string>\n    <string name=\"open\">開啟</string>\n    <string name=\"settings_check_update\">檢查更新</string>\n    <string name=\"settings_check_update_summary\">開啟程式時自動檢查更新。</string>\n    <string name=\"settings_uninstall\">解除安裝</string>\n    <string name=\"save_log\">儲存日誌</string>\n    <string name=\"processing\">處理中…</string>\n    <string name=\"refresh_pulling\">下拉刷新</string>\n    <string name=\"refresh_release\">鬆開刷新</string>\n    <string name=\"refresh_refresh\">正在刷新…</string>\n    <string name=\"refresh_complete\">刷新成功</string>\n    <string name=\"settings_sucompat\">傳統 su 命令支援</string>\n    <string name=\"settings_sucompat_summary\">允許透過 /system/bin/su 獲取 Root 權限，僅對新行程生效。</string>\n    <string name=\"settings_kernel_umount\">核心處理解除掛載模組</string>\n    <string name=\"settings_kernel_umount_summary\">在核心為需要的應用程式解除掛載模組</string>\n    <string name=\"module_shortcut_title\">建立捷徑</string>\n    <string name=\"module_shortcut_name_label\">捷徑名稱</string>\n    <string name=\"module_shortcut_icon_pick\">選擇自訂圖示</string>\n    <string name=\"module_shortcut_not_supported\">啟動器不支援桌面捷徑。</string>\n    <string name=\"module_shortcut_created\">已在桌面建立捷徑。</string>\n    <string name=\"module_shortcut_updated\">捷徑已更新。</string>\n    <string name=\"module_shortcut_delete\">刪除捷徑</string>\n    <string name=\"module_shortcut_permission_tip_xiaomi\">請在 Xiaomi 設定中為此程式啟用「建立桌面捷徑」權限。</string>\n    <string name=\"module_shortcut_permission_tip_oppo\">請在 OPPO 設定中為此程式啟用「桌面捷徑」權限。</string>\n    <string name=\"module_shortcut_permission_tip_default\">如未能建立捷徑，請在系統設定中為此程式開啟桌面捷徑權限。</string>\n    <string name=\"current_device_kmi\">此裝置 KMI</string>\n    <string name=\"current_kmi\">此裝置的 KMI 版本：%s</string>\n    <string name=\"settings_key_color_default\">預設</string>\n    <string name=\"color_blue\">藍色</string>\n    <string name=\"color_red\">紅色</string>\n    <string name=\"color_green\">綠色</string>\n    <string name=\"color_purple\">紫色</string>\n    <string name=\"color_orange\">橙色</string>\n    <string name=\"color_teal\">藍綠色</string>\n    <string name=\"color_pink\">粉紅色</string>\n    <string name=\"color_brown\">啡色</string>\n    <string name=\"network_offline\">未連接網絡</string>\n    <string name=\"network_retry\">重試</string>\n    <string name=\"tab_readme\">自述文件</string>\n    <string name=\"tab_releases\">版本</string>\n    <string name=\"tab_info\">資訊</string>\n    <string name=\"no_such_module\">模組 %s 不存在</string>\n    <string name=\"module_unavailable\">模組 %s 已停用、更新中或正等待移除</string>\n    <string name=\"select_file_tip_nogki\">請選擇需要修補的 GKI 裝置映像檔</string>\n    <string name=\"settings_theme_summary\">選擇主題模式。</string>\n    <string name=\"settings_theme_mode_system\">跟隨系統</string>\n    <string name=\"settings_theme_mode_light\">淺色模式</string>\n    <string name=\"settings_theme_mode_dark\">深色模式</string>\n    <string name=\"settings_key_color\">主題色</string>\n    <string name=\"group_contains_apps\">內含 %d 個應用程式</string>\n    <string name=\"app_profile_affects_following_apps\">受影響程式如下</string>\n    <string name=\"settings_module_check_update\">檢查模組更新</string>\n    <string name=\"action\">執行</string>\n    <string name=\"settings_theme\">介面主題</string>\n    <string name=\"install_select_partition\">選取分割區</string>\n    <string name=\"install_upload_lkm_file\">使用本地 LKM 檔案</string>\n    <string name=\"install_only_support_ko_file\">僅支援 .ko 格式檔案</string>\n    <string name=\"settings_uninstall_temporary\">暫時解除安裝</string>\n    <string name=\"settings_uninstall_permanent\">永久解除安裝</string>\n    <string name=\"settings_restore_stock_image\">恢復原廠映像</string>\n    <string name=\"settings_uninstall_temporary_message\">暫時解除安裝 KernelSU，下次重新啟動後恢復原狀。</string>\n    <string name=\"settings_uninstall_permanent_message\">徹底永久解除安裝 KernelSU（Root 權限及所有模組）。</string>\n    <string name=\"settings_restore_stock_image_message\">恢復原廠映像（如有備份），通常用於 OTA 前；如需解除安裝 KernelSU，請使用「永久解除安裝」。</string>\n    <string name=\"flashing\">正在刷入</string>\n    <string name=\"flash_success\">刷入成功</string>\n    <string name=\"flash_failed\">刷入失敗</string>\n    <string name=\"selected_lkm\">已選取 LKM：%s</string>\n    <string name=\"log_saved\">日誌已儲存</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/values-zh-rTW/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"home\">首頁</string>\n    <string name=\"home_not_installed\">尚未安裝</string>\n    <string name=\"home_click_to_install\">點選開始安裝</string>\n    <string name=\"home_working\">已開始運作</string>\n    <string name=\"home_working_version\">版本：%d</string>\n    <string name=\"home_unsupported\">未受支援</string>\n    <string name=\"home_unsupported_reason\">KernelSU 目前僅支援 GKI 核心</string>\n    <string name=\"home_kernel\">核心版本</string>\n    <string name=\"home_manager_version\">管理工具</string>\n    <string name=\"home_fingerprint\">指紋資訊</string>\n    <string name=\"home_selinux_status\">SELinux 狀態</string>\n    <string name=\"selinux_status_disabled\">已停用</string>\n    <string name=\"selinux_status_enforcing\">強制執行</string>\n    <string name=\"selinux_status_permissive\">容許執行</string>\n    <string name=\"selinux_status_unknown\">不明</string>\n    <string name=\"superuser\">授權</string>\n    <string name=\"module_failed_to_enable\">無法啟用模組：%s</string>\n    <string name=\"module_failed_to_disable\">無法停用模組：%s</string>\n    <string name=\"module_empty\">查無已安裝的模組</string>\n    <string name=\"module\">模組</string>\n    <string name=\"uninstall\">解除安裝</string>\n    <string name=\"module_install\">安裝</string>\n    <string name=\"install\">安裝</string>\n    <string name=\"reboot\">重新啟動</string>\n    <string name=\"settings\">設定</string>\n    <string name=\"reboot_userspace\">軟重啟</string>\n    <string name=\"reboot_recovery\">重新啟動至 Recovery</string>\n    <string name=\"reboot_bootloader\">重新啟動至 Bootloader</string>\n    <string name=\"reboot_download\">重新啟動至 Download</string>\n    <string name=\"reboot_edl\">重新啟動至 EDL</string>\n    <string name=\"about\">關於</string>\n    <string name=\"module_uninstall_confirm\">你是否要解除安裝「%s」模組？</string>\n    <string name=\"module_uninstall_success\">「%s」已解除安裝</string>\n    <string name=\"module_uninstall_failed\">無法解除安裝：%s</string>\n    <string name=\"module_version\">版本</string>\n    <string name=\"module_author\">作者</string>\n    <string name=\"show_system_apps\">顯示系統程式</string>\n    <string name=\"send_log\">傳送日誌</string>\n    <string name=\"safe_mode\">安全模式</string>\n    <string name=\"reboot_to_apply\">將在重新啟動時生效</string>\n    <string name=\"module_magisk_conflict\">與 Magisk 發生衝突，無法使用模組功能！</string>\n    <string name=\"home_learn_kernelsu\">深入瞭解 KernelSU</string>\n    <string name=\"home_learn_kernelsu_url\">https://kernelsu.org/zh_TW/guide/what-is-kernelsu.html</string>\n    <string name=\"home_click_to_learn_kernelsu\">知曉安裝、使用 KernelSU 本體與其模組功能的方法</string>\n    <string name=\"home_support_title\">協助發展</string>\n    <string name=\"home_support_content\">KernelSU 一向以免費與開放原始碼自居，矢志不渝。若想協助我們，可以用小額捐款表達你對專案發展的大力支持。</string>\n    <string name=\"about_source_code\"><![CDATA[前往 %1$s 檢閱原始碼<br/>前往 %2$s 加入頻道]]></string>\n    <string name=\"profile_umount_modules\">解除掛載模組功能</string>\n    <string name=\"failed_to_update_app_profile\">無法更新「%s」App Profile</string>\n    <string name=\"require_kernel_version\">管理工具無法以老舊的 KernelSU %1$d 版本正常運作。請升級至 %2$d 以上的版本！</string>\n    <string name=\"settings_umount_modules_default\">預設解除掛載模組功能</string>\n    <string name=\"settings_umount_modules_default_summary\">將 App Profile 的全域預設行為設作「解除掛載模組功能」。啟用後，將向未指派 Profile 的應用程式移除模組功能。</string>\n    <string name=\"profile_umount_modules_summary\">啟用選項後，KernelSU 會將應用程式內遭模組修改的檔案恢復原狀。</string>\n    <string name=\"profile_default\">預設</string>\n    <string name=\"profile_custom\">自訂</string>\n    <string name=\"profile_capabilities\">權限</string>\n    <string name=\"profile_selinux_rules\">規則</string>\n    <string name=\"module_downloading\">正在下載模組：%s</string>\n    <string name=\"restart_app\">重新執行</string>\n    <string name=\"profile_template\">範本</string>\n    <string name=\"profile_name\">Profile 名稱</string>\n    <string name=\"profile_namespace\">命名空間掛載</string>\n    <string name=\"profile_namespace_inherited\">繼承</string>\n    <string name=\"profile_namespace_global\">全域</string>\n    <string name=\"profile_namespace_individual\">個體</string>\n    <string name=\"profile_groups\">群組</string>\n    <string name=\"profile_selinux_context\">SELinux 上下文</string>\n    <string name=\"profile_selinux_domain\">定域</string>\n    <string name=\"module_update\">更新</string>\n    <string name=\"module_start_downloading\">準備下載模組：%s</string>\n    <string name=\"new_version_available\">版本 %s 現已開放下載，點選開始更新。</string>\n    <string name=\"launch_app\">開始執行</string>\n    <string name=\"force_stop_app\">強制停止</string>\n    <string name=\"failed_to_update_sepolicy\">無法為「%s」更新 SELinux 規則</string>\n    <string name=\"module_changelog\">更新說明</string>\n    <string name=\"app_profile_template_id_invalid\">範本編號無效</string>\n    <string name=\"app_profile_template_create\">建立範本</string>\n    <string name=\"app_profile_template_edit\">編輯範本</string>\n    <string name=\"app_profile_template_id\">編號</string>\n    <string name=\"settings_profile_template\">App Profile 範本</string>\n    <string name=\"settings_profile_template_summary\">管理 App Profile 的本地、線上範本</string>\n    <string name=\"app_profile_template_import_success\">已成功匯入</string>\n    <string name=\"app_profile_export_to_clipboard\">匯出至剪貼簿</string>\n    <string name=\"app_profile_template_export_empty\">查無可供匯出的本地範本！</string>\n    <string name=\"app_profile_template_id_exist\">編號已由其他範本領有！</string>\n    <string name=\"app_profile_import_from_clipboard\">自剪貼簿匯入</string>\n    <string name=\"app_profile_template_name\">名稱</string>\n    <string name=\"app_profile_import_export\">匯入／匯出</string>\n    <string name=\"app_profile_template_save_failed\">無法儲存範本</string>\n    <string name=\"app_profile_template_description\">說明</string>\n    <string name=\"app_profile_template_save\">儲存</string>\n    <string name=\"app_profile_template_delete\">刪除</string>\n    <string name=\"app_profile_template_import_empty\">查無剪貼簿內容！</string>\n    <string name=\"app_profile_template_view\">檢視範本</string>\n    <string name=\"enable_web_debugging_summary\">旨在偵錯 WebUI。請依自身狀況適時啟用。</string>\n    <string name=\"enable_web_debugging\">啟用 WebView 偵錯</string>\n    <string name=\"grant_root_failed\">無法獲取 Root 權限！</string>\n    <string name=\"open\">開啟</string>\n    <string name=\"settings_check_update\">檢查更新</string>\n    <string name=\"settings_check_update_summary\">開啟本應用程式時，自動檢查更新</string>\n    <string name=\"select_file\">選擇檔案</string>\n    <string name=\"install_inactive_slot\">安裝至非作用擴充槽（適用於 OTA 更新過後）</string>\n    <string name=\"install_inactive_slot_warning\">你的裝置將在重新啟動時**強制**開機至目前的非作用擴充槽！\\n確保僅在 OTA 更新過後選擇此選項。\\n是否繼續？</string>\n    <string name=\"direct_install\">直接安裝（推薦）</string>\n    <string name=\"install_next\">繼續</string>\n    <string name=\"select_kmi\">選擇 KMI</string>\n    <string name=\"select_file_tip\">建議選擇 %1$s 分區的映像檔</string>\n    <string name=\"settings_uninstall\">解除安裝</string>\n    <string name=\"settings_uninstall_temporary\">暫時性解除安裝</string>\n    <string name=\"settings_restore_stock_image\">恢復原廠映像檔</string>\n    <string name=\"settings_uninstall_temporary_message\">暫時解除安裝 KernelSU。會在下次重新啟動時恢復原狀。</string>\n    <string name=\"settings_uninstall_permanent\">永久性解除安裝</string>\n    <string name=\"settings_uninstall_permanent_message\">徹底解除安裝 KernelSU（含 Root 授權與所有的模組）。</string>\n    <string name=\"flashing\">正在閃刷</string>\n    <string name=\"flash_success\">閃刷成功</string>\n    <string name=\"settings_restore_stock_image_message\">若裝置內含有備份檔案，遂以 OTA 更新前的原廠系統映像檔進行復原。若需要解除安裝 KernelSU，請選擇「永久性解除安裝」。</string>\n    <string name=\"flash_failed\">閃刷失敗</string>\n    <string name=\"selected_lkm\">已選定 LKM：%s</string>\n    <string name=\"save_log\">儲存日誌</string>\n    <string name=\"action\">執行</string>\n    <string name=\"module_sort_enabled_first\">啟用優先</string>\n    <string name=\"module_sort_action_first\">執行優先</string>\n    <string name=\"module_install_prompt_with_name\">將安裝以下模組：%1$s</string>\n    <string name=\"confirm\">確認</string>\n    <string name=\"su_not_allowed\">無法授予「%s」超級使用者存取</string>\n    <string name=\"log_saved\">已儲存運作日誌</string>\n    <string name=\"processing\">處理中…</string>\n    <string name=\"refresh_pulling\">下拉刷新</string>\n    <string name=\"refresh_release\">鬆開刷新</string>\n    <string name=\"refresh_refresh\">正在刷新…</string>\n    <string name=\"refresh_complete\">刷新成功</string>\n    <string name=\"module_repos\">模組倉庫</string>\n    <string name=\"module_repos_source_code\">源碼</string>\n    <string name=\"module_repos_sort_name\">按名稱排序</string>\n    <string name=\"undo\">撤銷</string>\n    <string name=\"settings_theme\">主題</string>\n    <string name=\"settings_theme_mode_system\">跟隨系統</string>\n    <string name=\"settings_theme_mode_light\">淺色</string>\n    <string name=\"settings_theme_mode_dark\">深色</string>\n    <string name=\"settings_theme_summary\">選擇應用的主題模式。</string>\n    <string name=\"settings_sucompat\">傳統 su 命令支援</string>\n    <string name=\"settings_sucompat_summary\">允許透過 /system/bin/su 獲取 Root 權限，僅對新行程生效。</string>\n    <string name=\"settings_kernel_umount\">核心處理解除掛載模組</string>\n    <string name=\"settings_kernel_umount_summary\">在核心為需要的應用程式解除掛載模組</string>\n    <string name=\"module_shortcut_title\">建立捷徑</string>\n    <string name=\"module_shortcut_name_label\">捷徑名稱</string>\n    <string name=\"module_shortcut_icon_pick\">選擇自訂圖示</string>\n    <string name=\"module_shortcut_not_supported\">啟動器不支援桌面捷徑。</string>\n    <string name=\"module_shortcut_created\">已在桌面建立捷徑。</string>\n    <string name=\"module_shortcut_updated\">捷徑已更新。</string>\n    <string name=\"module_shortcut_delete\">刪除捷徑</string>\n    <string name=\"module_shortcut_permission_tip_xiaomi\">請在 Xiaomi 設定中為此應用啟用「建立桌面捷徑」權限。</string>\n    <string name=\"module_shortcut_permission_tip_oppo\">請在 OPPO 設定中為此應用啟用「桌面捷徑」權限。</string>\n    <string name=\"module_shortcut_permission_tip_default\">若建立捷徑失敗，請在系統設定中為此應用啟用桌面捷徑權限。</string>\n    <string name=\"safe_mode_module_disabled\">處於安全模式下，禁止安裝模組</string>\n    <string name=\"settings_module_check_update\">檢查模組更新</string>\n</resources>\n"
  },
  {
    "path": "manager/app/src/main/res/xml/backup_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample backup rules file; uncomment and customize as necessary.\n   See https://developer.android.com/guide/topics/data/autobackup\n   for details.\n   Note: This file is ignored for devices older that API 31\n   See https://developer.android.com/about/versions/12/backup-restore\n-->\n<full-backup-content>\n    <!--\n   <include domain=\"sharedpref\" path=\".\"/>\n   <exclude domain=\"sharedpref\" path=\"device.xml\"/>\n-->\n</full-backup-content>"
  },
  {
    "path": "manager/app/src/main/res/xml/data_extraction_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample data extraction rules file; uncomment and customize as necessary.\n   See https://developer.android.com/about/versions/12/backup-restore#xml-changes\n   for details.\n-->\n<data-extraction-rules>\n    <cloud-backup>\n        <!-- TODO: Use <include> and <exclude> to control what is backed up.\n        <include .../>\n        <exclude .../>\n        -->\n    </cloud-backup>\n    <!--\n    <device-transfer>\n        <include .../>\n        <exclude .../>\n    </device-transfer>\n    -->\n</data-extraction-rules>"
  },
  {
    "path": "manager/app/src/main/res/xml/filepaths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<paths>\n    <cache-path name=\"cache\" path=\".\"/>\n    <root-path name=\"root\" path=\".\" />\n</paths>\n"
  },
  {
    "path": "manager/app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <domain-config cleartextTrafficPermitted=\"true\">\n        <domain includeSubdomains=\"true\">127.0.0.1</domain>\n        <domain includeSubdomains=\"true\">0.0.0.0</domain>\n        <domain includeSubdomains=\"true\">::1</domain>\n    </domain-config>\n</network-security-config>\n"
  },
  {
    "path": "manager/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.agp.app) apply false\n    alias(libs.plugins.kotlin) apply false\n    alias(libs.plugins.compose.compiler) apply false\n}\n\nval androidMinSdkVersion by extra(31)\nval androidTargetSdkVersion by extra(36)\nval androidCompileSdkVersion by extra(36)\nval androidBuildToolsVersion by extra(\"36.1.0\")\nval androidCompileNdkVersion: String by extra(libs.versions.ndk.get())\nval androidSourceCompatibility by extra(JavaVersion.VERSION_21)\nval androidTargetCompatibility by extra(JavaVersion.VERSION_21)\nval managerVersionCode by extra(getVersionCode())\nval managerVersionName by extra(getVersionName())\n\nfun getGitCommitCount(): Int {\n    val process = Runtime.getRuntime().exec(arrayOf(\"git\", \"rev-list\", \"--count\", \"HEAD\"))\n    return process.inputStream.bufferedReader().use { it.readText().trim().toInt() }\n}\n\nfun getGitDescribe(): String {\n    val process = Runtime.getRuntime().exec(arrayOf(\"git\", \"describe\", \"--tags\", \"--always\"))\n    return process.inputStream.bufferedReader().use { it.readText().trim() }\n}\n\nfun getVersionCode(): Int {\n    val commitCount = getGitCommitCount()\n    return 30000 + commitCount\n}\n\nfun getVersionName(): String {\n    return getGitDescribe()\n}\n"
  },
  {
    "path": "manager/gradle/libs.versions.toml",
    "content": "[versions]\nagp = \"9.1.0\"\nkotlin = \"2.3.10\"\ncompose-bom = \"2026.03.00\"\nlifecycle = \"2.10.0\"\nactivity-compose = \"1.13.0\"\nkotlinx-coroutines = \"1.10.2\"\nnavigation3 = \"1.1.0-beta01\"\nnavigationevent = \"1.0.2\"\nmarkwon = \"4.6.2\"\nwebkit = \"1.15.0\"\nparcelablelist = \"2.0.1\"\nmaterial3 = \"1.5.0-alpha15\"\nmaterialKolor = \"4.1.1\"\nndk = \"29.0.14206865\"\nlibsu = \"6.0.0\"\napksign = \"1.4\"\nmiuix = \"0.8.7\"\nbackdrop = \"1.0.6\"\ncapsule = \"2.1.3\"\nhaze = \"1.7.2\"\nokhttp = \"5.3.2\"\nhiddenapibypass = \"6.1\"\n\n[plugins]\nagp-app = { id = \"com.android.application\", version.ref = \"agp\" }\n\nkotlin = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\ncompose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\nkotlin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin\" }\n\nlsplugin-apksign = { id = \"org.lsposed.lsplugin.apksign\", version.ref = \"apksign\" }\n\n[libraries]\nandroidx-activity-compose = { group = \"androidx.activity\", name = \"activity-compose\", version.ref = \"activity-compose\" }\n\nandroidx-compose-bom = { group = \"androidx.compose\", name = \"compose-bom\", version.ref = \"compose-bom\" }\nandroidx-compose-material-icons-extended = { group = \"androidx.compose.material\", name = \"material-icons-extended\" }\nandroidx-compose-material3 = { group = \"androidx.compose.material3\", name = \"material3\", version.ref = \"material3\" }\nandroidx-compose-ui = { group = \"androidx.compose.ui\", name = \"ui\" }\nandroidx-compose-ui-test-manifest = { group = \"androidx.compose.ui\", name = \"ui-test-manifest\" }\nandroidx-compose-ui-tooling = { group = \"androidx.compose.ui\", name = \"ui-tooling\" }\nandroidx-compose-ui-tooling-preview = { group = \"androidx.compose.ui\", name = \"ui-tooling-preview\" }\n\nandroidx-lifecycle-runtime-ktx = { group = \"androidx.lifecycle\", name = \"lifecycle-runtime-ktx\", version.ref = \"lifecycle\" }\nandroidx-lifecycle-runtime-compose = { group = \"androidx.lifecycle\", name = \"lifecycle-runtime-compose\", version.ref = \"lifecycle\" }\nandroidx-lifecycle-viewmodel-compose = { group = \"androidx.lifecycle\", name = \"lifecycle-viewmodel-compose\", version.ref = \"lifecycle\" }\nandroidx-lifecycle-viewmodel-navigation3 = { module = \"androidx.lifecycle:lifecycle-viewmodel-navigation3\", version.ref = \"lifecycle\" }\n\nandroidx-webkit = { module = \"androidx.webkit:webkit\", version.ref = \"webkit\" }\n\ncom-github-topjohnwu-libsu-core = { group = \"com.github.topjohnwu.libsu\", name = \"core\", version.ref = \"libsu\" }\ncom-github-topjohnwu-libsu-service = { group = \"com.github.topjohnwu.libsu\", name = \"service\", version.ref = \"libsu\" }\ncom-github-topjohnwu-libsu-io = { group = \"com.github.topjohnwu.libsu\", name = \"io\", version.ref = \"libsu\" }\n\ndev-rikka-rikkax-parcelablelist = { module = \"dev.rikka.rikkax.parcelablelist:parcelablelist\", version.ref = \"parcelablelist\" }\n\nkotlinx-coroutines-core = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-core\", version.ref = \"kotlinx-coroutines\" }\n\nandroidx-navigation3-runtime = { module = \"androidx.navigation3:navigation3-runtime\", version.ref = \"navigation3\" }\nandroidx-navigationevent-compose = { module = \"androidx.navigationevent:navigationevent-compose\", version.ref = \"navigationevent\" }\n\nmarkwon = { group = \"io.noties.markwon\", name = \"core\", version.ref = \"markwon\" }\n\nlsposed-cxx = { module = \"org.lsposed.libcxx:libcxx\", version.ref = \"ndk\" }\n\nmiuix = { module = \"top.yukonga.miuix.kmp:miuix-android\", version.ref = \"miuix\" }\nmiuix-icons = { module = \"top.yukonga.miuix.kmp:miuix-icons-android\", version.ref = \"miuix\" }\nmiuix-navigation3-ui = { module = \"top.yukonga.miuix.kmp:miuix-navigation3-ui-android\", version.ref = \"miuix\" }\n\nbackdrop = { module = \"io.github.kyant0:backdrop\", version.ref = \"backdrop\" }\ncapsule = { module = \"io.github.kyant0:capsule\", version.ref = \"capsule\" }\nhaze = { module = \"dev.chrisbanes.haze:haze-android\", version.ref = \"haze\" }\n\nokhttp = { module = \"com.squareup.okhttp3:okhttp\" }\nokhttp-bom = { module = \"com.squareup.okhttp3:okhttp-bom\", version.ref = \"okhttp\" }\n\nhiddenapibypass = { module = \"org.lsposed.hiddenapibypass:hiddenapibypass\", version.ref = \"hiddenapibypass\" }\n\nmaterial-kolor = { group = \"com.materialkolor\", name = \"material-kolor\", version.ref = \"materialKolor\" }\n"
  },
  {
    "path": "manager/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-9.4.0-bin.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "manager/gradle.properties",
    "content": "android.experimental.enableNewResourceShrinker.preciseShrinking=true\nandroid.enableAppCompileTimeRClass=true\nandroid.useAndroidX=true\norg.gradle.jvmargs=-Xmx2048m\norg.gradle.parallel=true\norg.gradle.vfs.watch=true\nandroid.r8.maxWorkers=4\nandroid.native.buildOutput=verbose\n"
  },
  {
    "path": "manager/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n# SPDX-License-Identifier: Apache-2.0\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\n# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)\nAPP_HOME=$( cd -P \"${APP_HOME:-./}\" > /dev/null && printf '%s\\n' \"$PWD\" ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC2039,SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command:\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,\n#     and any embedded shellness will be escaped.\n#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be\n#     treated as '${Hostname}' itself on the command line.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -jar \"$APP_HOME/gradle/wrapper/gradle-wrapper.jar\" \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "manager/gradlew.bat",
    "content": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n@rem SPDX-License-Identifier: Apache-2.0\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho. 1>&2\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2\r\necho. 1>&2\r\necho Please set the JAVA_HOME variable in your environment to match the 1>&2\r\necho location of your Java installation. 1>&2\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -jar \"%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\" %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "manager/settings.gradle.kts",
    "content": "@file:Suppress(\"UnstableApiUsage\")\n\nenableFeaturePreview(\"TYPESAFE_PROJECT_ACCESSORS\")\n\npluginManagement {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\ndependencyResolutionManagement {\n    repositories {\n        google()\n        mavenCentral()\n        maven(\"https://jitpack.io\")\n    }\n}\n\nrootProject.name = \"KernelSU\"\ninclude(\":app\")\n"
  },
  {
    "path": "manager/sign.example.properties",
    "content": "KEYSTORE_FILE=\nKEYSTORE_PASSWORD=\nKEY_ALIAS=\nKEY_PASSWORD="
  },
  {
    "path": "scripts/allowlist.bt",
    "content": "// Define constants as per the provided structure.\n#define KSU_MAX_PACKAGE_NAME 256\n#define KSU_MAX_GROUPS 32\n#define KSU_SELINUX_DOMAIN 64\n\n// Define the root_profile structure with padding for 64-bit alignment.\nstruct root_profile {\n    uint32 uid;\n    uint32 gid;\n\n    uint32 groups_count;\n    uint32 groups[KSU_MAX_GROUPS];\n    char padding1[4]; // Padding for 64-bit alignment.\n\n    struct {\n        uint64 effective;\n        uint64 permitted;\n        uint64 inheritable;\n    } capabilities;\n\n    char selinux_domain[KSU_SELINUX_DOMAIN];\n    \n    uint32 namespaces;\n    char padding2[4]; // Padding for 64-bit alignment.\n};\n\n// Define the non_root_profile structure with padding for 64-bit alignment.\nstruct non_root_profile {\n    byte umount_modules;\n    char padding[7]; // Padding to make the total size a multiple of 8.\n};\n\n// Define the rp_config structure with padding for 64-bit alignment.\nstruct rp_config_t {\n    byte use_default;\n    \n    char template_name[KSU_MAX_PACKAGE_NAME];\n    char padding[7]; // Padding to make the total size a multiple of 8.\n\n    struct root_profile profile;\n};\n\n// Define the nrp_config structure with padding for 64-bit alignment.\nstruct nrp_config_t {\n    byte use_default;\n    char padding1[7]; // Padding to make the total size a multiple of 8.\n\n    struct non_root_profile profile;\n    char padding2[488]; // Padding to align the union\n};\n\n// Define the main app_profile structure\ntypedef struct {\n    uint32 version;\n    char key[KSU_MAX_PACKAGE_NAME];\n    int32 current_uid;\n    int64 allow_su;\n\n    // Based on allow_su, decide which profile to use\n    if (allow_su != 0) {\n        rp_config_t rp_config;\n    } else {\n        nrp_config_t nrp_config;\n    }\n\n} app_profile;\n\n// Define the file header with magic number and version\ntypedef struct {\n    uint32 magic;\n    uint32 version;\n} file_header;\n\n// Main entry for parsing the file\nfile_header header;\n\nif (header.magic != 0x7f4b5355) {\n    Printf(\"Invalid file magic number.\\n\");\n    return;\n}\n\nFSeek(8); // Skip the header\n\n\n// Continually read app_profile instances until end of file\nwhile (!FEof()) {\n    app_profile profile;\n}\n\n"
  },
  {
    "path": "scripts/ksubot.py",
    "content": "import asyncio\r\nimport os\r\nimport sys\r\nfrom telethon import TelegramClient\r\nfrom telethon.tl.functions.help import GetConfigRequest\r\n\r\nAPI_ID = 611335\r\nAPI_HASH = \"d524b414d21f4d37f08684c1df41ac9c\"\r\n\r\n\r\nBOT_TOKEN = os.environ.get(\"BOT_TOKEN\")\r\nCHAT_ID = os.environ.get(\"CHAT_ID\")\r\nMESSAGE_THREAD_ID = os.environ.get(\"MESSAGE_THREAD_ID\")\r\nCOMMIT_URL = os.environ.get(\"COMMIT_URL\")\r\nCOMMIT_MESSAGE = os.environ.get(\"COMMIT_MESSAGE\")\r\nRUN_URL = os.environ.get(\"RUN_URL\")\r\nTITLE = os.environ.get(\"TITLE\")\r\nVERSION = os.environ.get(\"VERSION\")\r\nBRANCH = os.environ.get(\"BRANCH\")\r\nMSG_TEMPLATE = \"\"\"\r\n**{title}**\r\nBranch: {branch}\r\n#ci_{version}\r\n```\r\n{commit_message}\r\n```\r\n[Commit]({commit_url})\r\n[Workflow run]({run_url})\r\n\"\"\".strip()\r\n\r\n\r\ndef get_caption():\r\n    msg = MSG_TEMPLATE.format(\r\n        title=TITLE,\r\n        branch=BRANCH,\r\n        version=VERSION,\r\n        commit_message=COMMIT_MESSAGE,\r\n        commit_url=COMMIT_URL,\r\n        run_url=RUN_URL,\r\n    )\r\n    if len(msg) > 1024:\r\n        msg = COMMIT_URL\r\n    if BRANCH == \"dev\":\r\n        msg += \"\\n⚠️⚠️**DEV VERSION, PLEASE BACKUP BEFORE INSTALLATION**⚠️⚠️\"\r\n        msg += \"\\n⚠️⚠️**测试版，安装前请备份**⚠️⚠️\"\r\n    return msg\r\n\r\n\r\ndef check_environ():\r\n    global CHAT_ID, MESSAGE_THREAD_ID\r\n    if BOT_TOKEN is None:\r\n        print(\"[-] Invalid BOT_TOKEN\")\r\n        exit(1)\r\n    if CHAT_ID is None:\r\n        print(\"[-] Invalid CHAT_ID\")\r\n        exit(1)\r\n    else:\r\n        CHAT_ID = int(CHAT_ID)\r\n    if COMMIT_URL is None:\r\n        print(\"[-] Invalid COMMIT_URL\")\r\n        exit(1)\r\n    if COMMIT_MESSAGE is None:\r\n        print(\"[-] Invalid COMMIT_MESSAGE\")\r\n        exit(1)\r\n    if RUN_URL is None:\r\n        print(\"[-] Invalid RUN_URL\")\r\n        exit(1)\r\n    if TITLE is None:\r\n        print(\"[-] Invalid TITLE\")\r\n        exit(1)\r\n    if VERSION is None:\r\n        print(\"[-] Invalid VERSION\")\r\n        exit(1)\r\n    if BRANCH is None:\r\n        print(\"[-] Invalid BRANCH\")\r\n        exit(1)\r\n    if MESSAGE_THREAD_ID is None:\r\n        print(\"[-] Invaild MESSAGE_THREAD_ID\")\r\n        exit(1)\r\n    else:\r\n        MESSAGE_THREAD_ID = int(MESSAGE_THREAD_ID)\r\n\r\n\r\nasync def main():\r\n    print(\"[+] Uploading to telegram\")\r\n    check_environ()\r\n    files = sys.argv[1:]\r\n    print(\"[+] Files:\", files)\r\n    if len(files) <= 0:\r\n        print(\"[-] No files to upload\")\r\n        exit(1)\r\n    print(\"[+] Logging in Telegram with bot\")\r\n    script_dir = os.path.dirname(os.path.abspath(sys.argv[0]))\r\n    session_dir = os.path.join(script_dir, \"ksubot\")\r\n    async with await TelegramClient(session=session_dir, api_id=API_ID, api_hash=API_HASH).start(bot_token=BOT_TOKEN) as bot:\r\n        caption = [\"\"] * len(files)\r\n        caption[-1] = get_caption()\r\n        print(\"[+] Caption: \")\r\n        print(\"---\")\r\n        print(caption)\r\n        print(\"---\")\r\n        print(\"[+] Sending\")\r\n        await bot.send_file(entity=CHAT_ID, file=files, caption=caption, reply_to=MESSAGE_THREAD_ID, parse_mode=\"markdown\")\r\n        print(\"[+] Done!\")\r\n\r\nif __name__ == \"__main__\":\r\n    try:\r\n        asyncio.run(main())\r\n    except Exception as e:\r\n        print(f\"[-] An error occurred: {e}\")\r\n"
  },
  {
    "path": "userspace/ksud/.cargo/config.example.toml",
    "content": "# This file contains required configuration to correctly build ksud across platforms.\n# Rename this file to `config.toml` to apply.\n# Replace all UPPERCASE variables (ANDROID_NDK_ROOT, CLANG_PATH, etc.) with real paths.\n\n# Linux\n# CLANG_PATH=ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android26-clang\n# CLANG++_PATH=ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android26-clang++\n\n# macOS\n# CLANG_PATH=ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android26-clang\n# CLANG++_PATH=ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android26-clang++\n\n# Windows\n# CLANG_PATH=ANDROID_NDK_ROOT\\\\toolchains\\\\llvm\\\\prebuilt\\\\windows-x86_64\\\\bin\\\\aarch64-linux-android26-clang.cmd\n# CLANG++_PATH=ANDROID_NDK_ROOT\\\\toolchains\\\\llvm\\\\prebuilt\\\\windows-x86_64\\\\bin\\\\aarch64-linux-android26-clang++.cmd\n\n# Run `cargo build --target aarch64-linux-android` to build.\n\n[target.aarch64-linux-android]\nlinker = \"CLANG_PATH\"\n\n[env]\nCC_aarch64_linux_android = \"CLANG_PATH\"\nCXX_aarch64_linux_android = \"CLANG++_PATH\"\n"
  },
  {
    "path": "userspace/ksud/.gitignore",
    "content": "/target\n.cargo/config.toml\n"
  },
  {
    "path": "userspace/ksud/Cargo.toml",
    "content": "[package]\nname = \"ksud\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nanyhow = \"1\"\nclap = { version = \"4\", features = [\"derive\"] }\nconst_format = \"0.2\"\nlog = \"0.4\"\nenv_logger = { version = \"0.11\", default-features = false }\nrust-embed = { version = \"8\", features = [\n    \"debug-embed\",\n    \"compression\", # must clean build after updating binaries\n] }\nwhich = \"8\"\nsha1 = \"0.10\"\nsha256 = \"1\"\ntempfile = \"3\"\nchrono = \"0.4\"\nregex-lite = \"0.1\"\n\n[target.'cfg(target_os = \"android\")'.dependencies]\nrustix = { version = \"1\", features = [\n    \"all-apis\",\n] }\nandroid-properties = { version = \"0.2\", features = [\"bionic-deprecated\"] }\nandroid_logger = { version = \"0.15\", default-features = false }\nzip = { version = \"8\",  features = [\n    \"deflate\",\n    \"deflate64\",\n    \"time\",\n    \"lzma\",\n    \"xz\",\n], default-features = false }\nzip-extensions = { version = \"0.13\", features = [\n    \"deflate\",\n    \"lzma\",\n    \"xz\",\n], default-features = false }\njava-properties = { git = \"https://github.com/Kernel-SU/java-properties.git\", branch = \"master\", default-features = false }\nserde_json = \"1\"\nencoding_rs = \"0.8\"\nhumansize = \"2\"\nlibc = \"0.2\"\nextattr = \"1\"\njwalk = \"0.8\"\nis_executable = \"1\"\nnom = \"8\"\nderive-new = \"0.7\"\ngetopts = \"0.2\"\nksuinit = { path = \"../ksuinit\" }\nadb_client = { git = \"https://github.com/Kernel-SU/adb_client\" }\nprop-rs-android = { git = \"https://github.com/Kernel-SU/ksu_props\" }\n\n[profile.release]\nstrip = true\nopt-level = \"z\"\nlto = true\ncodegen-units = 1\n"
  },
  {
    "path": "userspace/ksud/bin/.gitignore",
    "content": "**/*.ko"
  },
  {
    "path": "userspace/ksud/build.rs",
    "content": "use std::env;\nuse std::fs::File;\nuse std::io::Write;\nuse std::path::Path;\nuse std::process::Command;\n\nfn get_git_version() -> Result<(u32, String), std::io::Error> {\n    let output = Command::new(\"git\")\n        .args([\"rev-list\", \"--count\", \"HEAD\"])\n        .output()?;\n\n    let output = output.stdout;\n    let version_code = String::from_utf8(output).expect(\"Failed to read git count stdout\");\n    let version_code: u32 = version_code\n        .trim()\n        .parse()\n        .map_err(|_| std::io::Error::other(\"Failed to parse git count\"))?;\n    let version_code = 30000 + version_code;\n\n    let version_name = String::from_utf8(\n        Command::new(\"git\")\n            .args([\"describe\", \"--tags\", \"--always\"])\n            .output()?\n            .stdout,\n    )\n    .map_err(|_| std::io::Error::other(\"Failed to read git describe stdout\"))?;\n    let version_name = version_name.trim_start_matches('v').to_string();\n    Ok((version_code, version_name))\n}\n\nfn main() {\n    let (code, name) = match get_git_version() {\n        Ok((code, name)) => (code, name),\n        Err(_) => {\n            // show warning if git is not installed\n            println!(\"cargo:warning=Failed to get git version, using 0.0.0\");\n            (0, \"0.0.0\".to_string())\n        }\n    };\n    let out_dir = env::var(\"OUT_DIR\").expect(\"Failed to get $OUT_DIR\");\n    let out_dir = Path::new(&out_dir);\n    File::create(Path::new(out_dir).join(\"VERSION_CODE\"))\n        .expect(\"Failed to create VERSION_CODE\")\n        .write_all(code.to_string().as_bytes())\n        .expect(\"Failed to write VERSION_CODE\");\n\n    File::create(Path::new(out_dir).join(\"VERSION_NAME\"))\n        .expect(\"Failed to create VERSION_NAME\")\n        .write_all(name.trim().as_bytes())\n        .expect(\"Failed to write VERSION_NAME\");\n}\n"
  },
  {
    "path": "userspace/ksud/src/apk_sign.rs",
    "content": "use anyhow::{Result, ensure};\nuse std::io::{Read, Seek, SeekFrom};\n\npub fn get_apk_signature(apk: &str) -> Result<(u32, String)> {\n    let mut buffer = [0u8; 0x10];\n    let mut size4 = [0u8; 4];\n    let mut size8 = [0u8; 8];\n    let mut size_of_block = [0u8; 8];\n\n    let mut f = std::fs::File::open(apk)?;\n\n    let mut i = 0;\n    loop {\n        let mut n = [0u8; 2];\n        f.seek(SeekFrom::End(-i - 2))?;\n        f.read_exact(&mut n)?;\n\n        let n = u16::from_le_bytes(n);\n        if i64::from(n) == i {\n            f.seek(SeekFrom::Current(-22))?;\n            f.read_exact(&mut size4)?;\n\n            if u32::from_le_bytes(size4) ^ 0xcafe_babe_u32 == 0xccfb_f1ee_u32 {\n                if i > 0 {\n                    println!(\"warning: comment length is {i}\");\n                }\n                break;\n            }\n        }\n\n        ensure!(n != 0xffff, \"not a zip file\");\n\n        i += 1;\n    }\n\n    f.seek(SeekFrom::Current(12))?;\n    // offset\n    f.read_exact(&mut size4)?;\n    f.seek(SeekFrom::Start(u64::from(u32::from_le_bytes(size4)) - 0x18))?;\n\n    f.read_exact(&mut size8)?;\n    f.read_exact(&mut buffer)?;\n\n    ensure!(&buffer == b\"APK Sig Block 42\", \"Can not found sig block\");\n\n    let pos = u64::from(u32::from_le_bytes(size4)) - (u64::from_le_bytes(size8) + 0x8);\n    f.seek(SeekFrom::Start(pos))?;\n    f.read_exact(&mut size_of_block)?;\n\n    ensure!(size_of_block == size8, \"not a signed apk\");\n\n    let mut v2_signing: Option<(u32, String)> = None;\n    let mut v3_signing_exist = false;\n    let mut v3_1_signing_exist = false;\n\n    loop {\n        let mut id = [0u8; 4];\n        let mut offset = 4u32;\n\n        f.read_exact(&mut size8)?; // sequence length\n        if size8 == size_of_block {\n            break;\n        }\n\n        f.read_exact(&mut id)?; // id\n\n        let id = u32::from_le_bytes(id);\n        if id == 0x7109_871a_u32 {\n            v2_signing = Some(calc_cert_sha256(&mut f, &mut size4, &mut offset)?);\n        } else if id == 0xf053_68c0_u32 {\n            // v3 signature scheme\n            v3_signing_exist = true;\n        } else if id == 0x1b93_ad61_u32 {\n            // v3.1 signature scheme: credits to vvb2060\n            v3_1_signing_exist = true;\n        }\n\n        f.seek(SeekFrom::Current(\n            i64::from_le_bytes(size8) - i64::from(offset),\n        ))?;\n    }\n\n    if v3_signing_exist || v3_1_signing_exist {\n        return Err(anyhow::anyhow!(\"Unexpected v3 signature found!\",));\n    }\n\n    v2_signing.ok_or_else(|| anyhow::anyhow!(\"No signature found!\"))\n}\n\nfn calc_cert_sha256(\n    f: &mut std::fs::File,\n    size4: &mut [u8; 4],\n    offset: &mut u32,\n) -> Result<(u32, String)> {\n    f.read_exact(size4)?; // signer-sequence length\n    f.read_exact(size4)?; // signer length\n    f.read_exact(size4)?; // signed data length\n    *offset += 0x4 * 3;\n\n    f.read_exact(size4)?; // digests-sequence length\n    let pos = u32::from_le_bytes(*size4); // skip digests\n    f.seek(SeekFrom::Current(i64::from(pos)))?;\n    *offset += 0x4 + pos;\n\n    f.read_exact(size4)?; // certificates length\n    f.read_exact(size4)?; // certificate length\n    *offset += 0x4 * 2;\n\n    let cert_len = u32::from_le_bytes(*size4);\n    let mut cert: Vec<u8> = vec![0; cert_len as usize];\n    f.read_exact(&mut cert)?;\n    *offset += cert_len;\n\n    Ok((cert_len, sha256::digest(&cert)))\n}\n"
  },
  {
    "path": "userspace/ksud/src/assets.rs",
    "content": "use anyhow::Result;\nuse rust_embed::RustEmbed;\nuse std::path::Path;\n\n#[cfg(target_os = \"android\")]\nmod android {\n    use crate::assets::Asset;\n    use crate::defs::BINARY_DIR;\n    use crate::utils::ensure_binary;\n    use const_format::concatcp;\n\n    pub const RESETPROP_PATH: &str = concatcp!(BINARY_DIR, \"resetprop\");\n    pub const BUSYBOX_PATH: &str = concatcp!(BINARY_DIR, \"busybox\");\n    pub const BOOTCTL_PATH: &str = concatcp!(BINARY_DIR, \"bootctl\");\n\n    pub fn ensure_binaries(ignore_if_exist: bool) -> anyhow::Result<()> {\n        for file in Asset::iter() {\n            if file == \"ksuinit\" || file.ends_with(\".ko\") {\n                // don't extract ksuinit and kernel modules\n                continue;\n            }\n            let asset =\n                Asset::get(&file).ok_or_else(|| anyhow::anyhow!(\"asset not found: {file}\"))?;\n            ensure_binary(format!(\"{BINARY_DIR}{file}\"), &asset.data, ignore_if_exist)?;\n        }\n\n        // Create resetprop -> ksud symlink (resetprop is now built into ksud)\n        let resetprop_link = RESETPROP_PATH;\n        let _ = std::fs::remove_file(resetprop_link);\n        std::os::unix::fs::symlink(\"/data/adb/ksud\", resetprop_link)?;\n\n        Ok(())\n    }\n}\n\n#[cfg(target_os = \"android\")]\npub use android::*;\n\n#[cfg(all(target_arch = \"x86_64\", target_os = \"android\"))]\n#[derive(RustEmbed)]\n#[folder = \"bin/x86_64\"]\nstruct Asset;\n\n// IF NOT x86_64 ANDROID, ie. macos, linux, windows, always use aarch64\n#[cfg(not(all(target_arch = \"x86_64\", target_os = \"android\")))]\n#[derive(RustEmbed)]\n#[folder = \"bin/aarch64\"]\nstruct Asset;\n\npub fn get_asset_data(name: &str) -> Result<std::borrow::Cow<'static, [u8]>> {\n    let asset = Asset::get(name).ok_or_else(|| anyhow::anyhow!(\"asset not found: {name}\"))?;\n    Ok(asset.data)\n}\n\npub fn copy_assets_to_file(name: &str, dst: impl AsRef<Path>) -> Result<()> {\n    let data = get_asset_data(name)?;\n    std::fs::write(dst, &*data)?;\n    Ok(())\n}\n\npub fn list_supported_kmi() -> std::vec::Vec<std::string::String> {\n    let mut list = Vec::new();\n    for file in Asset::iter() {\n        // kmi_name = \"xxx_kernelsu.ko\"\n        if let Some(kmi) = file.strip_suffix(\"_kernelsu.ko\") {\n            list.push(kmi.to_string());\n        }\n    }\n    list\n}\n"
  },
  {
    "path": "userspace/ksud/src/banner",
    "content": "  _  __                    _ ____  _   _ \n | |/ /___ _ __ _ __   ___| / ___|| | | |\n | ' // _ \\ '__| '_ \\ / _ \\ \\___ \\| | | |\n | . \\  __/ |  | | | |  __/ |___) | |_| |\n |_|\\_\\___|_|  |_| |_|\\___|_|____/ \\___/ \n"
  },
  {
    "path": "userspace/ksud/src/boot_patch.rs",
    "content": "#![allow(clippy::ref_option, clippy::needless_pass_by_value)]\n\nuse std::fs::File;\nuse std::io::Write;\n#[cfg(unix)]\nuse std::os::unix::fs::PermissionsExt;\nuse std::path::Path;\nuse std::path::PathBuf;\nuse std::process::Command;\nuse std::process::Stdio;\n\nuse anyhow::Context;\nuse anyhow::Result;\nuse anyhow::bail;\nuse anyhow::ensure;\nuse regex_lite::Regex;\nuse which::which;\n\nuse crate::assets;\n\n#[cfg(target_os = \"android\")]\nmod android {\n    use super::{PermissionsExt, Result, do_cpio_cmd};\n    pub(super) use crate::defs::{BACKUP_FILENAME, KSU_BACKUP_DIR, KSU_BACKUP_FILE_PREFIX};\n    use anyhow::{Context, anyhow, bail, ensure};\n    use regex_lite::Regex;\n    use std::path::{Path, PathBuf};\n    use std::process::{Command, Stdio};\n\n    use crate::utils;\n\n    pub(super) fn ensure_gki_kernel() -> Result<()> {\n        let version = get_kernel_version()?;\n        let is_gki = version.0 == 5 && version.1 >= 10 || version.2 > 5;\n        ensure!(is_gki, \"only support GKI kernel\");\n        Ok(())\n    }\n\n    pub fn get_kernel_version() -> Result<(i32, i32, i32)> {\n        let uname = rustix::system::uname();\n        let version = uname.release().to_string_lossy();\n        let re = Regex::new(r\"(\\d+)\\.(\\d+)\\.(\\d+)\")?;\n        if let Some(captures) = re.captures(&version) {\n            let major = captures\n                .get(1)\n                .and_then(|m| m.as_str().parse::<i32>().ok())\n                .ok_or_else(|| anyhow!(\"Major version parse error\"))?;\n            let minor = captures\n                .get(2)\n                .and_then(|m| m.as_str().parse::<i32>().ok())\n                .ok_or_else(|| anyhow!(\"Minor version parse error\"))?;\n            let patch = captures\n                .get(3)\n                .and_then(|m| m.as_str().parse::<i32>().ok())\n                .ok_or_else(|| anyhow!(\"Patch version parse error\"))?;\n            Ok((major, minor, patch))\n        } else {\n            Err(anyhow!(\"Invalid kernel version string\"))\n        }\n    }\n\n    fn parse_kmi(version: &str) -> Result<String> {\n        let re = Regex::new(r\"(.* )?(\\d+\\.\\d+)(\\S+)?(android\\d+)(.*)\")?;\n        let cap = re\n            .captures(version)\n            .ok_or_else(|| anyhow::anyhow!(\"Failed to get KMI from boot/modules\"))?;\n        let android_version = cap.get(4).map_or(\"\", |m| m.as_str());\n        let kernel_version = cap.get(2).map_or(\"\", |m| m.as_str());\n        Ok(format!(\"{android_version}-{kernel_version}\"))\n    }\n\n    fn parse_kmi_from_uname() -> Result<String> {\n        let uname = rustix::system::uname();\n        let version = uname.release().to_string_lossy();\n        parse_kmi(&version)\n    }\n\n    fn parse_kmi_from_modules() -> Result<String> {\n        use std::io::BufRead;\n        // find a *.ko in /vendor/lib/modules\n        let modfile = std::fs::read_dir(\"/vendor/lib/modules\")?\n            .filter_map(Result::ok)\n            .find(|entry| entry.path().extension().is_some_and(|ext| ext == \"ko\"))\n            .map(|entry| entry.path())\n            .ok_or_else(|| anyhow!(\"No kernel module found\"))?;\n        let output = Command::new(\"modinfo\").arg(modfile).output()?;\n        for line in output.stdout.lines().map_while(Result::ok) {\n            if line.starts_with(\"vermagic\") {\n                return parse_kmi(&line);\n            }\n        }\n        bail!(\"Parse KMI from modules failed\")\n    }\n\n    pub fn get_current_kmi() -> Result<String> {\n        parse_kmi_from_uname().or_else(|_| parse_kmi_from_modules())\n    }\n\n    fn calculate_sha1(file_path: impl AsRef<Path>) -> Result<String> {\n        use sha1::Digest;\n        use std::io::Read;\n        let mut file = std::fs::File::open(file_path.as_ref())?;\n        let mut hasher = sha1::Sha1::new();\n        let mut buffer = [0; 1024];\n\n        loop {\n            let n = file.read(&mut buffer)?;\n            if n == 0 {\n                break;\n            }\n            hasher.update(&buffer[..n]);\n        }\n\n        let result = hasher.finalize();\n        Ok(format!(\"{result:x}\"))\n    }\n\n    pub(super) fn do_backup(\n        magiskboot: &Path,\n        workdir: &Path,\n        cpio_path: &Path,\n        image: &Path,\n    ) -> Result<()> {\n        let sha1 = calculate_sha1(image)?;\n        let filename = format!(\"{KSU_BACKUP_FILE_PREFIX}{sha1}\");\n\n        println!(\"- Backup stock boot image\");\n        // magiskboot cpio ramdisk.cpio 'add 0755 $BACKUP_FILENAME'\n        let target = format!(\"{KSU_BACKUP_DIR}{filename}\");\n        std::fs::copy(image, &target).with_context(|| format!(\"backup to {target}\"))?;\n        std::fs::write(workdir.join(BACKUP_FILENAME), sha1.as_bytes()).context(\"write sha1\")?;\n        do_cpio_cmd(\n            magiskboot,\n            workdir,\n            cpio_path,\n            &format!(\"add 0755 {BACKUP_FILENAME} {BACKUP_FILENAME}\"),\n        )?;\n        println!(\"- Stock image has been backup to\");\n        println!(\"- {target}\");\n        Ok(())\n    }\n\n    pub(super) fn clean_backup(sha1: &str) -> Result<()> {\n        println!(\"- Clean up backup\");\n        let backup_name = format!(\"{KSU_BACKUP_FILE_PREFIX}{sha1}\");\n        let dir = std::fs::read_dir(KSU_BACKUP_DIR)?;\n        for entry in dir.flatten() {\n            let path = entry.path();\n            if !path.is_file() {\n                continue;\n            }\n            if let Some(name) = path.file_name() {\n                let name = name.to_string_lossy().to_string();\n                if name != backup_name\n                    && name.starts_with(KSU_BACKUP_FILE_PREFIX)\n                    && std::fs::remove_file(path).is_ok()\n                {\n                    println!(\"- removed {name}\");\n                }\n            }\n        }\n        Ok(())\n    }\n\n    pub(super) fn flash_boot(bootdevice: &Option<String>, new_boot: &PathBuf) -> Result<()> {\n        let Some(bootdevice) = bootdevice else {\n            bail!(\"boot device not found\")\n        };\n        let status = Command::new(\"blockdev\")\n            .arg(\"--setrw\")\n            .arg(bootdevice)\n            .status()?;\n        ensure!(status.success(), \"set boot device rw failed\");\n        dd(new_boot, bootdevice).context(\"flash boot failed\")?;\n        Ok(())\n    }\n\n    pub fn choose_boot_partition(\n        kmi: &str,\n        is_replace_kernel: bool,\n        partition: &Option<String>,\n    ) -> String {\n        let slot_suffix = get_slot_suffix(false);\n        let skip_init_boot = kmi.starts_with(\"android12-\");\n        let init_boot_exist =\n            Path::new(&format!(\"/dev/block/by-name/init_boot{slot_suffix}\")).exists();\n\n        // if specific partition is specified, use it\n        if let Some(part) = partition {\n            return match part.as_str() {\n                \"boot\" | \"init_boot\" | \"vendor_boot\" => part.clone(),\n                _ => \"boot\".to_string(),\n            };\n        }\n\n        // if init_boot exists and not skipping it, use it\n        if !is_replace_kernel && init_boot_exist && !skip_init_boot {\n            return \"init_boot\".to_string();\n        }\n\n        \"boot\".to_string()\n    }\n\n    pub fn get_slot_suffix(ota: bool) -> String {\n        let mut slot_suffix = utils::getprop(\"ro.boot.slot_suffix\").unwrap_or_default();\n        if !slot_suffix.is_empty() && ota {\n            if slot_suffix == \"_a\" {\n                slot_suffix = \"_b\".to_string();\n            } else {\n                slot_suffix = \"_a\".to_string();\n            }\n        }\n        slot_suffix\n    }\n\n    #[cfg(target_os = \"android\")]\n    pub fn list_available_partitions() -> Vec<String> {\n        let slot_suffix = get_slot_suffix(false);\n        let candidates = vec![\"boot\", \"init_boot\", \"vendor_boot\"];\n        candidates\n            .into_iter()\n            .filter(|name| Path::new(&format!(\"/dev/block/by-name/{name}{slot_suffix}\")).exists())\n            .map(ToString::to_string)\n            .collect()\n    }\n\n    #[cfg(target_os = \"android\")]\n    pub(super) fn post_ota() -> Result<()> {\n        use crate::assets::BOOTCTL_PATH;\n        use crate::defs::ADB_DIR;\n        let status = Command::new(BOOTCTL_PATH).arg(\"hal-info\").status()?;\n        if !status.success() {\n            return Ok(());\n        }\n\n        let current_slot = Command::new(BOOTCTL_PATH)\n            .arg(\"get-current-slot\")\n            .output()?\n            .stdout;\n        let current_slot = String::from_utf8(current_slot)?;\n        let current_slot = current_slot.trim();\n        let target_slot = i32::from(current_slot == \"0\");\n\n        Command::new(BOOTCTL_PATH)\n            .arg(format!(\"set-active-boot-slot {target_slot}\"))\n            .status()?;\n\n        let post_fs_data = Path::new(ADB_DIR).join(\"post-fs-data.d\");\n        utils::ensure_dir_exists(&post_fs_data)?;\n        let post_ota_sh = post_fs_data.join(\"post_ota.sh\");\n\n        let sh_content = format!(\n            r\"\n{BOOTCTL_PATH} mark-boot-successful\nrm -f {BOOTCTL_PATH}\nrm -f /data/adb/post-fs-data.d/post_ota.sh\n\"\n        );\n\n        std::fs::write(&post_ota_sh, sh_content)?;\n        #[cfg(unix)]\n        std::fs::set_permissions(post_ota_sh, std::fs::Permissions::from_mode(0o755))?;\n\n        Ok(())\n    }\n\n    pub(super) fn dd<P: AsRef<Path>, Q: AsRef<Path>>(ifile: P, ofile: Q) -> Result<()> {\n        let status = Command::new(\"dd\")\n            .stdout(Stdio::null())\n            .stderr(Stdio::null())\n            .arg(format!(\"if={}\", ifile.as_ref().display()))\n            .arg(format!(\"of={}\", ofile.as_ref().display()))\n            .status()?;\n        ensure!(\n            status.success(),\n            \"dd if={} of={} failed\",\n            ifile.as_ref().display(),\n            ofile.as_ref().display()\n        );\n        Ok(())\n    }\n}\n\n#[cfg(target_os = \"android\")]\npub use android::*;\n\nfn parse_kmi_from_kernel(kernel: &PathBuf, workdir: &Path) -> Result<String> {\n    use std::fs::{File, copy};\n    use std::io::{BufReader, Read};\n    let kernel_path = workdir.join(\"kernel\");\n    copy(kernel, &kernel_path).context(\"Failed to copy kernel\")?;\n\n    let file = File::open(&kernel_path).context(\"Failed to open kernel file\")?;\n    let mut reader = BufReader::new(file);\n    let mut buffer = Vec::new();\n    reader\n        .read_to_end(&mut buffer)\n        .context(\"Failed to read kernel file\")?;\n\n    let printable_strings: Vec<&str> = buffer\n        .split(|&b| b == 0)\n        .filter_map(|slice| std::str::from_utf8(slice).ok())\n        .filter(|s| s.chars().all(|c| c.is_ascii_graphic() || c == ' '))\n        .collect();\n\n    let re =\n        Regex::new(r\"(?:.* )?(\\d+\\.\\d+)(?:\\S+)?(android\\d+)\").context(\"Failed to compile regex\")?;\n    for s in printable_strings {\n        if let Some(caps) = re.captures(s)\n            && let (Some(kernel_version), Some(android_version)) = (caps.get(1), caps.get(2))\n        {\n            let kmi = format!(\"{}-{}\", android_version.as_str(), kernel_version.as_str());\n            return Ok(kmi);\n        }\n    }\n    println!(\"- Failed to get KMI version\");\n    bail!(\"Try to choose LKM manually\")\n}\n\nfn parse_kmi_from_boot(magiskboot: &Path, image: &PathBuf, workdir: &Path) -> Result<String> {\n    let image_path = workdir.join(\"image\");\n\n    std::fs::copy(image, &image_path).context(\"Failed to copy image\")?;\n\n    let status = Command::new(magiskboot)\n        .current_dir(workdir)\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .arg(\"unpack\")\n        .arg(&image_path)\n        .status()\n        .context(\"Failed to execute magiskboot command\")?;\n\n    if !status.success() {\n        bail!(\"magiskboot unpack failed with status: {status:?}\");\n    }\n\n    parse_kmi_from_kernel(&image_path, workdir)\n}\n\nfn do_cpio_cmd(magiskboot: &Path, workdir: &Path, cpio_path: &Path, cmd: &str) -> Result<()> {\n    let status = Command::new(magiskboot)\n        .current_dir(workdir)\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .arg(\"cpio\")\n        .arg(cpio_path)\n        .arg(cmd)\n        .status()?;\n    ensure!(status.success(), \"magiskboot cpio {cmd} failed\");\n    Ok(())\n}\n\nfn is_magisk_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result<bool> {\n    let status = Command::new(magiskboot)\n        .current_dir(workdir)\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .arg(\"cpio\")\n        .arg(cpio_path)\n        .arg(\"test\")\n        .status()?;\n    // 0: stock, 1: magisk\n    Ok(status.code() == Some(1))\n}\n\nfn is_kernelsu_patched(magiskboot: &Path, workdir: &Path, cpio_path: &Path) -> Result<bool> {\n    let status = Command::new(magiskboot)\n        .current_dir(workdir)\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .arg(\"cpio\")\n        .arg(cpio_path)\n        .arg(\"exists kernelsu.ko\")\n        .status()?;\n\n    Ok(status.success())\n}\n\nfn find_magiskboot(magiskboot_path: Option<PathBuf>, workdir: &Path) -> Result<PathBuf> {\n    let magiskboot = {\n        if which(\"magiskboot\").is_ok() {\n            #[cfg(target_os = \"android\")]\n            let _ = assets::ensure_binaries(true);\n            \"magiskboot\".into()\n        } else {\n            // magiskboot is not in $PATH, use builtin or specified one\n            let magiskboot = if let Some(magiskboot_path) = magiskboot_path {\n                std::fs::canonicalize(magiskboot_path)?\n            } else {\n                let magiskboot_path = workdir.join(\"magiskboot\");\n                assets::copy_assets_to_file(\"magiskboot\", &magiskboot_path)\n                    .context(\"copy magiskboot failed\")?;\n                magiskboot_path\n            };\n            ensure!(magiskboot.exists(), \"{} is not exist\", magiskboot.display());\n            #[cfg(unix)]\n            let _ = std::fs::set_permissions(&magiskboot, std::fs::Permissions::from_mode(0o755));\n            magiskboot\n        }\n    };\n    Ok(magiskboot)\n}\n\nfn find_boot_image(\n    image: &Option<PathBuf>,\n    kmi: &str,\n    ota: bool,\n    is_replace_kernel: bool,\n    workdir: &Path,\n    partition: &Option<String>,\n) -> Result<(PathBuf, Option<String>)> {\n    let bootimage;\n    let mut bootdevice = None;\n    if let Some(ref image) = *image {\n        ensure!(image.exists(), \"boot image not found\");\n        bootimage = std::fs::canonicalize(image)?;\n    } else {\n        #[cfg(not(target_os = \"android\"))]\n        {\n            println!(\"- Current OS is not android, refusing auto bootimage/bootdevice detection\");\n            bail!(\"Please specify a boot image\");\n        }\n\n        #[cfg(target_os = \"android\")]\n        {\n            let slot_suffix = get_slot_suffix(ota);\n            let boot_partition_name = choose_boot_partition(kmi, is_replace_kernel, partition);\n            let boot_partition = format!(\"/dev/block/by-name/{boot_partition_name}{slot_suffix}\");\n\n            println!(\"- Bootdevice: {boot_partition}\");\n            let tmp_boot_path = workdir.join(\"boot.img\");\n\n            dd(&boot_partition, &tmp_boot_path)?;\n\n            ensure!(tmp_boot_path.exists(), \"boot image not found\");\n\n            bootimage = tmp_boot_path;\n            bootdevice = Some(boot_partition);\n        }\n    }\n    Ok((bootimage, bootdevice))\n}\n\n#[allow(clippy::struct_excessive_bools)]\n#[derive(clap::Args, Debug)]\npub struct BootPatchArgs {\n    /// boot image path, if not specified, will try to find the boot image automatically\n    #[arg(short, long)]\n    pub boot: Option<PathBuf>,\n\n    /// kernel image path to replace\n    #[arg(short, long)]\n    pub kernel: Option<PathBuf>,\n\n    /// LKM module path to replace, if not specified, will use the builtin one\n    #[arg(short, long)]\n    pub module: Option<PathBuf>,\n\n    /// init to be replaced\n    #[arg(short, long, requires(\"module\"))]\n    pub init: Option<PathBuf>,\n\n    /// will use another slot when boot image is not specified\n    #[cfg(target_os = \"android\")]\n    #[arg(short = 'u', long, default_value = \"false\")]\n    pub ota: bool,\n\n    /// Flash it to boot partition after patch\n    #[cfg(target_os = \"android\")]\n    #[arg(short, long, default_value = \"false\")]\n    pub flash: bool,\n\n    /// Output path. If not specified, will use current directory.\n    /// If specified, the boot image will be written to the directory\n    /// even if --flash is specified.\n    #[cfg(target_os = \"android\")]\n    #[arg(short, long, default_value = None)]\n    pub out: Option<PathBuf>,\n\n    /// Output path. If not specified, will use current directory.\n    #[cfg(not(target_os = \"android\"))]\n    #[arg(short, long, default_value = None)]\n    pub out: Option<PathBuf>,\n\n    /// magiskboot path, if not specified, will search from $PATH\n    #[arg(long, default_value = None)]\n    pub magiskboot: Option<PathBuf>,\n\n    /// KMI version, if specified, will use the specified KMI\n    #[arg(long, default_value = None)]\n    pub kmi: Option<String>,\n\n    /// target partition override (init_boot | boot | vendor_boot)\n    #[cfg(target_os = \"android\")]\n    #[arg(long, default_value = None)]\n    pub partition: Option<String>,\n\n    /// File name of the output. If specified, the boot image will be\n    /// written to the output directory even if --flash is specified.\n    #[cfg(target_os = \"android\")]\n    #[arg(long, default_value = None)]\n    pub out_name: Option<String>,\n\n    /// File name of the output.\n    #[cfg(not(target_os = \"android\"))]\n    #[arg(long, default_value = None)]\n    pub out_name: Option<String>,\n\n    /// Extra cmdline to append to boot image header\n    #[arg(long, default_value = None)]\n    pub cmdline: Option<String>,\n\n    /// Always allow shell to get root permission\n    #[arg(long, default_value = \"false\")]\n    allow_shell: bool,\n\n    /// Force enable adbd and disable adbd auth\n    #[arg(long, default_value = \"false\")]\n    enable_adbd: bool,\n\n    /// Add more adb_debug prop\n    #[arg(long, required = false)]\n    adb_debug_prop: Option<String>,\n\n    /// Do not (re-)install kernelsu, only modify configs (allow_shell, etc.)\n    #[arg(long, default_value = \"false\")]\n    no_install: bool,\n}\n\npub fn patch(args: BootPatchArgs) -> Result<()> {\n    let inner = move || {\n        let BootPatchArgs {\n            boot: image,\n            init,\n            kernel,\n            module: kmod,\n            out,\n            magiskboot: magiskboot_path,\n            kmi,\n            out_name,\n            cmdline,\n            allow_shell,\n            enable_adbd,\n            adb_debug_prop,\n            no_install,\n            #[cfg(target_os = \"android\")]\n            ota,\n            #[cfg(target_os = \"android\")]\n            flash,\n            #[cfg(target_os = \"android\")]\n            partition,\n        } = args;\n\n        println!(include_str!(\"banner\"));\n\n        let patch_file = image.is_some();\n\n        #[cfg(target_os = \"android\")]\n        if !patch_file {\n            ensure_gki_kernel()?;\n        }\n\n        let is_replace_kernel = kernel.is_some();\n\n        if is_replace_kernel {\n            ensure!(\n                init.is_none() && kmod.is_none(),\n                \"init and module must not be specified.\"\n            );\n        }\n\n        let tmpdir = tempfile::Builder::new()\n            .prefix(\"KernelSU\")\n            .tempdir()\n            .context(\"create temp dir failed\")?;\n        let workdir = tmpdir.path();\n\n        // extract magiskboot\n        let magiskboot = find_magiskboot(magiskboot_path, workdir)?;\n\n        let kmi = kmi.map_or_else(\n            || -> Result<_> {\n                if kmod.is_some() {\n                    return Ok(String::new());\n                }\n                #[cfg(target_os = \"android\")]\n                match get_current_kmi() {\n                    Ok(value) => {\n                        return Ok(value);\n                    }\n                    Err(e) => {\n                        println!(\"- {e}\");\n                    }\n                }\n                Ok(if let Some(image_path) = &image {\n                    println!(\n                        \"- Trying to auto detect KMI version for {}\",\n                        image_path.display()\n                    );\n                    parse_kmi_from_boot(&magiskboot, image_path, tmpdir.path())?\n                } else if let Some(kernel_path) = &kernel {\n                    println!(\n                        \"- Trying to auto detect KMI version for {}\",\n                        kernel_path.display()\n                    );\n                    parse_kmi_from_kernel(kernel_path, tmpdir.path())?\n                } else {\n                    String::new()\n                })\n            },\n            Ok,\n        )?;\n\n        #[cfg(target_os = \"android\")]\n        let (bootimage, bootdevice) =\n            find_boot_image(&image, &kmi, ota, is_replace_kernel, workdir, &partition)?;\n\n        #[cfg(not(target_os = \"android\"))]\n        let (bootimage, _) =\n            find_boot_image(&image, &kmi, false, is_replace_kernel, workdir, &None)?;\n\n        let bootimage = bootimage.as_path();\n\n        // try extract magiskboot/bootctl\n        #[cfg(target_os = \"android\")]\n        let _ = assets::ensure_binaries(false);\n\n        if let Some(kernel) = kernel {\n            std::fs::copy(kernel, workdir.join(\"kernel\")).context(\"copy kernel from failed\")?;\n        }\n\n        println!(\"- Preparing assets\");\n\n        let kmod_file = workdir.join(\"kernelsu.ko\");\n        if let Some(kmod) = kmod {\n            std::fs::copy(kmod, kmod_file).context(\"copy kernel module failed\")?;\n        } else if !no_install {\n            // If kmod is not specified, extract from assets\n            println!(\"- KMI: {kmi}\");\n            let name = format!(\"{kmi}_kernelsu.ko\");\n            assets::copy_assets_to_file(&name, kmod_file)\n                .with_context(|| format!(\"Failed to copy {name}\"))?;\n        }\n\n        let init_file = workdir.join(\"init\");\n        if let Some(init) = init {\n            std::fs::copy(init, init_file).context(\"copy init failed\")?;\n        } else if !no_install {\n            assets::copy_assets_to_file(\"ksuinit\", init_file).context(\"copy ksuinit failed\")?;\n        }\n\n        println!(\"- Unpacking boot image\");\n        let status = Command::new(&magiskboot)\n            .current_dir(workdir)\n            .stdout(Stdio::null())\n            .stderr(Stdio::null())\n            .arg(\"unpack\")\n            .arg(bootimage)\n            .status()?;\n        ensure!(status.success(), \"magiskboot unpack failed\");\n\n        if let Some(ref cmdline_value) = cmdline {\n            let header_path = workdir.join(\"header\");\n            std::fs::write(&header_path, format!(\"cmdline={cmdline_value}\\n\"))\n                .context(\"write header file failed\")?;\n            println!(\"- Set cmdline to: {cmdline_value}\");\n        }\n\n        let mut ramdisk = workdir.join(\"ramdisk.cpio\");\n        if !ramdisk.exists() {\n            ramdisk = workdir.join(\"vendor_ramdisk\").join(\"init_boot.cpio\");\n        }\n        if !ramdisk.exists() {\n            ramdisk = workdir.join(\"vendor_ramdisk\").join(\"ramdisk.cpio\");\n        }\n        if !ramdisk.exists() {\n            println!(\"- No ramdisk, create by default\");\n            ramdisk = \"ramdisk.cpio\".into();\n        }\n        let ramdisk = ramdisk.as_path();\n        if !no_install {\n            let is_magisk_patched = is_magisk_patched(&magiskboot, workdir, ramdisk)?;\n            ensure!(!is_magisk_patched, \"Cannot work with Magisk patched image\");\n\n            println!(\"- Adding KernelSU LKM\");\n            let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?;\n\n            if !is_kernelsu_patched {\n                // kernelsu.ko is not exist, backup init if necessary\n                let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, \"exists init\");\n                if status.is_ok() {\n                    do_cpio_cmd(&magiskboot, workdir, ramdisk, \"mv init init.real\")?;\n                }\n            }\n\n            do_cpio_cmd(&magiskboot, workdir, ramdisk, \"add 0755 init init\")?;\n            do_cpio_cmd(\n                &magiskboot,\n                workdir,\n                ramdisk,\n                \"add 0755 kernelsu.ko kernelsu.ko\",\n            )?;\n\n            #[cfg(target_os = \"android\")]\n            if !is_kernelsu_patched\n                && flash\n                && let Err(e) = do_backup(&magiskboot, workdir, ramdisk, bootimage)\n            {\n                println!(\"- Backup stock image failed: {e}\");\n            }\n        }\n\n        if allow_shell {\n            println!(\"- Adding allow shell config\");\n            {\n                let allow_shell_file = workdir.join(\"ksu_allow_shell\");\n                File::create(allow_shell_file)?;\n            }\n            do_cpio_cmd(\n                &magiskboot,\n                workdir,\n                ramdisk,\n                \"add 0644 ksu_allow_shell ksu_allow_shell\",\n            )?;\n        } else if do_cpio_cmd(&magiskboot, workdir, ramdisk, \"exists ksu_allow_shell\").is_ok() {\n            println!(\"- Removing allow shell config\");\n            do_cpio_cmd(&magiskboot, workdir, ramdisk, \"rm ksu_allow_shell\").ok();\n        }\n\n        if enable_adbd || adb_debug_prop.is_some() {\n            println!(\"- Adding adb_debug props\");\n            {\n                let force_debuggable_file = workdir.join(\"force_debuggable\");\n                File::create(force_debuggable_file)?;\n            }\n            do_cpio_cmd(\n                &magiskboot,\n                workdir,\n                ramdisk,\n                \"add 0644 force_debuggable force_debuggable\",\n            )?;\n\n            {\n                let prop_path = workdir.join(\"adb_debug.prop\");\n                let mut prop_file = File::create(prop_path)?;\n                if enable_adbd {\n                    println!(\"- Adding props to enable adbd\");\n                    write!(\n                        prop_file,\n                        \"ro.debuggable=1\\nro.force.debuggable=1\\nro.adb.secure=0\\n\"\n                    )?;\n                }\n                if let Some(props) = adb_debug_prop {\n                    println!(\"- Adding custom props\");\n                    prop_file.write_all(props.as_bytes())?;\n                }\n            }\n            do_cpio_cmd(\n                &magiskboot,\n                workdir,\n                ramdisk,\n                \"add 0644 adb_debug.prop adb_debug.prop\",\n            )?;\n        } else {\n            if do_cpio_cmd(&magiskboot, workdir, ramdisk, \"exists force_debuggable\").is_ok() {\n                println!(\"- Removing /force_debuggable\");\n                do_cpio_cmd(&magiskboot, workdir, ramdisk, \"rm force_debuggable\").ok();\n            }\n            if do_cpio_cmd(&magiskboot, workdir, ramdisk, \"exists adb_debug.prop\").is_ok() {\n                println!(\"- Removing /adb_debug.prop\");\n                do_cpio_cmd(&magiskboot, workdir, ramdisk, \"rm adb_debug.prop\").ok();\n            }\n        }\n\n        println!(\"- Repacking boot image\");\n        // magiskboot repack boot.img\n        let status = Command::new(&magiskboot)\n            .current_dir(workdir)\n            .stdout(Stdio::null())\n            .stderr(Stdio::null())\n            .arg(\"repack\")\n            .arg(bootimage)\n            .status()?;\n        ensure!(status.success(), \"magiskboot repack failed\");\n        let new_boot = workdir.join(\"new-boot.img\");\n\n        // flash first, since the new_boot may be moved\n        #[cfg(target_os = \"android\")]\n        if flash {\n            println!(\"- Flashing new boot image\");\n            flash_boot(&bootdevice, &new_boot)?;\n\n            if ota {\n                post_ota()?;\n            }\n        }\n\n        #[cfg(target_os = \"android\")]\n        let should_write_output = patch_file || !flash || out_name.is_some() || out.is_some();\n        #[cfg(not(target_os = \"android\"))]\n        let should_write_output = patch_file;\n\n        if should_write_output {\n            // write patched image to output file when output is requested\n            let output_dir = out.unwrap_or(std::env::current_dir()?);\n            let name = out_name.unwrap_or_else(|| {\n                let now = chrono::Utc::now();\n                format!(\"kernelsu_patched_{}.img\", now.format(\"%Y%m%d_%H%M%S\"))\n            });\n            let output_image = output_dir.join(name);\n            if std::fs::rename(&new_boot, &output_image).is_err() {\n                std::fs::copy(&new_boot, &output_image).context(\"copy out new boot failed\")?;\n            }\n            println!(\"- Output file is written to\");\n            println!(\"- {}\", output_image.display().to_string().trim_matches('\"'));\n        }\n\n        println!(\"- Done!\");\n        Ok(())\n    };\n\n    let result = inner();\n    if let Err(ref e) = result {\n        println!(\"- Patch Error: {e}\");\n    }\n    result\n}\n\n#[derive(clap::Args, Debug)]\npub struct BootRestoreArgs {\n    /// boot image path, if not specified, will try to find the boot image automatically\n    #[arg(short, long)]\n    pub boot: Option<PathBuf>,\n\n    /// Flash it to boot partition after restore\n    #[cfg(target_os = \"android\")]\n    #[arg(short, long, default_value = \"false\")]\n    pub flash: bool,\n\n    /// magiskboot path, if not specified, will search from $PATH\n    #[arg(long, default_value = None)]\n    pub magiskboot: Option<PathBuf>,\n\n    /// Output path. If not specified, will use current directory.\n    /// If specified, the boot image will be written to the directory\n    /// even if --flash is specified.\n    #[cfg(target_os = \"android\")]\n    #[arg(short, long, default_value = None)]\n    pub out: Option<PathBuf>,\n\n    /// Output path. If not specified, will use current directory.\n    #[cfg(not(target_os = \"android\"))]\n    #[arg(short, long, default_value = None)]\n    pub out: Option<PathBuf>,\n\n    /// File name of the output. If specified, the boot image will be\n    /// written to the output directory even if --flash is specified.\n    #[cfg(target_os = \"android\")]\n    #[arg(long, default_value = None)]\n    pub out_name: Option<String>,\n\n    /// File name of the output.\n    #[cfg(not(target_os = \"android\"))]\n    #[arg(long, default_value = None)]\n    pub out_name: Option<String>,\n}\n\npub fn restore(args: BootRestoreArgs) -> Result<()> {\n    let BootRestoreArgs {\n        boot: image,\n        magiskboot: magiskboot_path,\n        out_name,\n        out,\n        #[cfg(target_os = \"android\")]\n        flash,\n    } = args;\n\n    let tmpdir = tempfile::Builder::new()\n        .prefix(\"KernelSU\")\n        .tempdir()\n        .context(\"create temp dir failed\")?;\n    let workdir = tmpdir.path();\n    let magiskboot = find_magiskboot(magiskboot_path, workdir)?;\n\n    #[cfg(target_os = \"android\")]\n    let kmi = get_current_kmi().unwrap_or_default();\n\n    #[cfg(target_os = \"android\")]\n    let (bootimage, bootdevice) = find_boot_image(&image, &kmi, false, false, workdir, &None)?;\n    #[cfg(not(target_os = \"android\"))]\n    let (bootimage, _) = find_boot_image(&image, \"\", false, false, workdir, &None)?;\n\n    println!(\"- Unpacking boot image\");\n    let status = Command::new(&magiskboot)\n        .current_dir(workdir)\n        .stdout(Stdio::null())\n        .stderr(Stdio::null())\n        .arg(\"unpack\")\n        .arg(bootimage.display().to_string())\n        .status()?;\n    ensure!(status.success(), \"magiskboot unpack failed\");\n\n    let mut ramdisk = workdir.join(\"ramdisk.cpio\");\n    if !ramdisk.exists() {\n        ramdisk = workdir.join(\"vendor_ramdisk\").join(\"init_boot.cpio\");\n    }\n    if !ramdisk.exists() {\n        ramdisk = workdir.join(\"vendor_ramdisk\").join(\"ramdisk.cpio\");\n    }\n    if !ramdisk.exists() {\n        bail!(\"No compatible ramdisk found.\")\n    }\n    let ramdisk = ramdisk.as_path();\n    let is_kernelsu_patched = is_kernelsu_patched(&magiskboot, workdir, ramdisk)?;\n    ensure!(is_kernelsu_patched, \"boot image is not patched by KernelSU\");\n\n    #[cfg(target_os = \"android\")]\n    let mut new_boot = None;\n    #[cfg(target_os = \"android\")]\n    let mut from_backup = false;\n    #[cfg(not(target_os = \"android\"))]\n    let from_backup = false;\n\n    #[cfg(target_os = \"android\")]\n    if do_cpio_cmd(\n        &magiskboot,\n        workdir,\n        ramdisk,\n        &format!(\"exists {BACKUP_FILENAME}\"),\n    )\n    .is_ok()\n    {\n        do_cpio_cmd(\n            &magiskboot,\n            workdir,\n            ramdisk,\n            &format!(\"extract {BACKUP_FILENAME} {BACKUP_FILENAME}\"),\n        )?;\n        let sha = std::fs::read(workdir.join(BACKUP_FILENAME))?;\n        let sha = String::from_utf8(sha)?;\n        let sha = sha.trim();\n        let backup_path =\n            PathBuf::from(KSU_BACKUP_DIR).join(format!(\"{KSU_BACKUP_FILE_PREFIX}{sha}\"));\n        if backup_path.is_file() {\n            new_boot = Some(backup_path);\n            from_backup = true;\n        } else {\n            println!(\"- Warning: no backup {} found!\", backup_path.display());\n        }\n\n        if let Err(e) = clean_backup(sha) {\n            println!(\"- Warning: Cleanup backup image failed: {e}\");\n        }\n    } else {\n        println!(\"- Backup info is absent!\");\n    }\n\n    let remove_ksu = || -> Result<_> {\n        // remove kernelsu.ko\n        do_cpio_cmd(&magiskboot, workdir, ramdisk, \"rm kernelsu.ko\")?;\n\n        // if init.real exists, restore it\n        let status = do_cpio_cmd(&magiskboot, workdir, ramdisk, \"exists init.real\").is_ok();\n        if status {\n            do_cpio_cmd(&magiskboot, workdir, ramdisk, \"mv init.real init\")?;\n        }\n\n        println!(\"- Repacking boot image\");\n        let status = Command::new(&magiskboot)\n            .current_dir(workdir)\n            .stdout(Stdio::null())\n            .stderr(Stdio::null())\n            .arg(\"repack\")\n            .arg(&bootimage)\n            .status()?;\n        ensure!(status.success(), \"magiskboot repack failed\");\n        Ok(workdir.join(\"new-boot.img\"))\n    };\n    #[cfg(target_os = \"android\")]\n    let new_boot = new_boot.map_or_else(remove_ksu, Ok)?;\n    #[cfg(not(target_os = \"android\"))]\n    let new_boot = remove_ksu()?;\n\n    // flash first, since the new_boot may be moved\n    #[cfg(target_os = \"android\")]\n    if flash {\n        if from_backup {\n            println!(\"- Flashing new boot image from {}\", new_boot.display());\n        } else {\n            println!(\"- Flashing new boot image\");\n        }\n        flash_boot(&bootdevice, &new_boot)?;\n    }\n\n    #[cfg(target_os = \"android\")]\n    let should_write_output = image.is_some() || !flash || out_name.is_some() || out.is_some();\n    #[cfg(not(target_os = \"android\"))]\n    let should_write_output = image.is_some();\n\n    if should_write_output {\n        // write restored image to output file when output is requested\n        let output_dir = out.unwrap_or(std::env::current_dir()?);\n        let name = out_name.unwrap_or_else(|| {\n            let now = chrono::Utc::now();\n            format!(\"kernelsu_restore_{}.img\", now.format(\"%Y%m%d_%H%M%S\"))\n        });\n        let output_image = output_dir.join(name);\n\n        if from_backup || std::fs::rename(&new_boot, &output_image).is_err() {\n            std::fs::copy(&new_boot, &output_image).context(\"copy out new boot failed\")?;\n        }\n        println!(\"- Output file is written to\");\n        println!(\"- {}\", output_image.display().to_string().trim_matches('\"'));\n    }\n    println!(\"- Done!\");\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/cli.rs",
    "content": "use anyhow::{Context, Ok, Result};\nuse clap::Parser;\nuse std::path::PathBuf;\n\nuse android_logger::Config;\nuse log::{LevelFilter, error, info};\n\nuse crate::boot_patch::{BootPatchArgs, BootRestoreArgs};\nuse crate::{apk_sign, assets, debug, defs, init_event, ksucalls, module, module_config, utils};\n\n/// KernelSU userspace cli\n#[derive(Parser, Debug)]\n#[command(author, version = defs::VERSION_NAME, about, long_about = None)]\nstruct Args {\n    #[command(subcommand)]\n    command: Commands,\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum Commands {\n    /// Manage KernelSU modules\n    Module {\n        #[command(subcommand)]\n        command: Module,\n    },\n\n    /// Trigger `post-fs-data` event\n    PostFsData,\n\n    /// Trigger `service` event\n    Services,\n\n    /// Trigger `boot-complete` event\n    BootCompleted,\n\n    /// Load kernelsu.ko and execute late-load stage scripts\n    LateLoad {\n        /// Use adb root to execute late-load for jailbreaking by Magica\n        #[arg(long, default_missing_value = \"5555\", num_args = 0..=1)]\n        magica: Option<u16>,\n\n        /// Restore adb properties after magica late-load\n        #[arg(long)]\n        post_magica: bool,\n    },\n\n    /// Install KernelSU userspace component to system\n    Install {\n        #[arg(long, default_value = None)]\n        magiskboot: Option<PathBuf>,\n    },\n\n    /// Uninstall KernelSU modules and itself(LKM Only)\n    Uninstall {\n        /// magiskboot path, if not specified, will search from $PATH\n        #[arg(long, default_value = None)]\n        magiskboot: Option<PathBuf>,\n    },\n\n    /// SELinux policy Patch tool\n    Sepolicy {\n        #[command(subcommand)]\n        command: Sepolicy,\n    },\n\n    /// Manage App Profiles\n    Profile {\n        #[command(subcommand)]\n        command: Profile,\n    },\n\n    /// Manage kernel features\n    Feature {\n        #[command(subcommand)]\n        command: Feature,\n    },\n\n    /// Patch boot or init_boot images to apply KernelSU\n    BootPatch(BootPatchArgs),\n\n    /// Restore boot or init_boot images patched by KernelSU\n    BootRestore(BootRestoreArgs),\n\n    /// Show boot information\n    BootInfo {\n        #[command(subcommand)]\n        command: BootInfo,\n    },\n    /// For developers\n    Debug {\n        #[command(subcommand)]\n        command: Debug,\n    },\n    /// Kernel interface\n    Kernel {\n        #[command(subcommand)]\n        command: Kernel,\n    },\n\n    /// Resetprop - Magisk-compatible system property tool\n    Resetprop {\n        /// Arguments passed to resetprop\n        #[arg(trailing_var_arg = true, allow_hyphen_values = true)]\n        args: Vec<String>,\n    },\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum BootInfo {\n    /// show current kmi version\n    CurrentKmi,\n\n    /// show supported kmi versions\n    SupportedKmis,\n\n    /// check if device is A/B capable\n    IsAbDevice,\n\n    /// show auto-selected boot partition name\n    DefaultPartition,\n\n    /// list available partitions for current or OTA toggled slot\n    AvailablePartitions,\n\n    /// show slot suffix for current or OTA toggled slot\n    SlotSuffix {\n        /// toggle to another slot\n        #[arg(short = 'u', long, default_value = \"false\")]\n        ota: bool,\n    },\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum Debug {\n    /// Set the manager app, kernel CONFIG_KSU_DEBUG should be enabled.\n    SetManager {\n        /// manager package name\n        #[arg(default_value_t = String::from(\"me.weishu.kernelsu\"))]\n        apk: String,\n    },\n\n    /// Get apk size and hash\n    GetSign {\n        /// apk path\n        apk: String,\n    },\n\n    /// Root Shell\n    Su {\n        /// switch to gloabl mount namespace\n        #[arg(short, long, default_value = \"false\")]\n        global_mnt: bool,\n    },\n\n    /// Get kernel version\n    Version,\n\n    /// For testing\n    Test,\n\n    /// Extract an embedded binary to a specified path\n    ExtractBinary {\n        /// binary name (e.g. busybox, resetprop, bootctl)\n        name: String,\n        /// destination file path\n        path: PathBuf,\n    },\n\n    /// Process mark management\n    Mark {\n        #[command(subcommand)]\n        command: MarkCommand,\n    },\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum MarkCommand {\n    /// Get mark status for a process (or all)\n    Get {\n        /// target pid (0 for total count)\n        #[arg(default_value = \"0\")]\n        pid: i32,\n    },\n\n    /// Mark a process\n    Mark {\n        /// target pid (0 for all processes)\n        #[arg(default_value = \"0\")]\n        pid: i32,\n    },\n\n    /// Unmark a process\n    Unmark {\n        /// target pid (0 for all processes)\n        #[arg(default_value = \"0\")]\n        pid: i32,\n    },\n\n    /// Refresh mark for all running processes\n    Refresh,\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum Sepolicy {\n    /// Patch sepolicy\n    Patch {\n        /// sepolicy statements\n        sepolicy: String,\n    },\n\n    /// Apply sepolicy from file\n    Apply {\n        /// sepolicy file path\n        file: String,\n    },\n\n    /// Check if sepolicy statement is supported/valid\n    Check {\n        /// sepolicy statements\n        sepolicy: String,\n    },\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum Module {\n    /// Install module <ZIP>\n    Install {\n        /// module zip file path\n        zip: String,\n    },\n\n    /// Undo module uninstall mark <id>\n    UndoUninstall {\n        /// module id\n        id: String,\n    },\n\n    /// Uninstall module <id>\n    Uninstall {\n        /// module id\n        id: String,\n    },\n\n    /// enable module <id>\n    Enable {\n        /// module id\n        id: String,\n    },\n\n    /// disable module <id>\n    Disable {\n        // module id\n        id: String,\n    },\n\n    /// run action for module <id>\n    Action {\n        // module id\n        id: String,\n    },\n\n    /// list all modules\n    List,\n\n    /// manage module configuration\n    Config {\n        #[command(subcommand)]\n        command: ModuleConfigCmd,\n    },\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum ModuleConfigCmd {\n    /// Get a config value\n    Get {\n        /// config key\n        key: String,\n    },\n\n    /// Set a config value\n    Set {\n        /// config key\n        key: String,\n        /// config value (omit to read from stdin)\n        value: Option<String>,\n        /// read value from stdin (default if value not provided)\n        #[arg(long)]\n        stdin: bool,\n        /// use temporary config (cleared on reboot)\n        #[arg(short, long)]\n        temp: bool,\n    },\n\n    /// List all config entries\n    List,\n\n    /// Delete a config entry\n    Delete {\n        /// config key\n        key: String,\n        /// delete from temporary config\n        #[arg(short, long)]\n        temp: bool,\n    },\n\n    /// Clear all config entries\n    Clear {\n        /// clear temporary config\n        #[arg(short, long)]\n        temp: bool,\n    },\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum Profile {\n    /// get root profile's selinux policy of <package-name>\n    GetSepolicy {\n        /// package name\n        package: String,\n    },\n\n    /// set root profile's selinux policy of <package-name> to <profile>\n    SetSepolicy {\n        /// package name\n        package: String,\n        /// policy statements\n        policy: String,\n    },\n\n    /// get template of <id>\n    GetTemplate {\n        /// template id\n        id: String,\n    },\n\n    /// set template of <id> to <template string>\n    SetTemplate {\n        /// template id\n        id: String,\n        /// template string\n        template: String,\n    },\n\n    /// delete template of <id>\n    DeleteTemplate {\n        /// template id\n        id: String,\n    },\n\n    /// list all templates\n    ListTemplates,\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum Feature {\n    /// Get feature value and support status\n    Get {\n        /// Feature ID or name (su_compat, kernel_umount)\n        id: String,\n        /// Read from config file\n        #[arg(long, default_value_t = false)]\n        config: bool,\n    },\n\n    /// Set feature value\n    Set {\n        /// Feature ID or name\n        id: String,\n        /// Feature value (0=disable, 1=enable)\n        value: u64,\n    },\n\n    /// List all available features\n    List,\n\n    /// Check feature status (supported/unsupported/managed)\n    Check {\n        /// Feature ID or name (su_compat, kernel_umount)\n        id: String,\n    },\n\n    /// Load configuration from file and apply to kernel\n    Load,\n\n    /// Save current kernel feature states to file\n    Save,\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum Kernel {\n    /// Nuke ext4 sysfs\n    NukeExt4Sysfs {\n        /// mount point\n        mnt: String,\n    },\n    /// Manage umount list\n    Umount {\n        #[command(subcommand)]\n        command: UmountOp,\n    },\n    /// Notify that module is mounted\n    NotifyModuleMounted,\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum UmountOp {\n    /// Add mount point to umount list\n    Add {\n        /// mount point path\n        mnt: String,\n        /// umount flags (default: 0, MNT_DETACH: 2)\n        #[arg(short, long, default_value = \"0\")]\n        flags: u32,\n    },\n    /// Delete mount point from umount list\n    Del {\n        /// mount point path\n        mnt: String,\n    },\n    /// Wipe all entries from umount list\n    Wipe,\n}\n\npub fn run() -> Result<()> {\n    android_logger::init_once(\n        Config::default()\n            .with_max_level(crate::debug_select!(LevelFilter::Trace, LevelFilter::Info))\n            .with_tag(\"KernelSU\"),\n    );\n\n    // the kernel executes su with argv[0] = \"su\" and replace it with us\n    let arg0 = std::env::args().next().unwrap_or_default();\n    if arg0 == \"su\" || arg0 == \"/system/bin/su\" {\n        return crate::su::root_shell();\n    }\n\n    if arg0.ends_with(\"resetprop\") {\n        let all_args: Vec<String> = std::env::args().collect();\n        return crate::resetprop::run_from_args(&all_args);\n    }\n\n    let cli = Args::parse();\n\n    log::info!(\"command: {:?}\", cli.command);\n\n    let result = match cli.command {\n        Commands::PostFsData => init_event::on_post_data_fs(),\n        Commands::BootCompleted => {\n            init_event::on_boot_completed();\n            Ok(())\n        }\n\n        Commands::Module { command } => {\n            utils::switch_mnt_ns(1)?;\n            match command {\n                Module::Install { zip } => module::install_module(&zip),\n                Module::UndoUninstall { id } => module::undo_uninstall_module(&id),\n                Module::Uninstall { id } => module::uninstall_module(&id),\n                Module::Enable { id } => module::enable_module(&id),\n                Module::Disable { id } => module::disable_module(&id),\n                Module::Action { id } => module::run_action(&id),\n                Module::List => module::list_modules(),\n                Module::Config { command } => {\n                    // Get module ID from environment variable\n                    let module_id = std::env::var(\"KSU_MODULE\").map_err(|_| {\n                        anyhow::anyhow!(\"This command must be run in the context of a module\")\n                    })?;\n\n                    match command {\n                        ModuleConfigCmd::Get { key } => {\n                            // Use merge_configs to respect priority (temp overrides persist)\n                            let config = module_config::merge_configs(&module_id)?;\n                            match config.get(&key) {\n                                Some(value) => {\n                                    println!(\"{value}\");\n                                    Ok(())\n                                }\n                                None => anyhow::bail!(\"Key '{key}' not found\"),\n                            }\n                        }\n                        ModuleConfigCmd::Set {\n                            key,\n                            value,\n                            stdin,\n                            temp,\n                        } => {\n                            // Validate key at CLI layer for better user experience\n                            module_config::validate_config_key(&key)?;\n\n                            // Read value from stdin or argument\n                            let value_str = match value {\n                                Some(v) if !stdin => v,\n                                _ => {\n                                    // Read from stdin\n                                    use std::io::Read;\n                                    let mut buffer = String::new();\n                                    std::io::stdin()\n                                        .read_to_string(&mut buffer)\n                                        .context(\"Failed to read from stdin\")?;\n                                    buffer\n                                }\n                            };\n\n                            // Validate value\n                            module_config::validate_config_value(&value_str)?;\n\n                            let config_type = if temp {\n                                module_config::ConfigType::Temp\n                            } else {\n                                module_config::ConfigType::Persist\n                            };\n                            module_config::set_config_value(\n                                &module_id,\n                                &key,\n                                &value_str,\n                                config_type,\n                            )\n                        }\n                        ModuleConfigCmd::List => {\n                            let config = module_config::merge_configs(&module_id)?;\n                            if config.is_empty() {\n                                println!(\"No config entries found\");\n                            } else {\n                                for (key, value) in config {\n                                    println!(\"{key}={value}\");\n                                }\n                            }\n                            Ok(())\n                        }\n                        ModuleConfigCmd::Delete { key, temp } => {\n                            let config_type = if temp {\n                                module_config::ConfigType::Temp\n                            } else {\n                                module_config::ConfigType::Persist\n                            };\n                            module_config::delete_config_value(&module_id, &key, config_type)\n                        }\n                        ModuleConfigCmd::Clear { temp } => {\n                            let config_type = if temp {\n                                module_config::ConfigType::Temp\n                            } else {\n                                module_config::ConfigType::Persist\n                            };\n                            module_config::clear_config(&module_id, config_type)\n                        }\n                    }\n                }\n            }\n        }\n        Commands::Install { magiskboot } => utils::install(magiskboot),\n        Commands::Uninstall { magiskboot } => utils::uninstall(magiskboot),\n        Commands::Sepolicy { command } => match command {\n            Sepolicy::Patch { sepolicy } => crate::sepolicy::live_patch(&sepolicy),\n            Sepolicy::Apply { file } => crate::sepolicy::apply_file(file),\n            Sepolicy::Check { sepolicy } => crate::sepolicy::check_rule(&sepolicy),\n        },\n        Commands::LateLoad {\n            magica,\n            post_magica,\n        } => {\n            if let Some(port) = magica {\n                return crate::magica::run(port).map_err(|e| {\n                    error!(\"Error running magica: {e}\");\n                    e\n                });\n            }\n            let result = crate::late_load::run();\n            if post_magica {\n                info!(\"Restoring adb properties (post-magica cleanup)...\");\n                if let Err(e) = crate::magica::disable_adb_root() {\n                    error!(\"disable adb root failed: {e}\");\n                }\n            }\n            result\n        }\n        Commands::Services => {\n            init_event::on_services();\n            Ok(())\n        }\n        Commands::Profile { command } => match command {\n            Profile::GetSepolicy { package } => crate::profile::get_sepolicy(package),\n            Profile::SetSepolicy { package, policy } => {\n                crate::profile::set_sepolicy(package, policy)\n            }\n            Profile::GetTemplate { id } => crate::profile::get_template(id),\n            Profile::SetTemplate { id, template } => crate::profile::set_template(id, template),\n            Profile::DeleteTemplate { id } => crate::profile::delete_template(id),\n            Profile::ListTemplates => crate::profile::list_templates(),\n        },\n\n        Commands::Feature { command } => match command {\n            Feature::Get { id, config } => {\n                if config {\n                    crate::feature::get_feature_config(&id)\n                } else {\n                    crate::feature::get_feature(&id)\n                }\n            }\n            Feature::Set { id, value } => crate::feature::set_feature(&id, value),\n            Feature::List => {\n                crate::feature::list_features();\n                Ok(())\n            }\n            Feature::Check { id } => crate::feature::check_feature(&id),\n            Feature::Load => crate::feature::load_config_and_apply(),\n            Feature::Save => crate::feature::save_config(),\n        },\n\n        Commands::Debug { command } => match command {\n            Debug::SetManager { apk } => debug::set_manager(&apk),\n            Debug::GetSign { apk } => {\n                let sign = apk_sign::get_apk_signature(&apk)?;\n                println!(\"size: {:#x}, hash: {}\", sign.0, sign.1);\n                Ok(())\n            }\n            Debug::Version => {\n                println!(\"Kernel Version: {}\", ksucalls::get_version());\n                Ok(())\n            }\n            Debug::Su { global_mnt } => crate::su::grant_root(global_mnt),\n            Debug::Test => assets::ensure_binaries(false),\n            Debug::ExtractBinary { name, path } => {\n                let data = assets::get_asset_data(&name)?;\n                utils::ensure_binary(&path, &data, false)\n            }\n            Debug::Mark { command } => match command {\n                MarkCommand::Get { pid } => debug::mark_get(pid),\n                MarkCommand::Mark { pid } => debug::mark_set(pid),\n                MarkCommand::Unmark { pid } => debug::mark_unset(pid),\n                MarkCommand::Refresh => debug::mark_refresh(),\n            },\n        },\n\n        Commands::BootPatch(boot_patch) => crate::boot_patch::patch(boot_patch),\n\n        Commands::BootInfo { command } => match command {\n            BootInfo::CurrentKmi => {\n                let kmi = crate::boot_patch::get_current_kmi()?;\n                println!(\"{kmi}\");\n                // return here to avoid printing the error message\n                return Ok(());\n            }\n            BootInfo::SupportedKmis => {\n                let kmi = crate::assets::list_supported_kmi();\n                for kmi in &kmi {\n                    println!(\"{kmi}\");\n                }\n                return Ok(());\n            }\n            BootInfo::IsAbDevice => {\n                let val = crate::utils::getprop(\"ro.build.ab_update\")\n                    .unwrap_or_else(|| String::from(\"false\"));\n                let is_ab = val.trim().to_lowercase() == \"true\";\n                println!(\"{}\", if is_ab { \"true\" } else { \"false\" });\n                return Ok(());\n            }\n            BootInfo::DefaultPartition => {\n                let kmi = crate::boot_patch::get_current_kmi().unwrap_or_else(|_| String::new());\n                let name = crate::boot_patch::choose_boot_partition(&kmi, false, &None);\n                println!(\"{name}\");\n                return Ok(());\n            }\n            BootInfo::SlotSuffix { ota } => {\n                let suffix = crate::boot_patch::get_slot_suffix(ota);\n                println!(\"{suffix}\");\n                return Ok(());\n            }\n            BootInfo::AvailablePartitions => {\n                let parts = crate::boot_patch::list_available_partitions();\n                for p in &parts {\n                    println!(\"{p}\");\n                }\n                return Ok(());\n            }\n        },\n        Commands::BootRestore(boot_restore) => crate::boot_patch::restore(boot_restore),\n        Commands::Resetprop { args } => {\n            let mut full_args = vec![\"resetprop\".to_string()];\n            full_args.extend(args);\n            crate::resetprop::run_from_args(&full_args)\n        }\n\n        Commands::Kernel { command } => match command {\n            Kernel::NukeExt4Sysfs { mnt } => ksucalls::nuke_ext4_sysfs(&mnt),\n            Kernel::Umount { command } => match command {\n                UmountOp::Add { mnt, flags } => ksucalls::umount_list_add(&mnt, flags),\n                UmountOp::Del { mnt } => ksucalls::umount_list_del(&mnt),\n                UmountOp::Wipe => ksucalls::umount_list_wipe().map_err(Into::into),\n            },\n            Kernel::NotifyModuleMounted => {\n                ksucalls::report_module_mounted();\n                Ok(())\n            }\n        },\n    };\n\n    if let Err(e) = &result {\n        log::error!(\"Error: {e:?}\");\n    }\n    result\n}\n"
  },
  {
    "path": "userspace/ksud/src/cli_non_android.rs",
    "content": "use anyhow::Result;\nuse clap::Parser;\n\nuse crate::boot_patch::{BootPatchArgs, BootRestoreArgs};\nuse crate::{apk_sign, defs};\n\n/// KernelSU cli for non-android\n#[derive(Parser, Debug)]\n#[command(author, version = defs::VERSION_NAME, about, long_about = None)]\nstruct Args {\n    #[command(subcommand)]\n    command: Commands,\n}\n\n#[derive(clap::Subcommand, Debug)]\nenum Commands {\n    /// Patch boot or init_boot images to apply KernelSU\n    BootPatch(BootPatchArgs),\n\n    /// Restore boot or init_boot images patched by KernelSU\n    BootRestore(BootRestoreArgs),\n\n    /// Get apk size and hash\n    GetSign {\n        /// apk path\n        apk: String,\n    },\n\n    /// show supported kmi versions\n    SupportedKmis,\n}\n\npub fn run() -> Result<()> {\n    env_logger::init();\n\n    let cli = Args::parse();\n\n    log::info!(\"command: {:?}\", cli.command);\n\n    let result = match cli.command {\n        Commands::GetSign { apk } => {\n            let sign = apk_sign::get_apk_signature(&apk)?;\n            println!(\"size: {:#x}, hash: {}\", sign.0, sign.1);\n            Ok(())\n        }\n\n        Commands::BootPatch(boot_patch) => crate::boot_patch::patch(boot_patch),\n\n        Commands::BootRestore(boot_restore) => crate::boot_patch::restore(boot_restore),\n\n        Commands::SupportedKmis => {\n            let kmi = crate::assets::list_supported_kmi();\n            for kmi in &kmi {\n                println!(\"{kmi}\");\n            }\n            Ok(())\n        }\n    };\n\n    if let Err(e) = &result {\n        log::error!(\"Error: {e:?}\");\n    }\n    result\n}\n"
  },
  {
    "path": "userspace/ksud/src/debug.rs",
    "content": "use anyhow::{Context, Ok, Result, bail, ensure};\nuse std::{\n    path::{Path, PathBuf},\n    process::Command,\n};\n\nuse crate::ksucalls;\n\nconst KERNEL_PARAM_PATH: &str = \"/sys/module/kernelsu\";\n\nfn read_u32(path: &PathBuf) -> Result<u32> {\n    let content = std::fs::read_to_string(path)?;\n    let content = content.trim();\n    let content = content.parse::<u32>()?;\n    Ok(content)\n}\n\nfn set_kernel_param(uid: u32) -> Result<()> {\n    let kernel_param_path = Path::new(KERNEL_PARAM_PATH).join(\"parameters\");\n\n    let ksu_debug_manager_uid = kernel_param_path.join(\"ksu_debug_manager_uid\");\n    let before_uid = read_u32(&ksu_debug_manager_uid)?;\n    std::fs::write(&ksu_debug_manager_uid, uid.to_string())?;\n    let after_uid = read_u32(&ksu_debug_manager_uid)?;\n\n    println!(\"set manager uid: {before_uid} -> {after_uid}\");\n\n    Ok(())\n}\n\nfn get_pkg_uid(pkg: &str) -> Result<u32> {\n    // stat /data/data/<pkg>\n    let uid = rustix::fs::stat(format!(\"/data/data/{pkg}\"))\n        .with_context(|| format!(\"stat /data/data/{pkg}\"))?\n        .st_uid;\n    Ok(uid)\n}\n\npub fn set_manager(pkg: &str) -> Result<()> {\n    ensure!(\n        Path::new(KERNEL_PARAM_PATH).exists(),\n        \"CONFIG_KSU_DEBUG is not enabled\"\n    );\n\n    let uid = get_pkg_uid(pkg)?;\n    set_kernel_param(uid)?;\n    // force-stop it\n    let _ = Command::new(\"am\").args([\"force-stop\", pkg]).status();\n    Ok(())\n}\n\n/// Get mark status for a process\npub fn mark_get(pid: i32) -> Result<()> {\n    let result = ksucalls::mark_get(pid)?;\n    if pid == 0 {\n        bail!(\"Please specify a pid to get its mark status\");\n    }\n    println!(\n        \"Process {pid} mark status: {}\",\n        if result != 0 { \"marked\" } else { \"unmarked\" }\n    );\n    Ok(())\n}\n\n/// Mark a process\npub fn mark_set(pid: i32) -> Result<()> {\n    ksucalls::mark_set(pid)?;\n    if pid == 0 {\n        println!(\"All processes marked successfully\");\n    } else {\n        println!(\"Process {pid} marked successfully\");\n    }\n    Ok(())\n}\n\n/// Unmark a process\npub fn mark_unset(pid: i32) -> Result<()> {\n    ksucalls::mark_unset(pid)?;\n    if pid == 0 {\n        println!(\"All processes unmarked successfully\");\n    } else {\n        println!(\"Process {pid} unmarked successfully\");\n    }\n    Ok(())\n}\n\n/// Refresh mark for all running processes\npub fn mark_refresh() -> Result<()> {\n    ksucalls::mark_refresh()?;\n    println!(\"Refreshed mark for all running processes\");\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/defs.rs",
    "content": "#[cfg(target_os = \"android\")]\nmod android {\n    use const_format::concatcp;\n\n    pub const ADB_DIR: &str = \"/data/adb/\";\n    pub const WORKING_DIR: &str = concatcp!(ADB_DIR, \"ksu/\");\n    pub const BINARY_DIR: &str = concatcp!(WORKING_DIR, \"bin/\");\n    pub const LOG_DIR: &str = concatcp!(WORKING_DIR, \"log/\");\n\n    pub const PROFILE_DIR: &str = concatcp!(WORKING_DIR, \"profile/\");\n    pub const PROFILE_SELINUX_DIR: &str = concatcp!(PROFILE_DIR, \"selinux/\");\n    pub const PROFILE_TEMPLATE_DIR: &str = concatcp!(PROFILE_DIR, \"templates/\");\n\n    pub const KSURC_PATH: &str = concatcp!(WORKING_DIR, \".ksurc\");\n    pub const DAEMON_PATH: &str = concatcp!(ADB_DIR, \"ksud\");\n    pub const MAGISKBOOT_PATH: &str = concatcp!(BINARY_DIR, \"magiskboot\");\n\n    pub const DAEMON_LINK_PATH: &str = concatcp!(BINARY_DIR, \"ksud\");\n\n    pub const MODULE_DIR: &str = concatcp!(ADB_DIR, \"modules/\");\n    pub const MODULE_UPDATE_DIR: &str = concatcp!(ADB_DIR, \"modules_update/\");\n    pub const METAMODULE_DIR: &str = concatcp!(ADB_DIR, \"metamodule/\");\n\n    pub const MODULE_WEB_DIR: &str = \"webroot\";\n    pub const MODULE_ACTION_SH: &str = \"action.sh\";\n    pub const DISABLE_FILE_NAME: &str = \"disable\";\n    pub const UPDATE_FILE_NAME: &str = \"update\";\n    pub const REMOVE_FILE_NAME: &str = \"remove\";\n\n    // Module config system\n    pub const MODULE_CONFIG_DIR: &str = concatcp!(WORKING_DIR, \"module_configs/\");\n    pub const PERSIST_CONFIG_NAME: &str = \"persist.config\";\n    pub const TEMP_CONFIG_NAME: &str = \"tmp.config\";\n\n    // Metamodule support\n    pub const METAMODULE_MOUNT_SCRIPT: &str = \"metamount.sh\";\n    pub const METAMODULE_METAINSTALL_SCRIPT: &str = \"metainstall.sh\";\n    pub const METAMODULE_METAUNINSTALL_SCRIPT: &str = \"metauninstall.sh\";\n\n    pub const KSU_BACKUP_DIR: &str = WORKING_DIR;\n    pub const KSU_BACKUP_FILE_PREFIX: &str = \"ksu_backup_\";\n    pub const BACKUP_FILENAME: &str = \"stock_image.sha1\";\n}\n\npub const VERSION_CODE: &str = include_str!(concat!(env!(\"OUT_DIR\"), \"/VERSION_CODE\"));\npub const VERSION_NAME: &str = include_str!(concat!(env!(\"OUT_DIR\"), \"/VERSION_NAME\"));\n\n#[cfg(target_os = \"android\")]\npub use android::*;\n"
  },
  {
    "path": "userspace/ksud/src/feature.rs",
    "content": "use anyhow::{Context, Result, bail};\nuse const_format::concatcp;\nuse std::collections::HashMap;\nuse std::fs::File;\nuse std::io::{Read, Write};\nuse std::path::Path;\n\nuse crate::defs;\n\nconst FEATURE_CONFIG_PATH: &str = concatcp!(defs::WORKING_DIR, \".feature_config\");\n#[allow(clippy::unreadable_literal)]\nconst FEATURE_MAGIC: u32 = 0x7f4b5355;\nconst FEATURE_VERSION: u32 = 1;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\n#[repr(u32)]\npub enum FeatureId {\n    SuCompat = 0,\n    KernelUmount = 1,\n}\n\nimpl FeatureId {\n    pub const fn from_u32(id: u32) -> Option<Self> {\n        match id {\n            0 => Some(Self::SuCompat),\n            1 => Some(Self::KernelUmount),\n            _ => None,\n        }\n    }\n\n    pub const fn name(self) -> &'static str {\n        match self {\n            Self::SuCompat => \"su_compat\",\n            Self::KernelUmount => \"kernel_umount\",\n        }\n    }\n\n    pub const fn description(self) -> &'static str {\n        match self {\n            Self::SuCompat => {\n                \"SU Compatibility Mode - allows authorized apps to gain root via traditional 'su' command\"\n            }\n            Self::KernelUmount => {\n                \"Kernel Umount - controls whether kernel automatically unmounts modules when not needed\"\n            }\n        }\n    }\n}\n\nfn parse_feature_id(name: &str) -> Result<FeatureId> {\n    match name {\n        \"su_compat\" | \"0\" => Ok(FeatureId::SuCompat),\n        \"kernel_umount\" | \"1\" => Ok(FeatureId::KernelUmount),\n        _ => bail!(\"Unknown feature: {name}\"),\n    }\n}\n\npub fn load_binary_config() -> Result<HashMap<u32, u64>> {\n    let path = Path::new(FEATURE_CONFIG_PATH);\n    if !path.exists() {\n        log::info!(\"Feature config not found, using defaults\");\n        return Ok(HashMap::new());\n    }\n\n    let mut file = File::open(path).with_context(|| \"Failed to open feature config\")?;\n\n    let mut magic_buf = [0u8; 4];\n    file.read_exact(&mut magic_buf)\n        .with_context(|| \"Failed to read magic\")?;\n    let magic = u32::from_le_bytes(magic_buf);\n\n    if magic != FEATURE_MAGIC {\n        bail!(\"Invalid feature config magic: expected 0x{FEATURE_MAGIC:08x}, got 0x{magic:08x}\",);\n    }\n\n    let mut version_buf = [0u8; 4];\n    file.read_exact(&mut version_buf)\n        .with_context(|| \"Failed to read version\")?;\n    let version = u32::from_le_bytes(version_buf);\n\n    if version != FEATURE_VERSION {\n        log::warn!(\n            \"Feature config version mismatch: expected {FEATURE_VERSION}, got {version\n            }\",\n        );\n    }\n\n    let mut count_buf = [0u8; 4];\n    file.read_exact(&mut count_buf)\n        .with_context(|| \"Failed to read count\")?;\n    let count = u32::from_le_bytes(count_buf);\n\n    let mut features = HashMap::new();\n    for _ in 0..count {\n        let mut id_buf = [0u8; 4];\n        let mut value_buf = [0u8; 8];\n\n        file.read_exact(&mut id_buf)\n            .with_context(|| \"Failed to read feature id\")?;\n        file.read_exact(&mut value_buf)\n            .with_context(|| \"Failed to read feature value\")?;\n\n        let id = u32::from_le_bytes(id_buf);\n        let value = u64::from_le_bytes(value_buf);\n\n        features.insert(id, value);\n    }\n\n    log::info!(\"Loaded {} features from config\", features.len());\n    Ok(features)\n}\n\npub fn save_binary_config(features: &HashMap<u32, u64>) -> Result<()> {\n    crate::utils::ensure_dir_exists(Path::new(defs::WORKING_DIR))?;\n\n    let path = Path::new(FEATURE_CONFIG_PATH);\n    let mut file = File::create(path).with_context(|| \"Failed to create feature config\")?;\n\n    file.write_all(&FEATURE_MAGIC.to_le_bytes())\n        .with_context(|| \"Failed to write magic\")?;\n\n    file.write_all(&FEATURE_VERSION.to_le_bytes())\n        .with_context(|| \"Failed to write version\")?;\n\n    let count = features.len() as u32;\n    file.write_all(&count.to_le_bytes())\n        .with_context(|| \"Failed to write count\")?;\n\n    for (&id, &value) in features {\n        file.write_all(&id.to_le_bytes())\n            .with_context(|| format!(\"Failed to write feature id {id}\"))?;\n        file.write_all(&value.to_le_bytes())\n            .with_context(|| format!(\"Failed to write feature value for id {id}\"))?;\n    }\n\n    file.sync_all()\n        .with_context(|| \"Failed to sync feature config\")?;\n\n    log::info!(\"Saved {} features to config\", features.len());\n    Ok(())\n}\n\npub fn apply_config(features: &HashMap<u32, u64>) {\n    log::info!(\"Applying feature configuration to kernel...\");\n\n    let mut applied = 0;\n    for (&id, &value) in features {\n        match crate::ksucalls::set_feature(id, value) {\n            Ok(()) => {\n                if let Some(feature_id) = FeatureId::from_u32(id) {\n                    log::info!(\"Set feature {} to {value}\", feature_id.name());\n                } else {\n                    log::info!(\"Set feature {id} to {value}\");\n                }\n                applied += 1;\n            }\n            Err(e) => {\n                log::warn!(\"Failed to set feature {id}: {e}\");\n            }\n        }\n    }\n\n    log::info!(\"Applied {applied} features successfully\");\n}\n\npub fn get_feature(id: &str) -> Result<()> {\n    let feature_id = parse_feature_id(id)?;\n    let (value, supported) = crate::ksucalls::get_feature(feature_id as u32)\n        .with_context(|| format!(\"Failed to get feature {id}\"))?;\n\n    if !supported {\n        println!(\"Feature '{id}' is not supported by kernel\");\n        return Ok(());\n    }\n\n    println!(\"Feature: {} ({})\", feature_id.name(), feature_id as u32);\n    println!(\"Description: {}\", feature_id.description());\n    println!(\"Value: {value}\");\n    println!(\n        \"Status: {}\",\n        if value != 0 { \"enabled\" } else { \"disabled\" }\n    );\n\n    Ok(())\n}\n\npub fn get_feature_config(id: &str) -> Result<()> {\n    let feature_id = parse_feature_id(id)?;\n\n    let features = load_binary_config()?;\n    let id_u32 = feature_id as u32;\n\n    println!(\"Feature: {} ({})\", feature_id.name(), id_u32);\n    println!(\"Description: {}\", feature_id.description());\n\n    if let Some(value) = features.get(&id_u32) {\n        println!(\"Value: {value}\");\n        println!(\n            \"Status: {}\",\n            if *value != 0 { \"enabled\" } else { \"disabled\" }\n        );\n    } else {\n        println!(\"Not set in config\");\n    }\n\n    Ok(())\n}\n\npub fn set_feature(id: &str, value: u64) -> Result<()> {\n    let feature_id = parse_feature_id(id)?;\n\n    // Check if this feature is managed by any module\n    if let Ok(managed_features_map) = crate::module::get_managed_features() {\n        // Find which modules manage this feature\n        let managing_modules: Vec<&String> = managed_features_map\n            .iter()\n            .filter(|(_, features)| features.iter().any(|f| f == feature_id.name()))\n            .map(|(module_id, _)| module_id)\n            .collect();\n\n        if !managing_modules.is_empty() {\n            // Feature is managed, check if caller is an authorized module\n            let caller_module = std::env::var(\"KSU_MODULE\").unwrap_or_default();\n\n            if caller_module.is_empty() || !managing_modules.contains(&&caller_module) {\n                bail!(\n                    \"Feature '{}' is managed by module(s): {}. Direct modification is not allowed.\",\n                    feature_id.name(),\n                    managing_modules\n                        .iter()\n                        .map(|s| s.as_str())\n                        .collect::<Vec<_>>()\n                        .join(\", \")\n                );\n            }\n\n            log::info!(\n                \"Module '{caller_module}' is setting managed feature '{}'\",\n                feature_id.name()\n            );\n        }\n    }\n\n    crate::ksucalls::set_feature(feature_id as u32, value)\n        .with_context(|| format!(\"Failed to set feature {id} to {value}\"))?;\n\n    println!(\n        \"Feature '{}' set to {value} ({})\",\n        feature_id.name(),\n        if value != 0 { \"enabled\" } else { \"disabled\" }\n    );\n\n    Ok(())\n}\n\npub fn list_features() {\n    println!(\"Available Features:\");\n    println!(\"{}\", \"=\".repeat(80));\n\n    // Get managed features from modules\n    let managed_features_map = crate::module::get_managed_features().unwrap_or_default();\n\n    // Build a reverse map: feature_name -> Vec<module_id>\n    let mut feature_to_modules: HashMap<String, Vec<String>> = HashMap::new();\n    for (module_id, feature_list) in &managed_features_map {\n        for feature_name in feature_list {\n            feature_to_modules\n                .entry(feature_name.clone())\n                .or_default()\n                .push(module_id.clone());\n        }\n    }\n\n    let all_features = [FeatureId::SuCompat, FeatureId::KernelUmount];\n\n    for feature_id in &all_features {\n        let id = *feature_id as u32;\n        let (value, supported) = crate::ksucalls::get_feature(id).unwrap_or((0, false));\n\n        let status = if !supported {\n            \"NOT_SUPPORTED\".to_string()\n        } else if value != 0 {\n            format!(\"ENABLED ({value})\")\n        } else {\n            \"DISABLED\".to_string()\n        };\n\n        let managed_by = feature_to_modules.get(feature_id.name());\n        let managed_mark = if managed_by.is_some() {\n            \" [MODULE_MANAGED]\"\n        } else {\n            \"\"\n        };\n\n        println!(\n            \"[{}] {} (ID={}){}\",\n            status,\n            feature_id.name(),\n            id,\n            managed_mark\n        );\n        println!(\"    {}\", feature_id.description());\n\n        if let Some(modules) = managed_by {\n            println!(\n                \"    ⚠️  Managed by module(s): {} (forced to 0 on initialization)\",\n                modules.join(\", \")\n            );\n        }\n\n        println!();\n    }\n}\n\npub fn load_config_and_apply() -> Result<()> {\n    let features = load_binary_config()?;\n\n    if features.is_empty() {\n        println!(\"No features found in config file\");\n        return Ok(());\n    }\n\n    apply_config(&features);\n    println!(\"Feature configuration loaded and applied\");\n    Ok(())\n}\n\npub fn save_config() -> Result<()> {\n    let mut features = HashMap::new();\n\n    let all_features = [FeatureId::SuCompat, FeatureId::KernelUmount];\n\n    for feature_id in &all_features {\n        let id = *feature_id as u32;\n        if let Ok((value, supported)) = crate::ksucalls::get_feature(id)\n            && supported\n        {\n            features.insert(id, value);\n            log::info!(\"Saved feature {} = {value}\", feature_id.name());\n        }\n    }\n\n    save_binary_config(&features)?;\n    println!(\n        \"Current feature states saved to config file ({} features)\",\n        features.len()\n    );\n    Ok(())\n}\n\npub fn check_feature(id: &str) -> Result<()> {\n    let feature_id = parse_feature_id(id)?;\n\n    // Check if this feature is managed by any module\n    let managed_features_map = crate::module::get_managed_features().unwrap_or_default();\n    let is_managed = managed_features_map\n        .values()\n        .any(|features| features.iter().any(|f| f == feature_id.name()));\n\n    if is_managed {\n        println!(\"managed\");\n        return Ok(());\n    }\n\n    // Check if the feature is supported by kernel\n    let (_value, supported) = crate::ksucalls::get_feature(feature_id as u32)\n        .with_context(|| format!(\"Failed to get feature {id}\"))?;\n\n    if supported {\n        println!(\"supported\");\n    } else {\n        println!(\"unsupported\");\n    }\n\n    Ok(())\n}\n\npub fn init_features() -> Result<()> {\n    log::info!(\"Initializing features from config...\");\n\n    let mut features = load_binary_config()?;\n\n    // Get managed features from active modules and skip them during init\n    if let Ok(managed_features_map) = crate::module::get_managed_features() {\n        if !managed_features_map.is_empty() {\n            log::info!(\n                \"Found {} modules managing features\",\n                managed_features_map.len()\n            );\n\n            // Build a set of all managed feature IDs to skip\n            for (module_id, feature_list) in &managed_features_map {\n                log::info!(\n                    \"Module '{module_id}' manages {} feature(s)\",\n                    feature_list.len()\n                );\n\n                for feature_name in feature_list {\n                    if let Ok(feature_id) = parse_feature_id(feature_name) {\n                        let feature_id_u32 = feature_id as u32;\n                        // Remove managed features from config, let modules control them\n                        if features.remove(&feature_id_u32).is_some() {\n                            log::info!(\n                                \"  - Skipping managed feature '{feature_name}' (controlled by module: {module_id})\",\n                            );\n                        } else {\n                            log::info!(\n                                \"  - Feature '{feature_name}' is managed by module '{module_id}', skipping\",\n                            );\n                        }\n                    } else {\n                        log::warn!(\n                            \"  - Unknown managed feature '{feature_name}' from module '{module_id}', ignoring\",\n                        );\n                    }\n                }\n            }\n        }\n    } else {\n        log::warn!(\n            \"Failed to get managed features from modules, continuing with normal initialization\"\n        );\n    }\n\n    if features.is_empty() {\n        log::info!(\"No features to apply, skipping initialization\");\n        return Ok(());\n    }\n\n    apply_config(&features);\n\n    // Save the configuration (excluding managed features)\n    save_binary_config(&features)?;\n    log::info!(\"Saved feature configuration to file\");\n\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/init_event.rs",
    "content": "use anyhow::{Context, Result};\nuse log::{info, warn};\nuse std::path::Path;\n\nuse crate::module::{handle_updated_modules, prune_modules};\nuse crate::utils::is_safe_mode;\nuse crate::{\n    assets, defs, ksucalls, metamodule, restorecon,\n    utils::{self},\n};\n\npub fn on_post_data_fs() -> Result<()> {\n    ksucalls::report_post_fs_data();\n\n    utils::umask(0);\n\n    // Clear all temporary module configs early\n    if let Err(e) = crate::module_config::clear_all_temp_configs() {\n        warn!(\"clear temp configs failed: {e}\");\n    }\n\n    #[cfg(unix)]\n    let _ = catch_bootlog(\"logcat\", &[\"logcat\", \"-b\", \"all\"]);\n    #[cfg(unix)]\n    let _ = catch_bootlog(\"dmesg\", &[\"dmesg\", \"-w\", \"-r\"]);\n\n    if utils::has_magisk() {\n        warn!(\"Magisk detected, skip post-fs-data!\");\n        return Ok(());\n    }\n\n    let safe_mode = crate::utils::is_safe_mode();\n\n    if safe_mode {\n        // we should still ensure module directory exists in safe mode\n        // because we may need to operate the module dir in safe mode\n        warn!(\"safe mode, skip common post-fs-data.d scripts\");\n    } else {\n        // Then exec common post-fs-data scripts\n        if let Err(e) = crate::module::exec_common_scripts(\"post-fs-data.d\", true) {\n            warn!(\"exec common post-fs-data scripts failed: {e}\");\n        }\n    }\n\n    let module_dir = defs::MODULE_DIR;\n\n    assets::ensure_binaries(true).with_context(|| \"Failed to extract bin assets\")?;\n\n    // if we are in safe mode, we should disable all modules\n    if safe_mode {\n        warn!(\"safe mode, skip post-fs-data scripts and disable all modules!\");\n        if let Err(e) = crate::module::disable_all_modules() {\n            warn!(\"disable all modules failed: {e}\");\n        }\n        return Ok(());\n    }\n\n    if let Err(e) = handle_updated_modules() {\n        warn!(\"handle updated modules failed: {e}\");\n    }\n\n    if let Err(e) = prune_modules() {\n        warn!(\"prune modules failed: {e}\");\n    }\n\n    if let Err(e) = restorecon::restorecon() {\n        warn!(\"restorecon failed: {e}\");\n    }\n\n    // load sepolicy.rule\n    if crate::module::load_sepolicy_rule().is_err() {\n        warn!(\"load sepolicy.rule failed\");\n    }\n\n    if let Err(e) = crate::profile::apply_sepolies() {\n        warn!(\"apply root profile sepolicy failed: {e}\");\n    }\n\n    // load feature config\n    if is_safe_mode() {\n        warn!(\"safe mode, skip load feature config\");\n    } else if let Err(e) = crate::feature::init_features() {\n        warn!(\"init features failed: {e}\");\n    }\n\n    // execute metamodule post-fs-data script first (priority)\n    if let Err(e) = metamodule::exec_stage_script(\"post-fs-data\", true) {\n        warn!(\"exec metamodule post-fs-data script failed: {e}\");\n    }\n\n    // exec modules post-fs-data scripts\n    // TODO: Add timeout\n    if let Err(e) = crate::module::exec_stage_script(\"post-fs-data\", true) {\n        warn!(\"exec post-fs-data scripts failed: {e}\");\n    }\n\n    // load system.prop\n    if let Err(e) = crate::module::load_system_prop() {\n        warn!(\"load system.prop failed: {e}\");\n    }\n\n    // execute metamodule mount script\n    if let Err(e) = metamodule::exec_mount_script(module_dir) {\n        warn!(\"execute metamodule mount failed: {e}\");\n    }\n\n    run_stage(\"post-mount\", true);\n\n    std::env::set_current_dir(\"/\").with_context(|| \"failed to chdir to /\")?;\n\n    Ok(())\n}\n\npub fn run_stage(stage: &str, block: bool) {\n    utils::umask(0);\n\n    if utils::has_magisk() {\n        warn!(\"Magisk detected, skip {stage}\");\n        return;\n    }\n\n    if crate::utils::is_safe_mode() {\n        warn!(\"safe mode, skip {stage} scripts\");\n        return;\n    }\n\n    if let Err(e) = crate::module::exec_common_scripts(&format!(\"{stage}.d\"), block) {\n        warn!(\"Failed to exec common {stage} scripts: {e}\");\n    }\n\n    // execute metamodule stage script first (priority)\n    if let Err(e) = metamodule::exec_stage_script(stage, block) {\n        warn!(\"Failed to exec metamodule {stage} script: {e}\");\n    }\n\n    // execute regular modules stage scripts\n    if let Err(e) = crate::module::exec_stage_script(stage, block) {\n        warn!(\"Failed to exec {stage} scripts: {e}\");\n    }\n}\n\npub fn on_services() {\n    info!(\"on_services triggered!\");\n    run_stage(\"service\", false);\n}\n\npub fn on_boot_completed() {\n    ksucalls::report_boot_complete();\n    info!(\"on_boot_completed triggered!\");\n\n    run_stage(\"boot-completed\", false);\n}\n\n#[cfg(unix)]\nfn catch_bootlog(logname: &str, command: &[&str]) -> Result<()> {\n    use std::os::unix::process::CommandExt;\n    use std::process::Stdio;\n\n    let logdir = Path::new(defs::LOG_DIR);\n    utils::ensure_dir_exists(logdir)?;\n    let bootlog = logdir.join(format!(\"{logname}.log\"));\n    let oldbootlog = logdir.join(format!(\"{logname}.old.log\"));\n\n    if bootlog.exists() {\n        std::fs::rename(&bootlog, oldbootlog)?;\n    }\n\n    let bootlog = std::fs::File::create(bootlog)?;\n\n    let mut args = vec![\"-s\", \"9\", \"30s\"];\n    args.extend_from_slice(command);\n    // timeout -s 9 30s logcat > boot.log\n    let result = unsafe {\n        std::process::Command::new(\"timeout\")\n            .process_group(0)\n            .pre_exec(|| {\n                utils::switch_cgroups();\n                Ok(())\n            })\n            .args(args)\n            .stdout(Stdio::from(bootlog))\n            .spawn()\n    };\n\n    if let Err(e) = result {\n        warn!(\"Failed to start logcat: {e:#}\");\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/installer.sh",
    "content": "#!/system/bin/sh\n############################################\n# KernelSU installer script\n# mostly from module_installer.sh\n# and util_functions.sh in Magisk\n############################################\n\numask 022\n\nui_print() {\n  if $BOOTMODE; then\n    echo \"$1\"\n  else\n    echo -e \"ui_print $1\\nui_print\" >> /proc/self/fd/$OUTFD\n  fi\n}\n\ntoupper() {\n  echo \"$@\" | tr '[:lower:]' '[:upper:]'\n}\n\ngrep_cmdline() {\n  local REGEX=\"s/^$1=//p\"\n  { echo $(cat /proc/cmdline)$(sed -e 's/[^\"]//g' -e 's/\"\"//g' /proc/cmdline) | xargs -n 1; \\\n    sed -e 's/ = /=/g' -e 's/, /,/g' -e 's/\"//g' /proc/bootconfig; \\\n  } 2>/dev/null | sed -n \"$REGEX\"\n}\n\ngrep_prop() {\n  local REGEX=\"s/$1=//p\"\n  shift\n  local FILES=$@\n  [ -z \"$FILES\" ] && FILES='/system/build.prop'\n  cat $FILES 2>/dev/null | dos2unix | sed -n \"$REGEX\" | head -n 1 | xargs\n}\n\ngrep_get_prop() {\n  local result=$(grep_prop $@)\n  if [ -z \"$result\" ]; then\n    # Fallback to getprop\n    getprop \"$1\"\n  else\n    echo $result\n  fi\n}\n\nis_mounted() {\n  grep -q \" $(readlink -f $1) \" /proc/mounts 2>/dev/null\n  return $?\n}\n\nabort() {\n  ui_print \"$1\"\n  $BOOTMODE || recovery_cleanup\n  [ ! -z $MODPATH ] && rm -rf $MODPATH\n  rm -rf $TMPDIR\n  exit 1\n}\n\nprint_title() {\n  local len line1len line2len bar\n  line1len=$(echo -n $1 | wc -c)\n  line2len=$(echo -n $2 | wc -c)\n  len=$line2len\n  [ $line1len -gt $line2len ] && len=$line1len\n  len=$((len + 2))\n  bar=$(printf \"%${len}s\" | tr ' ' '*')\n  ui_print \"$bar\"\n  ui_print \" $1 \"\n  [ \"$2\" ] && ui_print \" $2 \"\n  ui_print \"$bar\"\n}\n\ncheck_sepolicy() {\n    /data/adb/ksud sepolicy check \"$1\"\n    return $?\n}\n\n######################\n# Environment Related\n######################\n\nsetup_flashable() {\n  ensure_bb\n  $BOOTMODE && return\n  if [ -z $OUTFD ] || readlink /proc/$$/fd/$OUTFD | grep -q /tmp; then\n    # We will have to manually find out OUTFD\n    for FD in `ls /proc/$$/fd`; do\n      if readlink /proc/$$/fd/$FD | grep -q pipe; then\n        if ps | grep -v grep | grep -qE \" 3 $FD |status_fd=$FD\"; then\n          OUTFD=$FD\n          break\n        fi\n      fi\n    done\n  fi\n  recovery_actions\n}\n\nensure_bb() {\n  :\n}\n\nrecovery_actions() {\n  :\n}\n\nrecovery_cleanup() {\n  :\n}\n\n#######################\n# Installation Related\n#######################\n\n# find_block [partname...]\nfind_block() {\n  local BLOCK DEV DEVICE DEVNAME PARTNAME UEVENT\n  for BLOCK in \"$@\"; do\n    DEVICE=`find /dev/block \\( -type b -o -type c -o -type l \\) -iname $BLOCK | head -n 1` 2>/dev/null\n    if [ ! -z $DEVICE ]; then\n      readlink -f $DEVICE\n      return 0\n    fi\n  done\n  # Fallback by parsing sysfs uevents\n  for UEVENT in /sys/dev/block/*/uevent; do\n    DEVNAME=`grep_prop DEVNAME $UEVENT`\n    PARTNAME=`grep_prop PARTNAME $UEVENT`\n    for BLOCK in \"$@\"; do\n      if [ \"$(toupper $BLOCK)\" = \"$(toupper $PARTNAME)\" ]; then\n        echo /dev/block/$DEVNAME\n        return 0\n      fi\n    done\n  done\n  # Look just in /dev in case we're dealing with MTD/NAND without /dev/block devices/links\n  for DEV in \"$@\"; do\n    DEVICE=`find /dev \\( -type b -o -type c -o -type l \\) -maxdepth 1 -iname $DEV | head -n 1` 2>/dev/null\n    if [ ! -z $DEVICE ]; then\n      readlink -f $DEVICE\n      return 0\n    fi\n  done\n  return 1\n}\n\n# setup_mntpoint <mountpoint>\nsetup_mntpoint() {\n  local POINT=$1\n  [ -L $POINT ] && mv -f $POINT ${POINT}_link\n  if [ ! -d $POINT ]; then\n    rm -f $POINT\n    mkdir -p $POINT\n  fi\n}\n\n# mount_name <partname(s)> <mountpoint> <flag>\nmount_name() {\n  local PART=$1\n  local POINT=$2\n  local FLAG=$3\n  setup_mntpoint $POINT\n  is_mounted $POINT && return\n  # First try mounting with fstab\n  mount $FLAG $POINT 2>/dev/null\n  if ! is_mounted $POINT; then\n    local BLOCK=$(find_block $PART)\n    mount $FLAG $BLOCK $POINT || return\n  fi\n  ui_print \"- Mounting $POINT\"\n}\n\n# mount_ro_ensure <partname(s)> <mountpoint>\nmount_ro_ensure() {\n  # We handle ro partitions only in recovery\n  $BOOTMODE && return\n  local PART=$1\n  local POINT=$2\n  mount_name \"$PART\" $POINT '-o ro'\n  is_mounted $POINT || abort \"! Cannot mount $POINT\"\n}\n\nmount_partitions() {\n  # Check A/B slot\n  SLOT=`grep_cmdline androidboot.slot_suffix`\n  if [ -z $SLOT ]; then\n    SLOT=`grep_cmdline androidboot.slot`\n    [ -z $SLOT ] || SLOT=_${SLOT}\n  fi\n  [ -z $SLOT ] || ui_print \"- Current boot slot: $SLOT\"\n\n  # Mount ro partitions\n  if is_mounted /system_root; then\n    umount /system 2&>/dev/null\n    umount /system_root 2&>/dev/null\n  fi\n  mount_ro_ensure \"system$SLOT app$SLOT\" /system\n  if [ -f /system/init -o -L /system/init ]; then\n    SYSTEM_ROOT=true\n    setup_mntpoint /system_root\n    if ! mount --move /system /system_root; then\n      umount /system\n      umount -l /system 2>/dev/null\n      mount_ro_ensure \"system$SLOT app$SLOT\" /system_root\n    fi\n    mount -o bind /system_root/system /system\n  else\n    SYSTEM_ROOT=false\n    grep ' / ' /proc/mounts | grep -qv 'rootfs' || grep -q ' /system_root ' /proc/mounts && SYSTEM_ROOT=true\n  fi\n  # /vendor is used only on some older devices for recovery AVBv1 signing so is not critical if fails\n  [ -L /system/vendor ] && mount_name vendor$SLOT /vendor '-o ro'\n  $SYSTEM_ROOT && ui_print \"- Device is system-as-root\"\n\n  # Mount sepolicy rules dir locations in recovery (best effort)\n  if ! $BOOTMODE; then\n    mount_name \"cache cac\" /cache\n    mount_name metadata /metadata\n    mount_name persist /persist\n  fi\n}\n\napi_level_arch_detect() {\n  API=$(grep_get_prop ro.build.version.sdk)\n  ABI=$(grep_get_prop ro.product.cpu.abi)\n  if [ \"$ABI\" = \"x86\" ]; then\n    ARCH=x86\n    ABI32=x86\n    IS64BIT=false\n  elif [ \"$ABI\" = \"arm64-v8a\" ]; then\n    ARCH=arm64\n    ABI32=armeabi-v7a\n    IS64BIT=true\n  elif [ \"$ABI\" = \"x86_64\" ]; then\n    ARCH=x64\n    ABI32=x86\n    IS64BIT=true\n  else\n    ARCH=arm\n    ABI=armeabi-v7a\n    ABI32=armeabi-v7a\n    IS64BIT=false\n  fi\n}\n\n#################\n# Module Related\n#################\n\ncheck_managed_features() {\n  local PROP_FILE=$1\n  local MANAGED_FEATURES=$(grep_prop managedFeatures \"$PROP_FILE\")\n\n  [ -z \"$MANAGED_FEATURES\" ] && return 0\n\n  ui_print \"- Checking managed features: $MANAGED_FEATURES\"\n\n  # Split features by comma\n  echo \"$MANAGED_FEATURES\" | tr ',' '\\n' | while read -r feature; do\n    # Trim whitespace\n    feature=$(echo \"$feature\" | xargs)\n    [ -z \"$feature\" ] && continue\n\n    # Check feature status using ksud\n    local status=$(/data/adb/ksud feature check \"$feature\" 2>/dev/null)\n\n    case \"$status\" in\n      \"unsupported\")\n        ui_print \"! WARNING: Feature '$feature' is NOT SUPPORTED by kernel\"\n        ui_print \"!          This module may not work correctly!\"\n        ;;\n      \"managed\")\n        ui_print \"! WARNING: Feature '$feature' is already MANAGED by another module\"\n        ui_print \"!          Feature conflicts may occur!\"\n        ;;\n      \"supported\")\n        ui_print \"- Feature '$feature' is supported and available\"\n        ;;\n      *)\n        ui_print \"! WARNING: Unable to check feature '$feature' status\"\n        ;;\n    esac\n  done\n}\n\nset_perm() {\n  chown $2:$3 $1 || return 1\n  chmod $4 $1 || return 1\n  local CON=$5\n  [ -z $CON ] && CON=u:object_r:system_file:s0\n  chcon $CON $1 || return 1\n}\n\nset_perm_recursive() {\n  find $1 -type d 2>/dev/null | while read dir; do\n    set_perm $dir $2 $3 $4 $6\n  done\n  find $1 -type f -o -type l 2>/dev/null | while read file; do\n    set_perm $file $2 $3 $5 $6\n  done\n}\n\nmktouch() {\n  mkdir -p ${1%/*} 2>/dev/null\n  [ -z $2 ] && touch $1 || echo $2 > $1\n  chmod 644 $1\n}\n\nmark_remove() {\n  mkdir -p ${1%/*} 2>/dev/null\n  mknod $1 c 0 0\n  chmod 644 $1\n}\n\nrequest_size_check() {\n  reqSizeM=`du -ms \"$1\" | cut -f1`\n}\n\nrequest_zip_size_check() {\n  reqSizeM=`unzip -l \"$1\" | tail -n 1 | awk '{ print int(($1 - 1) / 1048576 + 1) }'`\n}\n\nboot_actions() { return; }\n\n# Require ZIPFILE to be set\nis_legacy_script() {\n  unzip -l \"$ZIPFILE\" install.sh | grep -q install.sh\n  return $?\n}\n\nhandle_partition() {\n    # if /system/vendor is a symlink, we need to move it out of $MODPATH/system\n    # if /system/vendor is a normal directory, no special handling is needed.\n    if [ ! -e $MODPATH/system/$1 ]; then\n        # no partition found\n        return;\n    fi\n\n    # we move the folder to / only if it is a native folder that is not a symlink\n    if [ -d \"/$1\" ] && [ ! -L \"/$1\" ]; then\n        ui_print \"- Handle partition /$1\"\n        # we create a symlink if module want to access $MODPATH/system/$1\n        # but it doesn't always work(ie. write it in post-fs-data.sh would fail because it is readonly)\n        mv -f $MODPATH/system/$1 $MODPATH/$1 && ln -sf ../$1 $MODPATH/system/$1\n    fi\n}\n\n# Require OUTFD, ZIPFILE to be set\ninstall_module() {\n  rm -rf $TMPDIR\n  mkdir -p $TMPDIR\n  chcon u:object_r:system_file:s0 $TMPDIR\n  cd $TMPDIR\n\n  mount_partitions\n  api_level_arch_detect\n\n  # Setup busybox and binaries\n  if $BOOTMODE; then\n    boot_actions\n  else\n    recovery_actions\n  fi\n\n  # Extract prop file\n  unzip -o \"$ZIPFILE\" module.prop -d $TMPDIR >&2\n  [ ! -f $TMPDIR/module.prop ] && abort \"! Unable to extract zip file!\"\n\n  local MODDIRNAME=modules\n  $BOOTMODE && MODDIRNAME=modules_update\n  local MODULEROOT=$NVBASE/$MODDIRNAME\n  MODID=`grep_prop id $TMPDIR/module.prop`\n  MODNAME=`grep_prop name $TMPDIR/module.prop`\n  MODAUTH=`grep_prop author $TMPDIR/module.prop`\n  MODPATH=$MODULEROOT/$MODID\n\n  # Check managed features\n  check_managed_features $TMPDIR/module.prop\n\n  # Create mod paths\n  rm -rf $MODPATH\n  mkdir -p $MODPATH\n\n  if is_legacy_script; then\n    unzip -oj \"$ZIPFILE\" module.prop install.sh uninstall.sh 'common/*' -d $TMPDIR >&2\n\n    # Load install script\n    . $TMPDIR/install.sh\n\n    # Callbacks\n    print_modname\n    on_install\n\n    [ -f $TMPDIR/uninstall.sh ] && cp -af $TMPDIR/uninstall.sh $MODPATH/uninstall.sh\n    $SKIPMOUNT && touch $MODPATH/skip_mount\n    $PROPFILE && cp -af $TMPDIR/system.prop $MODPATH/system.prop\n    cp -af $TMPDIR/module.prop $MODPATH/module.prop\n    $POSTFSDATA && cp -af $TMPDIR/post-fs-data.sh $MODPATH/post-fs-data.sh\n    $LATESTARTSERVICE && cp -af $TMPDIR/service.sh $MODPATH/service.sh\n\n    ui_print \"- Setting permissions\"\n    set_permissions\n  else\n    print_title \"$MODNAME\" \"by $MODAUTH\"\n    print_title \"Powered by KernelSU\"\n\n    unzip -o \"$ZIPFILE\" customize.sh -d $MODPATH >&2\n\n    if ! grep -q '^SKIPUNZIP=1$' $MODPATH/customize.sh 2>/dev/null; then\n      ui_print \"- Extracting module files\"\n      unzip -o \"$ZIPFILE\" -x 'META-INF/*' -d $MODPATH >&2\n\n      # Default permissions\n      set_perm_recursive $MODPATH 0 0 0755 0644\n      set_perm_recursive $MODPATH/system/bin 0 2000 0755 0755\n      set_perm_recursive $MODPATH/system/xbin 0 2000 0755 0755\n      set_perm_recursive $MODPATH/system/system_ext/bin 0 2000 0755 0755\n      set_perm_recursive $MODPATH/system/vendor 0 2000 0755 0755 u:object_r:vendor_file:s0\n    fi\n\n    # Load customization script\n    [ -f $MODPATH/customize.sh ] && . $MODPATH/customize.sh\n  fi\n\n  # Handle replace folders\n  for TARGET in $REPLACE; do\n    ui_print \"- Replace target: $TARGET\"\n    mark_replace $MODPATH$TARGET\n  done\n\n  # Handle remove files\n  for TARGET in $REMOVE; do\n    ui_print \"- Remove target: $TARGET\"\n    mark_remove $MODPATH$TARGET\n  done\n\n  handle_partition vendor\n  handle_partition system_ext\n  handle_partition product\n  handle_partition odm\n\n  if $BOOTMODE; then\n    mktouch $NVBASE/modules/$MODID/update\n    rm -rf $NVBASE/modules/$MODID/remove 2>/dev/null\n    rm -rf $NVBASE/modules/$MODID/disable 2>/dev/null\n    cp -af $MODPATH/module.prop $NVBASE/modules/$MODID/module.prop\n  fi\n\n  # Remove stuff that doesn't belong to modules and clean up any empty directories\n  rm -rf \\\n  $MODPATH/system/placeholder $MODPATH/customize.sh \\\n  $MODPATH/README.md $MODPATH/.git*\n  rmdir -p $MODPATH 2>/dev/null\n\n  cd /\n  $BOOTMODE || recovery_cleanup\n  rm -rf $TMPDIR\n\n  ui_print \"- Done\"\n}\n\n##########\n# Presets\n##########\n\n# Detect whether in boot mode\n[ -z $BOOTMODE ] && ps | grep zygote | grep -qv grep && BOOTMODE=true\n[ -z $BOOTMODE ] && ps -A 2>/dev/null | grep zygote | grep -qv grep && BOOTMODE=true\n[ -z $BOOTMODE ] && BOOTMODE=false\n\nNVBASE=/data/adb\nTMPDIR=/dev/tmp\nPOSTFSDATAD=$NVBASE/post-fs-data.d\nSERVICED=$NVBASE/service.d\n\n# Some modules dependents on this\nexport MAGISK_VER=25.2\nexport MAGISK_VER_CODE=25200\n"
  },
  {
    "path": "userspace/ksud/src/ksucalls.rs",
    "content": "#![allow(clippy::unreadable_literal)]\nuse libc::{_IO, _IOR, _IOW, _IOWR};\nuse std::fs;\nuse std::os::fd::RawFd;\nuse std::sync::OnceLock;\n\n// Event constants\nconst EVENT_POST_FS_DATA: u32 = 1;\nconst EVENT_BOOT_COMPLETED: u32 = 2;\nconst EVENT_MODULE_MOUNTED: u32 = 3;\n\nconst K: u32 = b'K' as u32;\nconst KSU_IOCTL_GRANT_ROOT: i32 = _IO(K, 1);\nconst KSU_IOCTL_GET_INFO: i32 = _IOR::<()>(K, 2);\nconst KSU_IOCTL_REPORT_EVENT: i32 = _IOW::<()>(K, 3);\nconst KSU_IOCTL_SET_SEPOLICY: i32 = _IOWR::<()>(K, 4);\nconst KSU_IOCTL_CHECK_SAFEMODE: i32 = _IOR::<()>(K, 5);\nconst KSU_IOCTL_GET_FEATURE: i32 = _IOWR::<()>(K, 13);\nconst KSU_IOCTL_SET_FEATURE: i32 = _IOW::<()>(K, 14);\nconst KSU_IOCTL_GET_WRAPPER_FD: i32 = _IOW::<()>(K, 15);\nconst KSU_IOCTL_MANAGE_MARK: i32 = _IOWR::<()>(K, 16);\nconst KSU_IOCTL_NUKE_EXT4_SYSFS: i32 = _IOW::<()>(K, 17);\nconst KSU_IOCTL_ADD_TRY_UMOUNT: i32 = _IOW::<()>(K, 18);\n\n// Keep in sync with kernel/supercalls.h.\nconst KSU_GET_INFO_FLAG_LATE_LOAD: u32 = 1 << 2;\n\n#[repr(C)]\n#[derive(Clone, Copy, Default)]\nstruct GetInfoCmd {\n    version: u32,\n    flags: u32,\n    features: u32,\n}\n\n#[repr(C)]\nstruct ReportEventCmd {\n    event: u32,\n}\n\n#[repr(C)]\n#[derive(Clone, Copy, Default)]\npub struct SetSepolicyCmd {\n    pub data_len: u64,\n    pub data: u64,\n}\n\n#[repr(C)]\n#[derive(Clone, Copy, Default)]\nstruct CheckSafemodeCmd {\n    in_safe_mode: u8,\n}\n\n#[repr(C)]\n#[derive(Clone, Copy, Default)]\nstruct GetFeatureCmd {\n    feature_id: u32,\n    value: u64,\n    supported: u8,\n}\n\n#[repr(C)]\n#[derive(Clone, Copy, Default)]\nstruct SetFeatureCmd {\n    feature_id: u32,\n    value: u64,\n}\n\n#[repr(C)]\n#[derive(Clone, Copy, Default)]\nstruct GetWrapperFdCmd {\n    fd: i32,\n    flags: u32,\n}\n\n#[repr(C)]\n#[derive(Clone, Copy, Default)]\nstruct ManageMarkCmd {\n    operation: u32,\n    pid: i32,\n    result: u32,\n}\n\n#[repr(C)]\n#[derive(Clone, Copy, Default)]\npub struct NukeExt4SysfsCmd {\n    pub arg: u64,\n}\n\n#[repr(C)]\n#[derive(Clone, Copy, Default)]\nstruct AddTryUmountCmd {\n    arg: u64,   // char ptr, this is the mountpoint\n    flags: u32, // this is the flag we use for it\n    mode: u8,   // denotes what to do with it 0:wipe_list 1:add_to_list 2:delete_entry\n}\n\n// Mark operation constants\nconst KSU_MARK_GET: u32 = 1;\nconst KSU_MARK_MARK: u32 = 2;\nconst KSU_MARK_UNMARK: u32 = 3;\nconst KSU_MARK_REFRESH: u32 = 4;\n\n// Umount operation constants\nconst KSU_UMOUNT_WIPE: u8 = 0;\nconst KSU_UMOUNT_ADD: u8 = 1;\nconst KSU_UMOUNT_DEL: u8 = 2;\n\n// Global driver fd cache\nstatic DRIVER_FD: OnceLock<RawFd> = OnceLock::new();\nstatic INFO_CACHE: OnceLock<GetInfoCmd> = OnceLock::new();\n\nconst KSU_INSTALL_MAGIC1: u32 = 0xDEADBEEF;\nconst KSU_INSTALL_MAGIC2: u32 = 0xCAFEBABE;\n\nfn scan_driver_fd() -> Option<RawFd> {\n    let fd_dir = fs::read_dir(\"/proc/self/fd\").ok()?;\n\n    for entry in fd_dir.flatten() {\n        if let Ok(fd_num) = entry.file_name().to_string_lossy().parse::<i32>() {\n            let link_path = format!(\"/proc/self/fd/{fd_num}\");\n            if let Ok(target) = fs::read_link(&link_path) {\n                let target_str = target.to_string_lossy();\n                if target_str.contains(\"[ksu_driver]\") {\n                    return Some(fd_num);\n                }\n            }\n        }\n    }\n\n    None\n}\n\n// Get cached driver fd\nfn init_driver_fd() -> Option<RawFd> {\n    let fd = scan_driver_fd();\n    if fd.is_none() {\n        let mut fd = -1;\n        unsafe {\n            libc::syscall(\n                libc::SYS_reboot,\n                KSU_INSTALL_MAGIC1,\n                KSU_INSTALL_MAGIC2,\n                0,\n                &mut fd,\n            );\n        };\n        if fd >= 0 { Some(fd) } else { None }\n    } else {\n        fd\n    }\n}\n\n// ioctl wrapper using libc\nfn ksuctl<T>(request: i32, arg: *mut T) -> std::io::Result<i32> {\n    use std::io;\n\n    let fd = *DRIVER_FD.get_or_init(|| init_driver_fd().unwrap_or(-1));\n    unsafe {\n        let ret = libc::ioctl(fd as libc::c_int, request, arg);\n        if ret < 0 {\n            Err(io::Error::last_os_error())\n        } else {\n            Ok(ret)\n        }\n    }\n}\n\n// API implementations\nfn get_info() -> GetInfoCmd {\n    *INFO_CACHE.get_or_init(|| {\n        let mut cmd = GetInfoCmd {\n            version: 0,\n            flags: 0,\n            features: 0,\n        };\n        let _ = ksuctl(KSU_IOCTL_GET_INFO, &raw mut cmd);\n        cmd\n    })\n}\n\npub fn get_version() -> i32 {\n    get_info().version as i32\n}\n\npub fn is_late_load() -> bool {\n    get_info().flags & KSU_GET_INFO_FLAG_LATE_LOAD != 0\n}\n\npub fn grant_root() -> std::io::Result<()> {\n    ksuctl(KSU_IOCTL_GRANT_ROOT, std::ptr::null_mut::<u8>())?;\n    Ok(())\n}\n\nfn report_event(event: u32) {\n    let mut cmd = ReportEventCmd { event };\n    let _ = ksuctl(KSU_IOCTL_REPORT_EVENT, &raw mut cmd);\n}\n\npub fn report_post_fs_data() {\n    report_event(EVENT_POST_FS_DATA);\n}\n\npub fn report_boot_complete() {\n    report_event(EVENT_BOOT_COMPLETED);\n}\n\npub fn report_module_mounted() {\n    report_event(EVENT_MODULE_MOUNTED);\n}\n\npub fn check_kernel_safemode() -> bool {\n    let mut cmd = CheckSafemodeCmd { in_safe_mode: 0 };\n    let _ = ksuctl(KSU_IOCTL_CHECK_SAFEMODE, &raw mut cmd);\n    cmd.in_safe_mode != 0\n}\n\npub fn set_sepolicy(cmd: &SetSepolicyCmd) -> std::io::Result<i32> {\n    let mut ioctl_cmd = *cmd;\n    ksuctl(KSU_IOCTL_SET_SEPOLICY, &raw mut ioctl_cmd)\n}\n\n/// Get feature value and support status from kernel\n/// Returns (value, supported)\npub fn get_feature(feature_id: u32) -> std::io::Result<(u64, bool)> {\n    let mut cmd = GetFeatureCmd {\n        feature_id,\n        value: 0,\n        supported: 0,\n    };\n    ksuctl(KSU_IOCTL_GET_FEATURE, &raw mut cmd)?;\n    Ok((cmd.value, cmd.supported != 0))\n}\n\n/// Set feature value in kernel\npub fn set_feature(feature_id: u32, value: u64) -> std::io::Result<()> {\n    let mut cmd = SetFeatureCmd { feature_id, value };\n    ksuctl(KSU_IOCTL_SET_FEATURE, &raw mut cmd)?;\n    Ok(())\n}\n\npub fn get_wrapped_fd(fd: RawFd) -> std::io::Result<RawFd> {\n    let mut cmd = GetWrapperFdCmd { fd, flags: 0 };\n    let result = ksuctl(KSU_IOCTL_GET_WRAPPER_FD, &raw mut cmd)?;\n    Ok(result)\n}\n\n/// Get mark status for a process (pid=0 returns total marked count)\npub fn mark_get(pid: i32) -> std::io::Result<u32> {\n    let mut cmd = ManageMarkCmd {\n        operation: KSU_MARK_GET,\n        pid,\n        result: 0,\n    };\n    ksuctl(KSU_IOCTL_MANAGE_MARK, &raw mut cmd)?;\n    Ok(cmd.result)\n}\n\n/// Mark a process (pid=0 marks all processes)\npub fn mark_set(pid: i32) -> std::io::Result<()> {\n    let mut cmd = ManageMarkCmd {\n        operation: KSU_MARK_MARK,\n        pid,\n        result: 0,\n    };\n    ksuctl(KSU_IOCTL_MANAGE_MARK, &raw mut cmd)?;\n    Ok(())\n}\n\n/// Unmark a process (pid=0 unmarks all processes)\npub fn mark_unset(pid: i32) -> std::io::Result<()> {\n    let mut cmd = ManageMarkCmd {\n        operation: KSU_MARK_UNMARK,\n        pid,\n        result: 0,\n    };\n    ksuctl(KSU_IOCTL_MANAGE_MARK, &raw mut cmd)?;\n    Ok(())\n}\n\n/// Refresh mark for all running processes\npub fn mark_refresh() -> std::io::Result<()> {\n    let mut cmd = ManageMarkCmd {\n        operation: KSU_MARK_REFRESH,\n        pid: 0,\n        result: 0,\n    };\n    ksuctl(KSU_IOCTL_MANAGE_MARK, &raw mut cmd)?;\n    Ok(())\n}\n\npub fn nuke_ext4_sysfs(mnt: &str) -> anyhow::Result<()> {\n    let c_mnt = std::ffi::CString::new(mnt)?;\n    let mut ioctl_cmd = NukeExt4SysfsCmd {\n        arg: c_mnt.as_ptr() as u64,\n    };\n    ksuctl(KSU_IOCTL_NUKE_EXT4_SYSFS, &raw mut ioctl_cmd)?;\n    Ok(())\n}\n\n/// Wipe all entries from umount list\npub fn umount_list_wipe() -> std::io::Result<()> {\n    let mut cmd = AddTryUmountCmd {\n        arg: 0,\n        flags: 0,\n        mode: KSU_UMOUNT_WIPE,\n    };\n    ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &raw mut cmd)?;\n    Ok(())\n}\n\n/// Add mount point to umount list\npub fn umount_list_add(path: &str, flags: u32) -> anyhow::Result<()> {\n    let c_path = std::ffi::CString::new(path)?;\n    let mut cmd = AddTryUmountCmd {\n        arg: c_path.as_ptr() as u64,\n        flags,\n        mode: KSU_UMOUNT_ADD,\n    };\n    ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &raw mut cmd)?;\n    Ok(())\n}\n\n/// Delete mount point from umount list\npub fn umount_list_del(path: &str) -> anyhow::Result<()> {\n    let c_path = std::ffi::CString::new(path)?;\n    let mut cmd = AddTryUmountCmd {\n        arg: c_path.as_ptr() as u64,\n        flags: 0,\n        mode: KSU_UMOUNT_DEL,\n    };\n    ksuctl(KSU_IOCTL_ADD_TRY_UMOUNT, &raw mut cmd)?;\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/late_load.rs",
    "content": "use anyhow::{Context, Result};\nuse log::{info, warn};\nuse std::process::Command;\n\nuse crate::module::{handle_updated_modules, prune_modules};\nuse crate::{assets, defs, init_event, metamodule, restorecon, utils};\n\nfn dump_process_info(label: &str) {\n    use rustix::process::{getgid, getgroups, getpid, getuid};\n\n    let pid = getpid().as_raw_nonzero();\n    let uid = getuid().as_raw();\n    let gid = getgid().as_raw();\n    let groups: Vec<String> = getgroups()\n        .unwrap_or_default()\n        .iter()\n        .map(|g| g.as_raw().to_string())\n        .collect();\n    let selinux = std::fs::read_to_string(\"/proc/self/attr/current\")\n        .unwrap_or_else(|_| \"unknown\".to_string());\n    let seccomp = std::fs::read_to_string(\"/proc/self/status\")\n        .ok()\n        .and_then(|s| {\n            s.lines()\n                .find(|l| l.starts_with(\"Seccomp:\"))\n                .map(|l| l.trim().to_string())\n        })\n        .unwrap_or_else(|| \"unknown\".to_string());\n\n    info!(\n        \"[{label}] pid={pid}, uid={uid}, gid={gid}, groups=[{}], selinux={}, {seccomp}\",\n        groups.join(\",\"),\n        selinux.trim(),\n    );\n}\n\npub fn run() -> Result<()> {\n    info!(\"late-load command triggered!\");\n    dump_process_info(\"late-load start\");\n\n    // 1. Check if KernelSU is already loaded\n    if ksuinit::has_kernelsu() {\n        info!(\"KernelSU already loaded, skip loading ko\");\n    } else {\n        // 2. Detect current KMI version\n        let kmi =\n            crate::boot_patch::get_current_kmi().context(\"Failed to detect current KMI version\")?;\n        info!(\"Detected KMI: {kmi}\");\n\n        // 3. Get kernelsu.ko from embedded assets\n        let ko_name = format!(\"{kmi}_kernelsu.ko\");\n        let ko_data = assets::get_asset_data(&ko_name)\n            .with_context(|| format!(\"Failed to get {ko_name} from assets\"))?;\n\n        // 4. Load kernelsu.ko from memory with manual relocation\n        info!(\"Loading kernelsu.ko for KMI {kmi}...\");\n        ksuinit::load_module(&ko_data).context(\"Failed to load kernelsu.ko\")?;\n        info!(\"kernelsu.ko loaded successfully!\");\n        dump_process_info(\"after load_module\");\n    }\n\n    utils::umask(0);\n\n    if let Err(e) = crate::module_config::clear_all_temp_configs() {\n        warn!(\"clear temp configs failed: {e}\");\n    }\n\n    utils::install(None).context(\"Failed to install ksud\")?;\n\n    // 5. Handle module updates\n    if let Err(e) = handle_updated_modules() {\n        warn!(\"handle updated modules failed: {e}\");\n    }\n\n    if let Err(e) = prune_modules() {\n        warn!(\"prune modules failed: {e}\");\n    }\n\n    if let Err(e) = restorecon::restorecon() {\n        warn!(\"restorecon failed: {e}\");\n    }\n\n    // 6. Load SELinux rules\n    if crate::module::load_sepolicy_rule().is_err() {\n        warn!(\"load sepolicy.rule failed\");\n    }\n\n    if let Err(e) = crate::profile::apply_sepolies() {\n        warn!(\"apply root profile sepolicy failed: {e}\");\n    }\n\n    // 7. Initialize features\n    if let Err(e) = crate::feature::init_features() {\n        warn!(\"init features failed: {e}\");\n    }\n\n    // 8. Execute late-load stage scripts (blocking)\n    init_event::run_stage(\"late-load\", true);\n\n    // 9. Load system.prop\n    if let Err(e) = crate::module::load_system_prop() {\n        warn!(\"load system.prop failed: {e}\");\n    }\n\n    // 10. Execute metamodule mount script (OverlayFS)\n    if let Err(e) = metamodule::exec_mount_script(defs::MODULE_DIR) {\n        warn!(\"execute metamodule mount failed: {e}\");\n    }\n\n    // 11. Execute post-mount stage scripts (blocking)\n    init_event::run_stage(\"post-mount\", true);\n\n    // 12. Execute service stage scripts (non-blocking)\n    init_event::run_stage(\"service\", false);\n\n    // 13. Execute boot-completed stage scripts (non-blocking)\n    init_event::run_stage(\"boot-completed\", false);\n\n    // 14. Restart Manager so it gets a fresh ksu fd from the newly loaded kernel module\n    info!(\"Restarting KernelSU Manager...\");\n    let pkg = \"me.weishu.kernelsu\";\n    let _ = Command::new(\"am\").args([\"force-stop\", pkg]).status();\n    let _ = Command::new(\"am\")\n        .args([\"start\", \"-n\", &format!(\"{pkg}/.ui.MainActivity\")])\n        .status();\n\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/magica.rs",
    "content": "use adb_client::ADBDeviceExt;\nuse adb_client::tcp::ADBTcpDevice;\nuse anyhow::{Context, Result, bail};\nuse log::{error, info};\nuse prop_rs_android::resetprop::ResetProp;\nuse prop_rs_android::sys_prop;\nuse std::net::{IpAddr, Ipv4Addr, SocketAddr};\nuse std::process::Command;\n\nconst fn resetprop() -> ResetProp {\n    ResetProp {\n        skip_svc: true,\n        persistent: false,\n        persist_only: false,\n        verbose: false,\n        show_context: false,\n    }\n}\n\nfn exec_shell_commands(commands: &[(&str, &[&str])], log_prefix: &str) -> Result<()> {\n    for (cmd, args) in commands {\n        info!(\"{log_prefix}: {cmd} {}\", args.join(\" \"));\n        let status = Command::new(cmd)\n            .args(*args)\n            .status()\n            .with_context(|| format!(\"Failed to execute {cmd}\"))?;\n        if !status.success() {\n            bail!(\"{cmd} {} exited with {status}\", args.join(\" \"));\n        }\n    }\n    Ok(())\n}\n\nfn enable_adb_root(port: u16) -> Result<()> {\n    // We are in limited root by magica\n    anyhow::ensure!(\n        rustix::process::getuid().as_raw() == 0,\n        \"must be run as root\"\n    );\n\n    sys_prop::init().context(\"Failed to initialize system property API\")?;\n    let rp = resetprop();\n\n    let debuggable_context = sys_prop::get_context(\"ro.debuggable\")\n        .context(\"Failed to get context for ro.debuggable\")?;\n    info!(\"ro.debuggable context: {debuggable_context}\");\n\n    let adb_secure_context = sys_prop::get_context(\"ro.adb.secure\")\n        .context(\"Failed to get context for ro.adb.secure\")?;\n    info!(\"ro.adb.secure context: {adb_secure_context}\");\n\n    let props_serial = \"/dev/__properties__/properties_serial\";\n    let debuggable_context = format!(\"/dev/__properties__/{debuggable_context}\");\n    let adb_secure_context = format!(\"/dev/__properties__/{adb_secure_context}\");\n    let port_str = port.to_string();\n\n    // chmod property files to writable\n    exec_shell_commands(\n        &[\n            (\"chmod\", &[\"0644\", props_serial]),\n            (\"chmod\", &[\"0644\", &debuggable_context]),\n            (\"chmod\", &[\"0644\", &adb_secure_context]),\n        ],\n        \"Executing\",\n    )?;\n\n    // Set properties via internal API\n    rp.set(\"ro.debuggable\", \"1\")\n        .context(\"Failed to set ro.debuggable\")?;\n    info!(\"Executing: resetprop -n ro.debuggable 1\");\n    rp.set(\"ro.adb.secure\", \"0\")\n        .context(\"Failed to set ro.adb.secure\")?;\n    info!(\"Executing: resetprop -n ro.adb.secure 0\");\n\n    // Restore permissions and restart adbd\n    exec_shell_commands(\n        &[\n            (\"chmod\", &[\"0444\", props_serial]),\n            (\"chmod\", &[\"0444\", &debuggable_context]),\n            (\"chmod\", &[\"0444\", &adb_secure_context]),\n            (\"setprop\", &[\"service.adb.root\", \"1\"]),\n            (\"setprop\", &[\"service.adb.tcp.port\", &port_str]),\n            (\"setprop\", &[\"ctl.restart\", \"adbd\"]),\n        ],\n        \"Executing\",\n    )?;\n\n    Ok(())\n}\n\npub fn disable_adb_root() -> Result<()> {\n    // We have full root now, no need to chmod\n    sys_prop::init().context(\"Failed to initialize system property API\")?;\n    let rp = resetprop();\n\n    info!(\"Restoring: resetprop -n ro.debuggable 0\");\n    rp.set(\"ro.debuggable\", \"0\")\n        .context(\"Failed to set ro.debuggable\")?;\n\n    info!(\"Restoring: resetprop -n ro.adb.secure 1\");\n    rp.set(\"ro.adb.secure\", \"1\")\n        .context(\"Failed to set ro.adb.secure\")?;\n\n    for prop in &[\"service.adb.root\", \"service.adb.tcp.port\"] {\n        info!(\"Restoring: resetprop --delete {prop}\");\n        let _ = rp.delete(prop);\n        if let Ok(ctx) = sys_prop::get_context(prop) {\n            let _ = sys_prop::compact(Some(&ctx));\n        }\n    }\n\n    exec_shell_commands(&[(\"setprop\", &[\"ctl.restart\", \"adbd\"])], \"Restoring\")?;\n\n    Ok(())\n}\n\nfn connect_to_device(port: u16) -> Result<ADBTcpDevice> {\n    const MAX_RETRIES: u32 = 30;\n    for attempt in 1..=MAX_RETRIES {\n        info!(\"Waiting for adbd to restart... (attempt {attempt}/{MAX_RETRIES})\");\n        std::thread::sleep(std::time::Duration::from_secs(1));\n\n        let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port);\n        info!(\"Connecting to ADB device at {addr}\");\n        match ADBTcpDevice::new(addr).context(\"Failed to create ADBTcpDevice\") {\n            Ok(device) => return Ok(device),\n            Err(e) => {\n                error!(\"Failed to connect to ADB device: {e:?}, retry after 1s\");\n            }\n        }\n    }\n    bail!(\"Failed to connect to ADB device after {MAX_RETRIES} attempts\")\n}\n\npub fn run(port: u16) -> Result<()> {\n    enable_adb_root(port)?;\n\n    let mut device = connect_to_device(port)?;\n\n    let self_path = std::env::current_exe().context(\"Failed to get self exe path\")?;\n\n    // Execute late-load with --post-magica via adb shell.\n    // The late-load process has full root + su domain and will:\n    // 1. Load kernelsu.ko, enforce SELinux, run stage scripts\n    // 2. Restore adb properties (disable adb root/tcp mode)\n    let cmd = format!(\"{} late-load --post-magica\", self_path.display());\n    info!(\"Executing '{cmd}' via adb shell...\");\n    let mut stdout = Vec::new();\n    let mut stderr = Vec::new();\n    if let Err(e) = device.shell_command(&cmd, Some(&mut stdout), Some(&mut stderr)) {\n        info!(\"adb shell finished with error (may be expected): {e}\");\n    }\n    if !stdout.is_empty() {\n        info!(\"stdout: {}\", String::from_utf8_lossy(&stdout));\n    }\n    if !stderr.is_empty() {\n        info!(\"stderr: {}\", String::from_utf8_lossy(&stderr));\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/main.rs",
    "content": "#![deny(clippy::all, clippy::pedantic)]\n#![warn(clippy::nursery)]\n#![allow(\n    clippy::module_name_repetitions,\n    clippy::cast_possible_truncation,\n    clippy::cast_sign_loss,\n    clippy::cast_precision_loss,\n    clippy::doc_markdown,\n    clippy::too_many_lines,\n    clippy::cast_possible_wrap\n)]\n\nmod apk_sign;\nmod assets;\nmod boot_patch;\n#[cfg(target_os = \"android\")]\nmod cli;\n#[cfg(not(target_os = \"android\"))]\nmod cli_non_android;\n#[cfg(target_os = \"android\")]\nmod debug;\nmod defs;\n#[cfg(target_os = \"android\")]\nmod feature;\n#[cfg(target_os = \"android\")]\nmod init_event;\n#[cfg(target_os = \"android\")]\nmod ksucalls;\n#[cfg(target_os = \"android\")]\nmod late_load;\n#[cfg(target_os = \"android\")]\nmod magica;\n#[cfg(target_os = \"android\")]\nmod metamodule;\n#[cfg(target_os = \"android\")]\nmod module;\n#[cfg(target_os = \"android\")]\nmod module_config;\n#[cfg(target_os = \"android\")]\nmod profile;\n#[cfg(target_os = \"android\")]\nmod resetprop;\n#[cfg(target_os = \"android\")]\nmod restorecon;\n#[cfg(target_os = \"android\")]\nmod sepolicy;\n#[cfg(target_os = \"android\")]\nmod su;\n#[cfg(target_os = \"android\")]\nmod utils;\n\nfn main() -> anyhow::Result<()> {\n    #[cfg(target_os = \"android\")]\n    {\n        cli::run()\n    }\n    #[cfg(not(target_os = \"android\"))]\n    {\n        cli_non_android::run()\n    }\n}\n"
  },
  {
    "path": "userspace/ksud/src/metamodule.rs",
    "content": "//! Metamodule management\n//!\n//! This module handles all metamodule-related functionality.\n//! Metamodules are special modules that manage how regular modules are mounted\n//! and provide hooks for module installation/uninstallation.\n\nuse anyhow::{Context, Result, ensure};\nuse log::{info, warn};\nuse std::{\n    collections::HashMap,\n    path::{Path, PathBuf},\n    process::Command,\n};\n\nuse crate::module::ModuleType::All;\nuse crate::{assets, defs};\n\n/// Determine whether the provided module properties mark it as a metamodule\npub fn is_metamodule(props: &HashMap<String, String>) -> bool {\n    props.get(\"metamodule\").is_some_and(|s| {\n        let trimmed = s.trim();\n        trimmed == \"1\" || trimmed.eq_ignore_ascii_case(\"true\")\n    })\n}\n\n/// Get metamodule path if it exists\n/// The metamodule is stored in /data/adb/modules/{id} with a symlink at /data/adb/metamodule\npub fn get_metamodule_path() -> Option<PathBuf> {\n    let path = Path::new(defs::METAMODULE_DIR);\n\n    // Check if symlink exists and resolve it\n    if path.is_symlink()\n        && let Ok(target) = std::fs::read_link(path)\n    {\n        // If target is relative, resolve it\n        let resolved = if target.is_absolute() {\n            target\n        } else {\n            path.parent()?.join(target)\n        };\n\n        if resolved.exists() && resolved.is_dir() {\n            return Some(resolved);\n        }\n        warn!(\n            \"Metamodule symlink points to non-existent path: {}\",\n            resolved.display()\n        );\n    }\n\n    // Fallback: search for metamodule=1 in modules directory\n    let mut result = None;\n    let _ = crate::module::foreach_module(All, |module_path| {\n        if let Ok(props) = crate::module::read_module_prop(module_path)\n            && is_metamodule(&props)\n        {\n            info!(\n                \"Found metamodule in modules directory: {}\",\n                module_path.display()\n            );\n            result = Some(module_path.to_path_buf());\n        }\n        Ok(())\n    });\n\n    result\n}\n\n/// Check if metamodule exists\npub fn has_metamodule() -> bool {\n    get_metamodule_path().is_some()\n}\n\n/// Check if it's safe to install a regular module\n/// Returns Ok(()) if safe, Err(is_disabled) if blocked\n/// - Err(true) means metamodule is disabled\n/// - Err(false) means metamodule is in other unstable state\npub fn check_install_safety() -> Result<(), bool> {\n    // No metamodule → safe\n    let Some(metamodule_path) = get_metamodule_path() else {\n        return Ok(());\n    };\n\n    // No metainstall.sh → safe (uses default installer)\n    // The staged update directory may contain the latest scripts, so check both locations\n    let has_metainstall = metamodule_path\n        .join(defs::METAMODULE_METAINSTALL_SCRIPT)\n        .exists()\n        || metamodule_path.file_name().is_some_and(|module_id| {\n            Path::new(defs::MODULE_UPDATE_DIR)\n                .join(module_id)\n                .join(defs::METAMODULE_METAINSTALL_SCRIPT)\n                .exists()\n        });\n    if !has_metainstall {\n        return Ok(());\n    }\n\n    // Check for marker files\n    let has_update = metamodule_path.join(defs::UPDATE_FILE_NAME).exists();\n    let has_remove = metamodule_path.join(defs::REMOVE_FILE_NAME).exists();\n    let has_disable = metamodule_path.join(defs::DISABLE_FILE_NAME).exists();\n\n    // Stable state (no markers) → safe\n    if !has_update && !has_remove && !has_disable {\n        return Ok(());\n    }\n\n    // Return true if disabled, false for other unstable states\n    Err(has_disable && !has_update && !has_remove)\n}\n\n/// Create or update the metamodule symlink\n/// Points /data/adb/metamodule -> /data/adb/modules/{module_id}\npub fn ensure_symlink(module_path: &Path) -> Result<()> {\n    // METAMODULE_DIR might have trailing slash, so we need to trim it\n    let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/'));\n\n    info!(\n        \"Creating metamodule symlink: {} -> {}\",\n        symlink_path.display(),\n        module_path.display()\n    );\n\n    // Remove existing symlink if it exists\n    if symlink_path.exists() || symlink_path.is_symlink() {\n        info!(\"Removing old metamodule symlink/path\");\n        if symlink_path.is_symlink() {\n            std::fs::remove_file(symlink_path).with_context(|| \"Failed to remove old symlink\")?;\n        } else {\n            // Could be a directory, remove it\n            std::fs::remove_dir_all(symlink_path)\n                .with_context(|| \"Failed to remove old directory\")?;\n        }\n    }\n\n    // Create symlink\n    #[cfg(unix)]\n    std::os::unix::fs::symlink(module_path, symlink_path)\n        .with_context(|| format!(\"Failed to create symlink to {}\", module_path.display()))?;\n\n    info!(\"Metamodule symlink created successfully\");\n    Ok(())\n}\n\n/// Remove the metamodule symlink\npub fn remove_symlink() -> Result<()> {\n    let symlink_path = Path::new(defs::METAMODULE_DIR.trim_end_matches('/'));\n\n    if symlink_path.is_symlink() {\n        std::fs::remove_file(symlink_path)\n            .with_context(|| \"Failed to remove metamodule symlink\")?;\n        info!(\"Metamodule symlink removed\");\n    }\n\n    Ok(())\n}\n\n/// Get the install script content, using metainstall.sh from metamodule if available\n/// Returns the script content to be executed\npub fn get_install_script(\n    is_metamodule: bool,\n    installer_content: &str,\n    install_module_script: &str,\n) -> Result<String> {\n    // Check if there's a metamodule with metainstall.sh\n    // Only apply this logic for regular modules (not when installing metamodule itself)\n    let install_script = if is_metamodule {\n        info!(\"Installing metamodule, using default installer\");\n        install_module_script.to_string()\n    } else if let Some(metamodule_path) = get_metamodule_path() {\n        if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() {\n            info!(\"Metamodule is disabled, using default installer\");\n            install_module_script.to_string()\n        } else {\n            let metainstall_path = metamodule_path.join(defs::METAMODULE_METAINSTALL_SCRIPT);\n\n            if metainstall_path.exists() {\n                info!(\"Using metainstall.sh from metamodule\");\n                let metamodule_content = std::fs::read_to_string(&metainstall_path)\n                    .with_context(|| \"Failed to read metamodule metainstall.sh\")?;\n                format!(\"{installer_content}\\n{metamodule_content}\\nexit 0\\n\")\n            } else {\n                info!(\"Metamodule exists but has no metainstall.sh, using default installer\");\n                install_module_script.to_string()\n            }\n        }\n    } else {\n        info!(\"No metamodule found, using default installer\");\n        install_module_script.to_string()\n    };\n\n    Ok(install_script)\n}\n\n/// Check if metamodule script exists and is ready to execute\n/// Returns None if metamodule doesn't exist, is disabled, or script is missing\n/// Returns Some(script_path) if script is ready to execute\nfn check_metamodule_script(script_name: &str) -> Option<PathBuf> {\n    // Check if metamodule exists\n    let metamodule_path = get_metamodule_path()?;\n\n    // Check if metamodule is disabled\n    if metamodule_path.join(defs::DISABLE_FILE_NAME).exists() {\n        info!(\"Metamodule is disabled, skipping {script_name}\");\n        return None;\n    }\n\n    // Check if script exists\n    let script_path = metamodule_path.join(script_name);\n    if !script_path.exists() {\n        return None;\n    }\n\n    Some(script_path)\n}\n\n/// Execute metamodule's metauninstall.sh for a specific module\npub fn exec_metauninstall_script(module_id: &str) -> Result<()> {\n    let Some(metauninstall_path) = check_metamodule_script(defs::METAMODULE_METAUNINSTALL_SCRIPT)\n    else {\n        return Ok(());\n    };\n\n    info!(\"Executing metamodule metauninstall.sh for module: {module_id}\",);\n\n    let result = Command::new(assets::BUSYBOX_PATH)\n        .args([\"sh\", metauninstall_path.to_str().unwrap()])\n        .current_dir(metauninstall_path.parent().unwrap())\n        .envs(crate::module::get_common_script_envs())\n        .env(\"MODULE_ID\", module_id)\n        .status()?;\n\n    ensure!(\n        result.success(),\n        \"Metamodule metauninstall.sh failed for module {module_id}: {result:?}\"\n    );\n\n    info!(\"Metamodule metauninstall.sh executed successfully for {module_id}\",);\n    Ok(())\n}\n\n/// Execute metamodule mount script\npub fn exec_mount_script(module_dir: &str) -> Result<()> {\n    let Some(mount_script) = check_metamodule_script(defs::METAMODULE_MOUNT_SCRIPT) else {\n        return Ok(());\n    };\n\n    info!(\"Executing mount script for metamodule\");\n\n    let result = Command::new(assets::BUSYBOX_PATH)\n        .args([\"sh\", mount_script.to_str().unwrap()])\n        .envs(crate::module::get_common_script_envs())\n        .env(\"MODULE_DIR\", module_dir)\n        .status()?;\n\n    ensure!(\n        result.success(),\n        \"Metamodule mount script failed with status: {result:?}\"\n    );\n\n    info!(\"Metamodule mount script executed successfully\");\n    Ok(())\n}\n\n/// Execute metamodule script for a specific stage\npub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {\n    let Some(script_path) = check_metamodule_script(&format!(\"{stage}.sh\")) else {\n        return Ok(());\n    };\n\n    info!(\"Executing metamodule {stage}.sh\");\n    crate::module::exec_script(&script_path, block)?;\n    info!(\"Metamodule {stage}.sh executed successfully\");\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/module.rs",
    "content": "#[allow(clippy::wildcard_imports)]\nuse crate::utils::*;\nuse crate::{\n    assets, defs, ksucalls, metamodule,\n    restorecon::{restore_syscon, setsyscon},\n    sepolicy,\n};\n\nuse anyhow::{Context, Result, anyhow, bail, ensure};\nuse const_format::concatcp;\nuse is_executable::is_executable;\nuse java_properties::PropertiesIter;\nuse log::{debug, info, warn};\nuse regex_lite::Regex;\n\nuse std::fs::{copy, rename};\nuse std::{\n    collections::HashMap,\n    env::var as env_var,\n    fs::{File, Permissions, canonicalize, remove_dir_all, set_permissions},\n    io::Cursor,\n    path::{Path, PathBuf},\n    process::Command,\n    str::FromStr,\n};\nuse zip_extensions::inflate::zip_extract::zip_extract_file_to_memory;\n\nuse crate::defs::{MODULE_DIR, MODULE_UPDATE_DIR, UPDATE_FILE_NAME};\nuse crate::module::ModuleType::{Active, All};\n#[cfg(unix)]\nuse std::os::unix::{prelude::PermissionsExt, process::CommandExt};\n\nconst INSTALLER_CONTENT: &str = include_str!(\"./installer.sh\");\nconst INSTALL_MODULE_SCRIPT: &str = concatcp!(\n    INSTALLER_CONTENT,\n    \"\\n\",\n    \"install_module\",\n    \"\\n\",\n    \"exit 0\",\n    \"\\n\"\n);\n\n/// Validate module_id format and security\n/// Module ID must match: ^[a-zA-Z][a-zA-Z0-9._-]+$\n/// - Must start with a letter (a-zA-Z)\n/// - Followed by one or more alphanumeric, dot, underscore, or hyphen characters\n/// - Minimum length: 2 characters\npub fn validate_module_id(module_id: &str) -> Result<()> {\n    let re = Regex::new(r\"^[a-zA-Z][a-zA-Z0-9._-]+$\")?;\n    if re.is_match(module_id) {\n        Ok(())\n    } else {\n        Err(anyhow!(\n            \"Invalid module ID: '{module_id}'. Must match /^[a-zA-Z][a-zA-Z0-9._-]+$/\"\n        ))\n    }\n}\n\n/// Get common environment variables for script execution\npub fn get_common_script_envs() -> Vec<(&'static str, String)> {\n    let mut envs = vec![\n        (\"ASH_STANDALONE\", \"1\".to_string()),\n        (\"KSU\", \"true\".to_string()),\n        (\"KSU_KERNEL_VER_CODE\", ksucalls::get_version().to_string()),\n        (\"KSU_VER_CODE\", defs::VERSION_CODE.to_string()),\n        (\"KSU_VER\", defs::VERSION_NAME.to_string()),\n        (\n            \"PATH\",\n            format!(\n                \"{}:{}\",\n                env_var(\"PATH\").unwrap_or_default(),\n                defs::BINARY_DIR.trim_end_matches('/')\n            ),\n        ),\n    ];\n\n    if ksucalls::is_late_load() {\n        envs.push((\"KSU_LATE_LOAD\", \"1\".to_string()));\n    }\n\n    envs\n}\n\nfn exec_install_script(module_file: &str, is_metamodule: bool) -> Result<()> {\n    let realpath = std::fs::canonicalize(module_file)\n        .with_context(|| format!(\"realpath: {module_file} failed\"))?;\n\n    // Get install script from metamodule module\n    let install_script =\n        metamodule::get_install_script(is_metamodule, INSTALLER_CONTENT, INSTALL_MODULE_SCRIPT)?;\n\n    let result = Command::new(assets::BUSYBOX_PATH)\n        .args([\"sh\", \"-c\", &install_script])\n        .envs(get_common_script_envs())\n        .env(\"OUTFD\", \"1\")\n        .env(\"ZIPFILE\", realpath)\n        .status()?;\n    ensure!(result.success(), \"Failed to install module script\");\n    Ok(())\n}\n\n// Check if Android boot is completed before installing modules\nfn ensure_boot_completed() -> Result<()> {\n    // ensure getprop sys.boot_completed == 1\n    if getprop(\"sys.boot_completed\").as_deref() != Some(\"1\") {\n        bail!(\"Android is Booting!\");\n    }\n    Ok(())\n}\n\n#[derive(PartialEq, Eq)]\npub enum ModuleType {\n    All,\n    Active,\n    Updated,\n}\n\n#[allow(clippy::needless_pass_by_value)]\npub fn foreach_module(\n    module_type: ModuleType,\n    mut f: impl FnMut(&Path) -> Result<()>,\n) -> Result<()> {\n    let modules_dir = Path::new(match module_type {\n        ModuleType::Updated => MODULE_UPDATE_DIR,\n        _ => defs::MODULE_DIR,\n    });\n    let dir = std::fs::read_dir(modules_dir)?;\n    for entry in dir.flatten() {\n        let path = entry.path();\n        if !path.is_dir() {\n            warn!(\"{} is not a directory, skip\", path.display());\n            continue;\n        }\n\n        if module_type == Active && path.join(defs::DISABLE_FILE_NAME).exists() {\n            info!(\"{} is disabled, skip\", path.display());\n            continue;\n        }\n        if module_type == Active && path.join(defs::REMOVE_FILE_NAME).exists() {\n            warn!(\"{} is removed, skip\", path.display());\n            continue;\n        }\n\n        f(&path)?;\n    }\n\n    Ok(())\n}\n\nfn foreach_active_module(f: impl FnMut(&Path) -> Result<()>) -> Result<()> {\n    foreach_module(Active, f)\n}\n\npub fn load_sepolicy_rule() -> Result<()> {\n    foreach_active_module(|path| {\n        let rule_file = path.join(\"sepolicy.rule\");\n        if !rule_file.exists() {\n            return Ok(());\n        }\n        info!(\"load policy: {}\", &rule_file.display());\n\n        if sepolicy::apply_file(&rule_file).is_err() {\n            warn!(\"Failed to load sepolicy.rule for {}\", &rule_file.display());\n        }\n        Ok(())\n    })?;\n\n    Ok(())\n}\n\npub fn exec_script<T: AsRef<Path>>(path: T, wait: bool) -> Result<()> {\n    info!(\"exec {}\", path.as_ref().display());\n\n    let is_module_script = path.as_ref().starts_with(defs::MODULE_DIR);\n    // Extract module_id from path if it matches /data/adb/modules/{id}/...\n    let module_id = if is_module_script {\n        path.as_ref()\n            .strip_prefix(defs::MODULE_DIR)\n            .ok()\n            .and_then(|p| p.components().next())\n            .and_then(|c| c.as_os_str().to_str())\n            .map(ToString::to_string)\n    } else {\n        None\n    };\n\n    // Validate and log module_id extraction\n    let validated_module_id = module_id\n        .as_ref()\n        .and_then(|id| match validate_module_id(id) {\n            Ok(()) => {\n                debug!(\"Module ID extracted from script path: '{id}'\");\n                Some(id.as_str())\n            }\n            Err(e) => {\n                warn!(\n                    \"Invalid module ID '{id}' extracted from script path '{}': {e}\",\n                    path.as_ref().display(),\n                );\n                None\n            }\n        });\n\n    if is_module_script && module_id.is_none() {\n        debug!(\n            \"Failed to extract module_id from script path '{}'. Script will run without KSU_MODULE environment variable.\",\n            path.as_ref().display()\n        );\n    }\n\n    let mut command = &mut Command::new(assets::BUSYBOX_PATH);\n    #[cfg(unix)]\n    {\n        command = command.process_group(0);\n        command = unsafe {\n            command.pre_exec(|| {\n                // ignore the error?\n                switch_cgroups();\n                Ok(())\n            })\n        };\n    }\n    command = command\n        .current_dir(path.as_ref().parent().unwrap())\n        .arg(\"sh\")\n        .arg(path.as_ref())\n        .envs(get_common_script_envs());\n\n    // Set KSU_MODULE environment variable if module_id was validated successfully\n    if let Some(id) = validated_module_id {\n        command = command.env(\"KSU_MODULE\", id);\n    }\n\n    let result = if wait {\n        command.status().map(|_| ())\n    } else {\n        command.spawn().map(|_| ())\n    };\n    result.map_err(|e| anyhow!(\"Failed to exec {}: {e}\", path.as_ref().display()))\n}\n\npub fn exec_stage_script(stage: &str, block: bool) -> Result<()> {\n    let metamodule_dir = metamodule::get_metamodule_path().and_then(|path| canonicalize(path).ok());\n\n    foreach_active_module(|module| {\n        if metamodule_dir.as_ref().is_some_and(|meta_dir| {\n            canonicalize(module)\n                .map(|resolved| resolved == *meta_dir)\n                .unwrap_or(false)\n        }) {\n            return Ok(());\n        }\n\n        let script_path = module.join(format!(\"{stage}.sh\"));\n        if !script_path.exists() {\n            return Ok(());\n        }\n\n        exec_script(&script_path, block)\n    })?;\n\n    Ok(())\n}\n\npub fn exec_common_scripts(dir: &str, wait: bool) -> Result<()> {\n    let script_dir = Path::new(defs::ADB_DIR).join(dir);\n    if !script_dir.exists() {\n        info!(\"{} not exists, skip\", script_dir.display());\n        return Ok(());\n    }\n\n    let dir = std::fs::read_dir(&script_dir)?;\n    for entry in dir.flatten() {\n        let path = entry.path();\n\n        if !is_executable(&path) {\n            warn!(\"{} is not executable, skip\", path.display());\n            continue;\n        }\n\n        exec_script(path, wait)?;\n    }\n\n    Ok(())\n}\n\npub fn load_system_prop() -> Result<()> {\n    foreach_active_module(|module| {\n        let system_prop = module.join(\"system.prop\");\n        if !system_prop.exists() {\n            return Ok(());\n        }\n        info!(\"load {} system.prop\", module.display());\n\n        crate::resetprop::load_system_prop_file(&system_prop)?;\n\n        Ok(())\n    })?;\n\n    Ok(())\n}\n\npub fn prune_modules() -> Result<()> {\n    foreach_module(All, |module| {\n        if !module.join(defs::REMOVE_FILE_NAME).exists() {\n            return Ok(());\n        }\n\n        info!(\"remove module: {}\", module.display());\n\n        // Execute metamodule's metauninstall.sh first\n        let module_id = module.file_name().and_then(|n| n.to_str()).unwrap_or(\"\");\n\n        // Check if this is a metamodule\n        let is_metamodule = read_module_prop(module)\n            .map(|props| metamodule::is_metamodule(&props))\n            .unwrap_or(false);\n\n        if is_metamodule {\n            info!(\"Removing metamodule symlink\");\n            if let Err(e) = metamodule::remove_symlink() {\n                warn!(\"Failed to remove metamodule symlink: {e}\");\n            }\n        } else if let Err(e) = metamodule::exec_metauninstall_script(module_id) {\n            warn!(\"Failed to exec metamodule uninstall for {module_id}: {e}\",);\n        }\n\n        // Then execute module's own uninstall.sh\n        let uninstaller = module.join(\"uninstall.sh\");\n        if uninstaller.exists()\n            && let Err(e) = exec_script(uninstaller, true)\n        {\n            warn!(\"Failed to exec uninstaller: {e}\");\n        }\n\n        // Clear module configs before removing module directory\n        if let Err(e) = crate::module_config::clear_module_configs(module_id) {\n            warn!(\"Failed to clear configs for {module_id}: {e}\");\n        }\n\n        // Finally remove the module directory\n        if let Err(e) = remove_dir_all(module) {\n            warn!(\"Failed to remove {}: {e}\", module.display());\n        }\n\n        Ok(())\n    })?;\n\n    // collect remaining modules, if none, clean up metamodule record\n    let remaining_modules: Vec<_> = std::fs::read_dir(defs::MODULE_DIR)?\n        .filter_map(std::result::Result::ok)\n        .filter(|entry| entry.path().join(\"module.prop\").exists())\n        .collect();\n\n    if remaining_modules.is_empty() {\n        info!(\"no remaining modules.\");\n    }\n\n    Ok(())\n}\n\npub fn handle_updated_modules() -> Result<()> {\n    let modules_root = Path::new(MODULE_DIR);\n    foreach_module(ModuleType::Updated, |updated_module| {\n        if !updated_module.is_dir() {\n            return Ok(());\n        }\n\n        if let Some(name) = updated_module.file_name() {\n            let module_dir = modules_root.join(name);\n            let mut disabled = false;\n            let mut removed = false;\n            if module_dir.exists() {\n                // If the old module is disabled, we need to also disable the new one\n                disabled = module_dir.join(defs::DISABLE_FILE_NAME).exists();\n                removed = module_dir.join(defs::REMOVE_FILE_NAME).exists();\n                remove_dir_all(&module_dir)?;\n            }\n            rename(updated_module, &module_dir)?;\n            if removed {\n                let path = module_dir.join(defs::REMOVE_FILE_NAME);\n                if let Err(e) = ensure_file_exists(&path) {\n                    warn!(\"Failed to create {}: {e}\", path.display());\n                }\n            } else if disabled {\n                let path = module_dir.join(defs::DISABLE_FILE_NAME);\n                if let Err(e) = ensure_file_exists(&path) {\n                    warn!(\"Failed to create {}: {e}\", path.display());\n                }\n            }\n        }\n        Ok(())\n    })?;\n    Ok(())\n}\n\nfn install_module_to_system(zip: &str) -> Result<()> {\n    ensure_boot_completed()?;\n\n    // print banner\n    println!(include_str!(\"banner\"));\n\n    assets::ensure_binaries(false).with_context(|| \"Failed to extract assets\")?;\n\n    // first check if working dir is usable\n    ensure_dir_exists(defs::WORKING_DIR).with_context(|| \"Failed to create working dir\")?;\n    ensure_dir_exists(defs::BINARY_DIR).with_context(|| \"Failed to create bin dir\")?;\n\n    // read the module_id from zip, if failed it will return early.\n    let mut buffer: Vec<u8> = Vec::new();\n    let entry_path = PathBuf::from_str(\"module.prop\")?;\n    let zip_path = PathBuf::from_str(zip)?;\n    let zip_path = zip_path.canonicalize()?;\n    zip_extract_file_to_memory(&zip_path, &entry_path, &mut buffer)?;\n\n    let mut module_prop = HashMap::new();\n    PropertiesIter::new_with_encoding(Cursor::new(buffer), encoding_rs::UTF_8).read_into(\n        |k, v| {\n            module_prop.insert(k, v);\n        },\n    )?;\n    info!(\"module prop: {module_prop:?}\");\n\n    let Some(module_id) = module_prop.get(\"id\") else {\n        bail!(\"module id not found in module.prop!\");\n    };\n    let module_id = module_id.trim();\n\n    // Validate module_id format\n    validate_module_id(module_id)\n        .with_context(|| format!(\"Invalid module ID in module.prop: '{module_id}'\"))?;\n\n    // Check if this module is a metamodule\n    let is_metamodule = metamodule::is_metamodule(&module_prop);\n\n    // Check if it's safe to install regular module\n    if !is_metamodule && let Err(is_disabled) = metamodule::check_install_safety() {\n        println!(\"\\n❌ Installation Blocked\");\n        println!(\"┌────────────────────────────────\");\n        println!(\"│ A metamodule with custom installer is active\");\n        println!(\"│\");\n        if is_disabled {\n            println!(\"│ Current state: Disabled\");\n            println!(\"│ Action required: Re-enable or uninstall it, then reboot\");\n        } else {\n            println!(\"│ Current state: Pending changes\");\n            println!(\"│ Action required: Reboot to apply changes first\");\n        }\n        println!(\"└─────────────────────────────────\\n\");\n        bail!(\"Metamodule installation blocked\");\n    }\n\n    // All modules (including metamodules) are installed to MODULE_UPDATE_DIR\n    let updated_dir = Path::new(defs::MODULE_UPDATE_DIR).join(module_id);\n\n    if is_metamodule {\n        info!(\"Installing metamodule: {module_id}\");\n\n        // Check if there's already a metamodule installed\n        if metamodule::has_metamodule()\n            && let Some(existing_path) = metamodule::get_metamodule_path()\n        {\n            let existing_id = read_module_prop(&existing_path)\n                .ok()\n                .and_then(|m| m.get(\"id\").cloned())\n                .unwrap_or_else(|| \"unknown\".to_string());\n\n            if existing_id != module_id {\n                println!(\"\\n❌ Installation Failed\");\n                println!(\"┌────────────────────────────────\");\n                println!(\"│ A metamodule is already installed\");\n                println!(\"│   Current metamodule: {existing_id}\");\n                println!(\"│\");\n                println!(\"│ Only one metamodule can be active at a time.\");\n                println!(\"│\");\n                println!(\"│ To install this metamodule:\");\n                println!(\"│   1. Uninstall the current metamodule\");\n                println!(\"│   2. Reboot your device\");\n                println!(\"│   3. Install the new metamodule\");\n                println!(\"└─────────────────────────────────\\n\");\n                bail!(\"Cannot install multiple metamodules\");\n            }\n        }\n    }\n\n    let zip_uncompressed_size = get_zip_uncompressed_size(zip)?;\n    info!(\n        \"zip uncompressed size: {}\",\n        humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)\n    );\n    println!(\n        \"- Module size: {}\",\n        humansize::format_size(zip_uncompressed_size, humansize::DECIMAL)\n    );\n\n    // Ensure module directory exists and set SELinux context\n    ensure_dir_exists(defs::MODULE_UPDATE_DIR)?;\n    setsyscon(defs::MODULE_UPDATE_DIR)?;\n\n    // Prepare target directory\n    println!(\"- Installing to {}\", updated_dir.display());\n    ensure_clean_dir(&updated_dir)?;\n    info!(\"target dir: {}\", updated_dir.display());\n\n    // Extract zip to target directory\n    println!(\"- Extracting module files\");\n    let file = File::open(zip)?;\n    let mut archive = zip::ZipArchive::new(file)?;\n    archive.extract(&updated_dir)?;\n\n    // Set permission and selinux context for $MOD/system\n    let module_system_dir = updated_dir.join(\"system\");\n    if module_system_dir.exists() {\n        #[cfg(unix)]\n        set_permissions(&module_system_dir, Permissions::from_mode(0o755))?;\n        restore_syscon(&module_system_dir)?;\n    }\n\n    // Execute install script\n    println!(\"- Running module installer\");\n    exec_install_script(zip, is_metamodule)?;\n\n    let module_dir = Path::new(MODULE_DIR).join(module_id);\n    ensure_dir_exists(&module_dir)?;\n    copy(\n        updated_dir.join(\"module.prop\"),\n        module_dir.join(\"module.prop\"),\n    )?;\n    ensure_file_exists(module_dir.join(UPDATE_FILE_NAME))?;\n\n    // Create symlink for metamodule\n    if is_metamodule {\n        println!(\"- Creating metamodule symlink\");\n        metamodule::ensure_symlink(&module_dir)?;\n    }\n\n    println!(\"- Module installed successfully!\");\n    info!(\"Module {module_id} installed successfully!\");\n\n    Ok(())\n}\n\npub fn install_module(zip: &str) -> Result<()> {\n    let result = install_module_to_system(zip);\n    if let Err(ref e) = result {\n        println!(\"- Error: {e}\");\n    }\n    result\n}\n\npub fn undo_uninstall_module(id: &str) -> Result<()> {\n    validate_module_id(id)?;\n\n    let module_path = Path::new(defs::MODULE_DIR).join(id);\n    ensure!(module_path.exists(), \"Module {id} not found\");\n\n    // Remove the remove mark\n    let remove_file = module_path.join(defs::REMOVE_FILE_NAME);\n    if remove_file.exists() {\n        std::fs::remove_file(&remove_file)\n            .with_context(|| format!(\"Failed to delete remove file for module '{id}'\"))?;\n        info!(\"Removed the remove mark for module {id}\");\n    }\n\n    Ok(())\n}\n\npub fn uninstall_module(id: &str) -> Result<()> {\n    validate_module_id(id)?;\n\n    let module_path = Path::new(defs::MODULE_DIR).join(id);\n    ensure!(module_path.exists(), \"Module {id} not found\");\n\n    // Mark for removal\n    let remove_file = module_path.join(defs::REMOVE_FILE_NAME);\n    File::create(remove_file).with_context(|| \"Failed to create remove file\")?;\n\n    info!(\"Module {id} marked for removal\");\n\n    Ok(())\n}\n\npub fn run_action(id: &str) -> Result<()> {\n    validate_module_id(id)?;\n\n    let action_script_path = format!(\"/data/adb/modules/{id}/action.sh\");\n    exec_script(&action_script_path, true)\n}\n\npub fn enable_module(id: &str) -> Result<()> {\n    validate_module_id(id)?;\n\n    let module_path = Path::new(defs::MODULE_DIR).join(id);\n    ensure!(module_path.exists(), \"Module {id} not found\");\n\n    let disable_path = module_path.join(defs::DISABLE_FILE_NAME);\n    if disable_path.exists() {\n        std::fs::remove_file(&disable_path).with_context(|| {\n            format!(\"Failed to remove disable file: {}\", disable_path.display())\n        })?;\n        info!(\"Module {id} enabled\");\n    }\n\n    Ok(())\n}\n\npub fn disable_module(id: &str) -> Result<()> {\n    let module_path = Path::new(defs::MODULE_DIR).join(id);\n    ensure!(module_path.exists(), \"Module {id} not found\");\n\n    let disable_path = module_path.join(defs::DISABLE_FILE_NAME);\n    ensure_file_exists(disable_path)?;\n\n    info!(\"Module {id} disabled\");\n\n    Ok(())\n}\n\npub fn disable_all_modules() -> Result<()> {\n    mark_all_modules(defs::DISABLE_FILE_NAME)\n}\n\npub fn uninstall_all_modules() -> Result<()> {\n    info!(\"Uninstalling all modules\");\n    mark_all_modules(defs::REMOVE_FILE_NAME)\n}\n\nfn mark_all_modules(flag_file: &str) -> Result<()> {\n    // we assume the module dir is already mounted\n    let dir = std::fs::read_dir(defs::MODULE_DIR)?;\n    for entry in dir.flatten() {\n        let path = entry.path();\n        let flag = path.join(flag_file);\n        if let Err(e) = ensure_file_exists(flag) {\n            warn!(\"Failed to mark module: {}: {e}\", path.display());\n        }\n    }\n\n    Ok(())\n}\n\n/// Read module.prop from the given module path and return as a HashMap\npub fn read_module_prop(module_path: &Path) -> Result<HashMap<String, String>> {\n    let module_prop = module_path.join(\"module.prop\");\n    ensure!(\n        module_prop.exists(),\n        \"module.prop not found in {}\",\n        module_path.display()\n    );\n\n    let content = std::fs::read(&module_prop)\n        .with_context(|| format!(\"Failed to read module.prop: {}\", module_prop.display()))?;\n\n    let mut prop_map: HashMap<String, String> = HashMap::new();\n    PropertiesIter::new_with_encoding(Cursor::new(content), encoding_rs::UTF_8)\n        .read_into(|k, v| {\n            prop_map.insert(k, v);\n        })\n        .with_context(|| format!(\"Failed to parse module.prop: {}\", module_prop.display()))?;\n\n    Ok(prop_map)\n}\n\n/// Resolve a module icon path to an absolute on-disk path\nfn resolve_module_icon_path(\n    module_prop_map: &mut HashMap<String, String>,\n    key: &str,\n    module_path: &Path,\n) {\n    if let Some(icon_value) = module_prop_map.get(key) {\n        let icon_value = icon_value.trim();\n        if icon_value.is_empty() {\n            return;\n        }\n        let path = std::path::Path::new(icon_value);\n        if path.is_absolute() {\n            log::warn!(\n                \"Rejected {} with absolute path for module {}: {}\",\n                key,\n                module_prop_map.get(\"id\").map_or(\"\", String::as_str),\n                icon_value\n            );\n            return;\n        }\n        let has_parent = path\n            .components()\n            .any(|c| matches!(c, std::path::Component::ParentDir));\n        if has_parent {\n            log::warn!(\n                \"Rejected {} with parent traversal for module {}: {}\",\n                key,\n                module_prop_map.get(\"id\").map_or(\"\", String::as_str),\n                icon_value\n            );\n            return;\n        }\n        let candidate = module_path.join(path);\n        if candidate.exists() && candidate.is_file() {\n            if let Some(s) = candidate.to_str() {\n                module_prop_map.insert(key.to_owned(), s.to_string());\n            }\n        } else {\n            log::debug!(\n                \"{} not found for module {}: {}\",\n                key,\n                module_prop_map.get(\"id\").map_or(\"\", String::as_str),\n                candidate.display()\n            );\n        }\n    }\n}\n\nfn list_module(path: &str) -> Vec<HashMap<String, String>> {\n    // Load all module configs once to minimize I/O overhead\n    let all_configs = match crate::module_config::get_all_module_configs() {\n        Ok(configs) => configs,\n        Err(e) => {\n            warn!(\"Failed to load module configs: {e}\");\n            HashMap::new()\n        }\n    };\n\n    // first check enabled modules\n    let dir = std::fs::read_dir(path);\n    let Ok(dir) = dir else {\n        return Vec::new();\n    };\n\n    let mut modules: Vec<HashMap<String, String>> = Vec::new();\n\n    for entry in dir.flatten() {\n        let path = entry.path();\n        info!(\"path: {}\", path.display());\n\n        if !path.join(\"module.prop\").exists() {\n            continue;\n        }\n\n        let mut module_prop_map = match read_module_prop(&path) {\n            Ok(prop) => prop,\n            Err(e) => {\n                warn!(\"Failed to read module.prop for {}: {e}\", path.display());\n                continue;\n            }\n        };\n\n        // If id is missing or empty, use directory name as fallback\n        if !module_prop_map.contains_key(\"id\") || module_prop_map[\"id\"].is_empty() {\n            if let Some(id) = entry.file_name().to_str() {\n                info!(\"Use dir name as module id: {id}\");\n                module_prop_map.insert(\"id\".to_owned(), id.to_owned());\n            } else {\n                info!(\"Failed to get module id from dir name\");\n                continue;\n            }\n        }\n\n        // Add enabled, update, remove, web, action flags\n        let enabled = !path.join(defs::DISABLE_FILE_NAME).exists();\n        let update = path.join(defs::UPDATE_FILE_NAME).exists();\n        let remove = path.join(defs::REMOVE_FILE_NAME).exists();\n        let web = path.join(defs::MODULE_WEB_DIR).exists();\n        let action = path.join(defs::MODULE_ACTION_SH).exists();\n        let need_mount = path.join(\"system\").exists() && !path.join(\"skip_mount\").exists();\n\n        module_prop_map.insert(\"enabled\".to_owned(), enabled.to_string());\n        module_prop_map.insert(\"update\".to_owned(), update.to_string());\n        module_prop_map.insert(\"remove\".to_owned(), remove.to_string());\n        module_prop_map.insert(\"web\".to_owned(), web.to_string());\n        module_prop_map.insert(\"action\".to_owned(), action.to_string());\n        module_prop_map.insert(\"mount\".to_owned(), need_mount.to_string());\n\n        resolve_module_icon_path(&mut module_prop_map, \"actionIcon\", &path);\n        resolve_module_icon_path(&mut module_prop_map, \"webuiIcon\", &path);\n\n        // Apply module config overrides and extract managed features\n        if let Some(module_id) = module_prop_map.get(\"id\")\n            && let Some(config) = all_configs.get(module_id.as_str())\n        {\n            // Apply override.description\n            if let Some(desc) = config.get(\"override.description\") {\n                module_prop_map.insert(\"description\".to_owned(), desc.clone());\n            }\n\n            // Extract managed features from manage.* config entries\n            let managed_features: Vec<String> = config\n                .iter()\n                .filter_map(|(k, v)| {\n                    if k.starts_with(\"manage.\") && crate::module_config::parse_bool_config(v) {\n                        k.strip_prefix(\"manage.\")\n                            .map(std::string::ToString::to_string)\n                    } else {\n                        None\n                    }\n                })\n                .collect();\n\n            if !managed_features.is_empty() {\n                module_prop_map.insert(\"managedFeatures\".to_owned(), managed_features.join(\",\"));\n            }\n        }\n\n        modules.push(module_prop_map);\n    }\n\n    modules\n}\n\npub fn list_modules() -> Result<()> {\n    let modules = list_module(defs::MODULE_DIR);\n    println!(\"{}\", serde_json::to_string_pretty(&modules)?);\n    Ok(())\n}\n\n/// Get all managed features from active modules\n/// Modules declare managed features via config system (manage.<feature>=true)\n/// Returns: HashMap<ModuleId, Vec<ManagedFeature>>\npub fn get_managed_features() -> Result<HashMap<String, Vec<String>>> {\n    let mut managed_features_map: HashMap<String, Vec<String>> = HashMap::new();\n\n    foreach_active_module(|module_path| {\n        // Get module ID\n        let Some(module_id) = module_path.file_name().and_then(|n| n.to_str()) else {\n            warn!(\n                \"Failed to get module id from path: {}\",\n                module_path.display()\n            );\n            return Ok(());\n        };\n\n        // Read module config\n        let config = match crate::module_config::merge_configs(module_id) {\n            Ok(c) => c,\n            Err(e) => {\n                warn!(\"Failed to merge configs for module '{module_id}': {e}\");\n                return Ok(()); // Skip this module\n            }\n        };\n\n        // Extract manage.* config entries\n        let mut feature_list = Vec::new();\n        for (key, value) in &config {\n            if key.starts_with(\"manage.\") {\n                // Parse feature name\n                if let Some(feature_name) = key.strip_prefix(\"manage.\")\n                    && crate::module_config::parse_bool_config(value)\n                {\n                    feature_list.push(feature_name.to_string());\n                }\n            }\n        }\n\n        if !feature_list.is_empty() {\n            managed_features_map.insert(module_id.to_string(), feature_list);\n        }\n\n        Ok(())\n    })?;\n\n    Ok(managed_features_map)\n}\n"
  },
  {
    "path": "userspace/ksud/src/module_config.rs",
    "content": "use anyhow::{Context, Result, bail};\nuse log::{debug, warn};\nuse std::collections::HashMap;\nuse std::fs::{self, File};\nuse std::io::{Read, Write};\nuse std::path::{Path, PathBuf};\n\nuse crate::defs;\nuse crate::utils::ensure_dir_exists;\n\n#[allow(clippy::unreadable_literal)]\nconst MODULE_CONFIG_MAGIC: u32 = 0x4b53554d; // \"KSUM\"\nconst MODULE_CONFIG_VERSION: u32 = 1;\n\n// Validation limits\npub const MAX_CONFIG_KEY_LEN: usize = 256;\npub const MAX_CONFIG_VALUE_LEN: usize = 1024 * 1024; // 1MB\npub const MAX_CONFIG_COUNT: usize = 32;\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum ConfigType {\n    Persist,\n    Temp,\n}\n\nimpl ConfigType {\n    const fn filename(self) -> &'static str {\n        match self {\n            Self::Persist => defs::PERSIST_CONFIG_NAME,\n            Self::Temp => defs::TEMP_CONFIG_NAME,\n        }\n    }\n}\n\n/// Parse a boolean config value\n/// Accepts \"true\", \"1\" (case-insensitive) as true, everything else as false\npub fn parse_bool_config(value: &str) -> bool {\n    let trimmed = value.trim();\n    trimmed.eq_ignore_ascii_case(\"true\") || trimmed == \"1\"\n}\n\n/// Validate config key\n/// Uses the same validation rules as module_id: ^[a-zA-Z][a-zA-Z0-9._-]+$\n/// - Must start with a letter (a-zA-Z)\n/// - Followed by one or more alphanumeric, dot, underscore, or hyphen characters\n/// - Minimum length: 2 characters\npub fn validate_config_key(key: &str) -> Result<()> {\n    if key.is_empty() {\n        bail!(\"Config key cannot be empty\");\n    }\n\n    if key.len() > MAX_CONFIG_KEY_LEN {\n        bail!(\n            \"Config key too long: {} bytes (max: {})\",\n            key.len(),\n            MAX_CONFIG_KEY_LEN\n        );\n    }\n\n    // Use same pattern as module_id for consistency\n    let re = regex_lite::Regex::new(r\"^[a-zA-Z][a-zA-Z0-9._-]+$\")?;\n    if !re.is_match(key) {\n        bail!(\"Invalid config key: '{key}'. Must match /^[a-zA-Z][a-zA-Z0-9._-]+$/\");\n    }\n\n    Ok(())\n}\n\n/// Validate config value\n/// Only enforces maximum length - no character restrictions\n/// Values are stored in binary format with length prefix, so any UTF-8 data is safe\npub fn validate_config_value(value: &str) -> Result<()> {\n    if value.len() > MAX_CONFIG_VALUE_LEN {\n        bail!(\n            \"Config value too long: {} bytes (max: {})\",\n            value.len(),\n            MAX_CONFIG_VALUE_LEN\n        );\n    }\n\n    // No character restrictions - binary storage format handles all UTF-8 safely\n    Ok(())\n}\n\n/// Validate config count\nfn validate_config_count(config: &HashMap<String, String>) -> Result<()> {\n    if config.len() > MAX_CONFIG_COUNT {\n        bail!(\n            \"Too many config entries: {} (max: {})\",\n            config.len(),\n            MAX_CONFIG_COUNT\n        );\n    }\n    Ok(())\n}\n\n/// Get the config directory path for a module\nfn get_config_dir(module_id: &str) -> PathBuf {\n    Path::new(defs::MODULE_CONFIG_DIR).join(module_id)\n}\n\n/// Get the config file path for a module\nfn get_config_path(module_id: &str, config_type: ConfigType) -> PathBuf {\n    get_config_dir(module_id).join(config_type.filename())\n}\n\n/// Ensure the config directory exists\nfn ensure_config_dir(module_id: &str) -> Result<PathBuf> {\n    let dir = get_config_dir(module_id);\n    ensure_dir_exists(&dir)?;\n    Ok(dir)\n}\n\n/// Load config from binary file\npub fn load_config(module_id: &str, config_type: ConfigType) -> Result<HashMap<String, String>> {\n    crate::module::validate_module_id(module_id)?;\n\n    let config_path = get_config_path(module_id, config_type);\n\n    if !config_path.exists() {\n        debug!(\"Config file not found: {}\", config_path.display());\n        return Ok(HashMap::new());\n    }\n\n    let mut file = File::open(&config_path)\n        .with_context(|| format!(\"Failed to open config file: {}\", config_path.display()))?;\n\n    // Read magic\n    let mut magic_buf = [0u8; 4];\n    file.read_exact(&mut magic_buf)\n        .with_context(|| \"Failed to read magic\")?;\n    let magic = u32::from_le_bytes(magic_buf);\n\n    if magic != MODULE_CONFIG_MAGIC {\n        bail!(\"Invalid config magic: expected 0x{MODULE_CONFIG_MAGIC:08x}, got 0x{magic:08x}\");\n    }\n\n    // Read version\n    let mut version_buf = [0u8; 4];\n    file.read_exact(&mut version_buf)\n        .with_context(|| \"Failed to read version\")?;\n    let version = u32::from_le_bytes(version_buf);\n\n    if version != MODULE_CONFIG_VERSION {\n        bail!(\"Unsupported config version: expected {MODULE_CONFIG_VERSION}, got {version}\");\n    }\n\n    // Read count\n    let mut count_buf = [0u8; 4];\n    file.read_exact(&mut count_buf)\n        .with_context(|| \"Failed to read count\")?;\n    let count = u32::from_le_bytes(count_buf);\n\n    // Read entries\n    let mut config = HashMap::new();\n    for i in 0..count {\n        // Read key length\n        let mut key_len_buf = [0u8; 4];\n        file.read_exact(&mut key_len_buf)\n            .with_context(|| format!(\"Failed to read key length for entry {i}\"))?;\n        let key_len = u32::from_le_bytes(key_len_buf) as usize;\n\n        // Read key data\n        let mut key_buf = vec![0u8; key_len];\n        file.read_exact(&mut key_buf)\n            .with_context(|| format!(\"Failed to read key data for entry {i}\"))?;\n        let key = String::from_utf8(key_buf)\n            .with_context(|| format!(\"Invalid UTF-8 in key for entry {i}\"))?;\n\n        // Read value length\n        let mut value_len_buf = [0u8; 4];\n        file.read_exact(&mut value_len_buf)\n            .with_context(|| format!(\"Failed to read value length for entry {i}\"))?;\n        let value_len = u32::from_le_bytes(value_len_buf) as usize;\n\n        // Read value data\n        let mut value_buf = vec![0u8; value_len];\n        file.read_exact(&mut value_buf)\n            .with_context(|| format!(\"Failed to read value data for entry {i}\"))?;\n        let value = String::from_utf8(value_buf)\n            .with_context(|| format!(\"Invalid UTF-8 in value for entry {i}\"))?;\n\n        config.insert(key, value);\n    }\n\n    debug!(\n        \"Loaded {} entries from {}\",\n        config.len(),\n        config_path.display()\n    );\n    Ok(config)\n}\n\n/// Save config to binary file\npub fn save_config(\n    module_id: &str,\n    config_type: ConfigType,\n    config: &HashMap<String, String>,\n) -> Result<()> {\n    crate::module::validate_module_id(module_id)?;\n\n    // Validate config count\n    validate_config_count(config)?;\n\n    // Validate all keys and values\n    for (key, value) in config {\n        validate_config_key(key).with_context(|| format!(\"Invalid config key: '{key}'\"))?;\n        validate_config_value(value)\n            .with_context(|| format!(\"Invalid config value for key '{key}'\"))?;\n    }\n\n    ensure_config_dir(module_id)?;\n\n    let config_path = get_config_path(module_id, config_type);\n    let temp_path = config_path.with_extension(\"tmp\");\n\n    // Write to temporary file first\n    let mut file = File::create(&temp_path)\n        .with_context(|| format!(\"Failed to create temp config file: {}\", temp_path.display()))?;\n\n    // Write magic\n    file.write_all(&MODULE_CONFIG_MAGIC.to_le_bytes())\n        .with_context(|| \"Failed to write magic\")?;\n\n    // Write version\n    file.write_all(&MODULE_CONFIG_VERSION.to_le_bytes())\n        .with_context(|| \"Failed to write version\")?;\n\n    // Write count\n    let count = config.len() as u32;\n    file.write_all(&count.to_le_bytes())\n        .with_context(|| \"Failed to write count\")?;\n\n    // Write entries\n    for (key, value) in config {\n        // Write key length\n        let key_bytes = key.as_bytes();\n        let key_len = key_bytes.len() as u32;\n        file.write_all(&key_len.to_le_bytes())\n            .with_context(|| format!(\"Failed to write key length for '{key}'\"))?;\n\n        // Write key data\n        file.write_all(key_bytes)\n            .with_context(|| format!(\"Failed to write key data for '{key}'\"))?;\n\n        // Write value length\n        let value_bytes = value.as_bytes();\n        let value_len = value_bytes.len() as u32;\n        file.write_all(&value_len.to_le_bytes())\n            .with_context(|| format!(\"Failed to write value length for '{key}'\"))?;\n\n        // Write value data\n        file.write_all(value_bytes)\n            .with_context(|| format!(\"Failed to write value data for '{key}'\"))?;\n    }\n\n    file.sync_all()\n        .with_context(|| \"Failed to sync config file\")?;\n\n    // Atomic rename\n    fs::rename(&temp_path, &config_path).with_context(|| {\n        format!(\n            \"Failed to rename config file: {} -> {}\",\n            temp_path.display(),\n            config_path.display()\n        )\n    })?;\n\n    debug!(\n        \"Saved {} entries to {}\",\n        config.len(),\n        config_path.display()\n    );\n    Ok(())\n}\n\n/// Get a single config value\n#[allow(dead_code)]\npub fn get_config_value(\n    module_id: &str,\n    key: &str,\n    config_type: ConfigType,\n) -> Result<Option<String>> {\n    let config = load_config(module_id, config_type)?;\n    Ok(config.get(key).cloned())\n}\n\n/// Set a single config value\npub fn set_config_value(\n    module_id: &str,\n    key: &str,\n    value: &str,\n    config_type: ConfigType,\n) -> Result<()> {\n    // Validate input early for better error messages\n    validate_config_key(key)?;\n    validate_config_value(value)?;\n\n    let mut config = load_config(module_id, config_type)?;\n    config.insert(key.to_string(), value.to_string());\n\n    // Note: save_config will also validate, but this provides earlier feedback\n    save_config(module_id, config_type, &config)?;\n    Ok(())\n}\n\n/// Delete a single config value\npub fn delete_config_value(module_id: &str, key: &str, config_type: ConfigType) -> Result<()> {\n    let mut config = load_config(module_id, config_type)?;\n\n    if config.remove(key).is_none() {\n        bail!(\"Key '{key}' not found in config\");\n    }\n\n    save_config(module_id, config_type, &config)?;\n    Ok(())\n}\n\n/// Clear all config values\npub fn clear_config(module_id: &str, config_type: ConfigType) -> Result<()> {\n    let config_path = get_config_path(module_id, config_type);\n\n    if config_path.exists() {\n        fs::remove_file(&config_path)\n            .with_context(|| format!(\"Failed to remove config file: {}\", config_path.display()))?;\n        debug!(\"Cleared config: {}\", config_path.display());\n    }\n\n    Ok(())\n}\n\n/// Merge persist and temp configs (temp takes priority)\npub fn merge_configs(module_id: &str) -> Result<HashMap<String, String>> {\n    crate::module::validate_module_id(module_id)?;\n\n    let mut merged = match load_config(module_id, ConfigType::Persist) {\n        Ok(config) => config,\n        Err(e) => {\n            warn!(\"Failed to load persist config for module '{module_id}': {e}\");\n            HashMap::new()\n        }\n    };\n\n    let temp = match load_config(module_id, ConfigType::Temp) {\n        Ok(config) => config,\n        Err(e) => {\n            warn!(\"Failed to load temp config for module '{module_id}': {e}\");\n            HashMap::new()\n        }\n    };\n\n    // Temp config overrides persist config\n    for (key, value) in temp {\n        merged.insert(key, value);\n    }\n\n    Ok(merged)\n}\n\n/// Get all module configs (for iteration)\n/// Loads all configs in a single pass to minimize I/O overhead\npub fn get_all_module_configs() -> Result<HashMap<String, HashMap<String, String>>> {\n    let config_root = Path::new(defs::MODULE_CONFIG_DIR);\n\n    if !config_root.exists() {\n        return Ok(HashMap::new());\n    }\n\n    let mut all_configs = HashMap::new();\n\n    for entry in fs::read_dir(config_root)\n        .with_context(|| format!(\"Failed to read config directory: {}\", config_root.display()))?\n    {\n        let entry = entry?;\n        let path = entry.path();\n\n        if !path.is_dir() {\n            continue;\n        }\n\n        if let Some(module_id) = path.file_name().and_then(|n| n.to_str()) {\n            match merge_configs(module_id) {\n                Ok(config) => {\n                    if !config.is_empty() {\n                        all_configs.insert(module_id.to_string(), config);\n                    }\n                }\n                Err(e) => {\n                    warn!(\"Failed to load config for module '{module_id}': {e}\");\n                    // Continue processing other modules\n                }\n            }\n        }\n    }\n\n    Ok(all_configs)\n}\n\n/// Clear all temporary configs (called during post-fs-data)\npub fn clear_all_temp_configs() -> Result<()> {\n    let config_root = Path::new(defs::MODULE_CONFIG_DIR);\n\n    if !config_root.exists() {\n        debug!(\"Config directory does not exist, nothing to clear\");\n        return Ok(());\n    }\n\n    let mut cleared_count = 0;\n\n    for entry in fs::read_dir(config_root)\n        .with_context(|| format!(\"Failed to read config directory: {}\", config_root.display()))?\n    {\n        let entry = entry?;\n        let path = entry.path();\n\n        if !path.is_dir() {\n            continue;\n        }\n\n        let temp_config = path.join(defs::TEMP_CONFIG_NAME);\n        if temp_config.exists() {\n            match fs::remove_file(&temp_config) {\n                Ok(()) => {\n                    debug!(\"Cleared temp config: {}\", temp_config.display());\n                    cleared_count += 1;\n                }\n                Err(e) => {\n                    warn!(\"Failed to clear temp config {}: {e}\", temp_config.display());\n                }\n            }\n        }\n    }\n\n    if cleared_count > 0 {\n        debug!(\"Cleared {cleared_count} temp config file(s)\");\n    }\n\n    Ok(())\n}\n\n/// Clear all configs for a module (called during uninstall)\npub fn clear_module_configs(module_id: &str) -> Result<()> {\n    crate::module::validate_module_id(module_id)?;\n\n    let config_dir = get_config_dir(module_id);\n\n    if config_dir.exists() {\n        fs::remove_dir_all(&config_dir).with_context(|| {\n            format!(\n                \"Failed to remove config directory: {}\",\n                config_dir.display()\n            )\n        })?;\n        debug!(\"Cleared all configs for module: {module_id}\");\n    }\n\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/profile.rs",
    "content": "use crate::utils::ensure_dir_exists;\nuse crate::{defs, sepolicy};\nuse anyhow::{Context, Result};\nuse std::path::Path;\n\npub fn set_sepolicy(pkg: String, policy: String) -> Result<()> {\n    ensure_dir_exists(defs::PROFILE_SELINUX_DIR)?;\n    let policy_file = Path::new(defs::PROFILE_SELINUX_DIR).join(pkg);\n    std::fs::write(&policy_file, policy)?;\n    sepolicy::apply_file(&policy_file)?;\n    Ok(())\n}\n\npub fn get_sepolicy(pkg: String) -> Result<()> {\n    let policy_file = Path::new(defs::PROFILE_SELINUX_DIR).join(pkg);\n    let policy = std::fs::read_to_string(policy_file)?;\n    println!(\"{policy}\");\n    Ok(())\n}\n\n// ksud doesn't guarteen the correctness of template, it just save\npub fn set_template(id: String, template: String) -> Result<()> {\n    ensure_dir_exists(defs::PROFILE_TEMPLATE_DIR)?;\n    let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);\n    std::fs::write(template_file, template)?;\n    Ok(())\n}\n\npub fn get_template(id: String) -> Result<()> {\n    let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);\n    let template = std::fs::read_to_string(template_file)?;\n    println!(\"{template}\");\n    Ok(())\n}\n\npub fn delete_template(id: String) -> Result<()> {\n    let template_file = Path::new(defs::PROFILE_TEMPLATE_DIR).join(id);\n    std::fs::remove_file(template_file)?;\n    Ok(())\n}\n\npub fn list_templates() -> Result<()> {\n    let templates = std::fs::read_dir(defs::PROFILE_TEMPLATE_DIR);\n    let Ok(templates) = templates else {\n        return Ok(());\n    };\n    for template in templates {\n        let template = template?;\n        let template = template.file_name();\n        if let Some(template) = template.to_str() {\n            println!(\"{template}\");\n        }\n    }\n    Ok(())\n}\n\npub fn apply_sepolies() -> Result<()> {\n    let path = Path::new(defs::PROFILE_SELINUX_DIR);\n    if !path.exists() {\n        log::info!(\"profile sepolicy dir not exists.\");\n        return Ok(());\n    }\n\n    let sepolicies =\n        std::fs::read_dir(path).with_context(|| \"profile sepolicy dir open failed.\".to_string())?;\n    for sepolicy in sepolicies {\n        let Ok(sepolicy) = sepolicy else {\n            log::info!(\"profile sepolicy dir read failed.\");\n            continue;\n        };\n        let sepolicy = sepolicy.path();\n        if sepolicy::apply_file(&sepolicy).is_ok() {\n            log::info!(\"profile sepolicy applied: {}\", sepolicy.display());\n        } else {\n            log::info!(\"profile sepolicy apply failed: {}\", sepolicy.display());\n        }\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/resetprop.rs",
    "content": "use anyhow::{Context, Result, bail};\nuse clap::Parser;\nuse log::info;\nuse prop_rs_android::resetprop::ResetProp;\nuse prop_rs_android::sys_prop;\nuse std::fs::File;\nuse std::io::{BufRead, BufReader};\nuse std::path::Path;\nuse std::time::Duration;\n\n/// Magisk-compatible Android system property tool.\n#[derive(Parser)]\n#[command(\n    name = \"resetprop\",\n    version,\n    about = \"Magisk-compatible system property tool\",\n    disable_help_subcommand = true\n)]\n#[allow(clippy::struct_excessive_bools)]\nstruct Args {\n    /// Skip property_service (force direct mmap operation).\n    #[arg(short = 'n', long = \"skip-svc\")]\n    skip_svc: bool,\n\n    /// Also operate on persistent property storage (persist.* files).\n    #[arg(short = 'p', long = \"persistent\")]\n    persistent: bool,\n\n    /// Only read persistent properties from storage.\n    #[arg(short = 'P')]\n    persist_only: bool,\n\n    /// Delete the named property.\n    #[arg(short = 'd', long = \"delete\")]\n    delete: bool,\n\n    /// Verbose output.\n    #[arg(short = 'v', long = \"verbose\")]\n    verbose: bool,\n\n    /// Wait for a property to exist or match a value.\n    #[arg(short = 'w', long = \"wait\")]\n    wait: bool,\n\n    /// Timeout in seconds for --wait (default: wait forever).\n    #[arg(long = \"timeout\")]\n    timeout: Option<f64>,\n\n    /// Load and set properties from FILE.\n    #[arg(short = 'f', long = \"file\")]\n    file: Option<String>,\n\n    /// Compact property area memory (reclaim holes left by deleted properties).\n    /// Optionally pass a SELinux context name to compact only that area.\n    #[arg(short = 'c', long = \"compact\")]\n    compact: bool,\n\n    /// Show SELinux context when listing properties.\n    #[arg(short = 'Z')]\n    show_context: bool,\n\n    /// Property name.\n    name: Option<String>,\n\n    /// Property value (for set or wait-for-value).\n    value: Option<String>,\n}\n\n/// Entry point for resetprop multicall and subcommand.\n///\n/// `args` should include argv\\[0\\] (the program name).\npub fn run_from_args(args: &[String]) -> Result<()> {\n    let cli = Args::try_parse_from(args).map_err(|e| anyhow::anyhow!(\"{e}\"))?;\n\n    sys_prop::init().context(\"Failed to initialize system property API\")?;\n\n    let rp = ResetProp {\n        skip_svc: cli.skip_svc,\n        persistent: cli.persistent,\n        persist_only: cli.persist_only,\n        verbose: cli.verbose,\n        show_context: cli.show_context,\n    };\n\n    // Validate: at most one special mode\n    let special_modes = u8::from(cli.wait)\n        + u8::from(cli.delete)\n        + u8::from(cli.compact)\n        + u8::from(cli.file.is_some());\n    if special_modes > 1 {\n        bail!(\"multiple operation modes detected\");\n    }\n\n    // -w: wait mode\n    if cli.wait {\n        let name = cli\n            .name\n            .as_deref()\n            .context(\"--wait requires a property name\")?;\n        let timeout = cli.timeout.map(Duration::from_secs_f64);\n        let ok = rp\n            .wait(name, cli.value.as_deref(), timeout)\n            .context(\"wait failed\")?;\n        if !ok {\n            bail!(\"timeout waiting for {name}\");\n        }\n        return Ok(());\n    }\n\n    // -c: compact property area memory\n    // When a positional argument is given, treat it as a SELinux context name.\n    if cli.compact {\n        let context = cli.name.as_deref();\n        let compacted = sys_prop::compact(context).context(\"compact failed\")?;\n        if !compacted {\n            bail!(\"nothing to compact\");\n        }\n        return Ok(());\n    }\n\n    // -f: load from file\n    if let Some(path) = &cli.file {\n        let file = File::open(path).with_context(|| format!(\"Failed to open {path}\"))?;\n        let reader = BufReader::new(file);\n        rp.load_props(reader.lines())\n            .context(\"Failed to load properties from file\")?;\n        return Ok(());\n    }\n\n    // -d: delete\n    if cli.delete {\n        let name = cli\n            .name\n            .as_deref()\n            .context(\"--delete requires a property name\")?;\n        let deleted = rp.delete(name).context(\"delete failed\")?;\n        if !deleted {\n            bail!(\"{name} not found\");\n        }\n        return Ok(());\n    }\n\n    match (&cli.name, &cli.value) {\n        // resetprop name value (set)\n        (Some(name), Some(value)) => {\n            rp.set(name, value)\n                .with_context(|| format!(\"Failed to set {name}\"))?;\n        }\n\n        // resetprop name (get)\n        (Some(name), None) => match rp.get(name) {\n            Some(val) => println!(\"{val}\"),\n            None => bail!(\"{name} not found\"),\n        },\n\n        // resetprop (list all)\n        (None, None) => {\n            let props = rp.list_all().context(\"Failed to list properties\")?;\n            for (name, value) in &props {\n                println!(\"[{name}]: [{value}]\");\n            }\n        }\n\n        // resetprop <no name> <value> — invalid\n        (None, Some(_)) => {\n            bail!(\"property name is required\");\n        }\n    }\n\n    Ok(())\n}\n\n/// Load system.prop file using internal resetprop API.\n///\n/// Equivalent to `resetprop -n --file <path>`.\npub fn load_system_prop_file(path: &Path) -> Result<()> {\n    sys_prop::init().context(\"Failed to initialize system property API\")?;\n\n    let rp = ResetProp {\n        skip_svc: true,\n        persistent: false,\n        persist_only: false,\n        verbose: false,\n        show_context: false,\n    };\n\n    let file = File::open(path).with_context(|| format!(\"Failed to open {}\", path.display()))?;\n    let reader = BufReader::new(file);\n    rp.load_props(reader.lines())\n        .with_context(|| format!(\"Failed to load properties from {}\", path.display()))?;\n\n    info!(\"Loaded system.prop from {}\", path.display());\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/restorecon.rs",
    "content": "use crate::defs;\nuse anyhow::Result;\nuse jwalk::{Parallelism::Serial, WalkDir};\nuse std::path::Path;\n\nuse anyhow::{Context, Ok};\nuse extattr::{Flags as XattrFlags, lsetxattr};\n\npub const SYSTEM_CON: &str = \"u:object_r:system_file:s0\";\npub const ADB_CON: &str = \"u:object_r:adb_data_file:s0\";\npub const UNLABEL_CON: &str = \"u:object_r:unlabeled:s0\";\n\nconst SELINUX_XATTR: &str = \"security.selinux\";\n\npub fn lsetfilecon<P: AsRef<Path>>(path: P, con: &str) -> Result<()> {\n    lsetxattr(&path, SELINUX_XATTR, con, XattrFlags::empty()).with_context(|| {\n        format!(\n            \"Failed to change SELinux context for {}\",\n            path.as_ref().display()\n        )\n    })?;\n    Ok(())\n}\n\npub fn lgetfilecon<P: AsRef<Path>>(path: P) -> Result<String> {\n    let con = extattr::lgetxattr(&path, SELINUX_XATTR).with_context(|| {\n        format!(\n            \"Failed to get SELinux context for {}\",\n            path.as_ref().display()\n        )\n    })?;\n    let con = String::from_utf8_lossy(&con);\n    Ok(con.to_string())\n}\n\npub fn setsyscon<P: AsRef<Path>>(path: P) -> Result<()> {\n    lsetfilecon(path, SYSTEM_CON)\n}\n\npub fn restore_syscon<P: AsRef<Path>>(dir: P) -> Result<()> {\n    for dir_entry in WalkDir::new(dir).parallelism(Serial) {\n        if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path()) {\n            setsyscon(&path)?;\n        }\n    }\n    Ok(())\n}\n\nfn restore_syscon_if_unlabeled<P: AsRef<Path>>(dir: P) -> Result<()> {\n    for dir_entry in WalkDir::new(dir).parallelism(Serial) {\n        if let Some(path) = dir_entry.ok().map(|dir_entry| dir_entry.path())\n            && let anyhow::Result::Ok(con) = lgetfilecon(&path)\n            && (con == UNLABEL_CON || con.is_empty())\n        {\n            lsetfilecon(&path, SYSTEM_CON)?;\n        }\n    }\n    Ok(())\n}\n\npub fn restorecon() -> Result<()> {\n    lsetfilecon(defs::DAEMON_PATH, ADB_CON)?;\n    restore_syscon_if_unlabeled(defs::MODULE_DIR)?;\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/sepolicy.rs",
    "content": "use anyhow::{Context, Result, bail};\nuse derive_new::new;\nuse nom::{\n    AsChar, IResult, Parser,\n    branch::alt,\n    bytes::complete::{tag, take_while, take_while_m_n, take_while1},\n    character::complete::{space0, space1},\n    combinator::map,\n};\nuse std::{path::Path, vec};\n\ntype SeObject<'a> = Vec<&'a str>;\n\nfn is_sepolicy_char(c: char) -> bool {\n    c.is_alphanum() || c == '_' || c == '-'\n}\n\nfn parse_single_word(input: &str) -> IResult<&str, &str> {\n    take_while1(is_sepolicy_char).parse(input)\n}\n\nfn parse_bracket_objs(input: &str) -> IResult<&str, SeObject<'_>> {\n    let (input, (_, words, _)) = (\n        tag(\"{\"),\n        take_while_m_n(1, 100, |c: char| is_sepolicy_char(c) || c.is_whitespace()),\n        tag(\"}\"),\n    )\n        .parse(input)?;\n    Ok((input, words.split_whitespace().collect()))\n}\n\nfn parse_single_obj(input: &str) -> IResult<&str, SeObject<'_>> {\n    let (input, word) = take_while1(is_sepolicy_char).parse(input)?;\n    Ok((input, vec![word]))\n}\n\nfn parse_star(input: &str) -> IResult<&str, SeObject<'_>> {\n    let (input, _) = tag(\"*\").parse(input)?;\n    Ok((input, vec![\"*\"]))\n}\n\n// 1. a single sepolicy word\n// 2. { obj1 obj2 obj3 ...}\n// 3. *\nfn parse_seobj(input: &str) -> IResult<&str, SeObject<'_>> {\n    let (input, strs) = alt((parse_single_obj, parse_bracket_objs, parse_star)).parse(input)?;\n    Ok((input, strs))\n}\n\nfn parse_seobj_no_star(input: &str) -> IResult<&str, SeObject<'_>> {\n    let (input, strs) = alt((parse_single_obj, parse_bracket_objs)).parse(input)?;\n    Ok((input, strs))\n}\n\ntrait SeObjectParser<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self>\n    where\n        Self: Sized;\n}\n\n#[derive(Debug, PartialEq, Eq, new)]\nstruct NormalPerm<'a> {\n    op: &'a str,\n    source: SeObject<'a>,\n    target: SeObject<'a>,\n    class: SeObject<'a>,\n    perm: SeObject<'a>,\n}\n\n#[derive(Debug, PartialEq, Eq, new)]\nstruct XPerm<'a> {\n    op: &'a str,\n    source: SeObject<'a>,\n    target: SeObject<'a>,\n    class: SeObject<'a>,\n    operation: &'a str,\n    perm_set: &'a str,\n}\n\n#[derive(Debug, PartialEq, Eq, new)]\nstruct TypeState<'a> {\n    op: &'a str,\n    stype: SeObject<'a>,\n}\n\n#[derive(Debug, PartialEq, Eq, new)]\nstruct TypeAttr<'a> {\n    stype: SeObject<'a>,\n    sattr: SeObject<'a>,\n}\n\n#[derive(Debug, PartialEq, Eq, new)]\nstruct Type<'a> {\n    name: &'a str,\n    attrs: SeObject<'a>,\n}\n\n#[derive(Debug, PartialEq, Eq, new)]\nstruct Attr<'a> {\n    name: &'a str,\n}\n\n#[derive(Debug, PartialEq, Eq, new)]\nstruct TypeTransition<'a> {\n    source: &'a str,\n    target: &'a str,\n    class: &'a str,\n    default_type: &'a str,\n    object_name: Option<&'a str>,\n}\n\n#[derive(Debug, PartialEq, Eq, new)]\nstruct TypeChange<'a> {\n    op: &'a str,\n    source: &'a str,\n    target: &'a str,\n    class: &'a str,\n    default_type: &'a str,\n}\n\n#[derive(Debug, PartialEq, Eq, new)]\nstruct GenFsCon<'a> {\n    fs_name: &'a str,\n    partial_path: &'a str,\n    fs_context: &'a str,\n}\n\n#[derive(Debug)]\nenum PolicyStatement<'a> {\n    // \"allow *source_type *target_type *class *perm_set\"\n    // \"deny *source_type *target_type *class *perm_set\"\n    // \"auditallow *source_type *target_type *class *perm_set\"\n    // \"dontaudit *source_type *target_type *class *perm_set\"\n    NormalPerm(NormalPerm<'a>),\n\n    // \"allowxperm *source_type *target_type *class operation xperm_set\"\n    // \"auditallowxperm *source_type *target_type *class operation xperm_set\"\n    // \"dontauditxperm *source_type *target_type *class operation xperm_set\"\n    XPerm(XPerm<'a>),\n\n    // \"permissive ^type\"\n    // \"enforce ^type\"\n    TypeState(TypeState<'a>),\n\n    // \"type type_name ^(attribute)\"\n    Type(Type<'a>),\n\n    // \"typeattribute ^type ^attribute\"\n    TypeAttr(TypeAttr<'a>),\n\n    // \"attribute ^attribute\"\n    Attr(Attr<'a>),\n\n    // \"type_transition source_type target_type class default_type (object_name)\"\n    TypeTransition(TypeTransition<'a>),\n\n    // \"type_change source_type target_type class default_type\"\n    // \"type_member source_type target_type class default_type\"\n    TypeChange(TypeChange<'a>),\n\n    // \"genfscon fs_name partial_path fs_context\"\n    GenFsCon(GenFsCon<'a>),\n}\n\nimpl<'a> SeObjectParser<'a> for NormalPerm<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self> {\n        let (input, op) = alt((\n            tag(\"allow\"),\n            tag(\"deny\"),\n            tag(\"auditallow\"),\n            tag(\"dontaudit\"),\n        ))\n        .parse(input)?;\n\n        let (input, _) = space0(input)?;\n        let (input, source) = parse_seobj(input)?;\n        let (input, _) = space0(input)?;\n        let (input, target) = parse_seobj(input)?;\n        let (input, _) = space0(input)?;\n        let (input, class) = parse_seobj(input)?;\n        let (input, _) = space0(input)?;\n        let (input, perm) = parse_seobj(input)?;\n        Ok((input, NormalPerm::new(op, source, target, class, perm)))\n    }\n}\n\nimpl<'a> SeObjectParser<'a> for XPerm<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self> {\n        let (input, op) = alt((\n            tag(\"allowxperm\"),\n            tag(\"auditallowxperm\"),\n            tag(\"dontauditxperm\"),\n        ))\n        .parse(input)?;\n\n        let (input, _) = space0(input)?;\n        let (input, source) = parse_seobj(input)?;\n        let (input, _) = space0(input)?;\n        let (input, target) = parse_seobj(input)?;\n        let (input, _) = space0(input)?;\n        let (input, class) = parse_seobj(input)?;\n        let (input, _) = space0(input)?;\n        let (input, operation) = parse_single_word(input)?;\n        let (input, _) = space0(input)?;\n        let (input, perm_set) = parse_single_word(input)?;\n\n        Ok((\n            input,\n            XPerm::new(op, source, target, class, operation, perm_set),\n        ))\n    }\n}\n\nimpl<'a> SeObjectParser<'a> for TypeState<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self> {\n        let (input, op) = alt((tag(\"permissive\"), tag(\"enforce\"))).parse(input)?;\n\n        let (input, _) = space1(input)?;\n        let (input, stype) = parse_seobj_no_star(input)?;\n\n        Ok((input, TypeState::new(op, stype)))\n    }\n}\n\nimpl<'a> SeObjectParser<'a> for Type<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self> {\n        let (input, _) = tag(\"type\")(input)?;\n        let (input, _) = space1(input)?;\n        let (input, name) = parse_single_word(input)?;\n\n        if input.is_empty() {\n            return Ok((input, Type::new(name, vec![\"domain\"]))); // default to domain\n        }\n\n        let (input, _) = space1(input)?;\n        let (input, attrs) = parse_seobj_no_star(input)?;\n\n        Ok((input, Type::new(name, attrs)))\n    }\n}\n\nimpl<'a> SeObjectParser<'a> for TypeAttr<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self> {\n        let (input, _) = alt((tag(\"typeattribute\"), tag(\"attradd\"))).parse(input)?;\n        let (input, _) = space1(input)?;\n        let (input, stype) = parse_seobj_no_star(input)?;\n        let (input, _) = space1(input)?;\n        let (input, attr) = parse_seobj_no_star(input)?;\n\n        Ok((input, TypeAttr::new(stype, attr)))\n    }\n}\n\nimpl<'a> SeObjectParser<'a> for Attr<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self> {\n        let (input, _) = tag(\"attribute\")(input)?;\n        let (input, _) = space1(input)?;\n        let (input, attr) = parse_single_word(input)?;\n\n        Ok((input, Attr::new(attr)))\n    }\n}\n\nimpl<'a> SeObjectParser<'a> for TypeTransition<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self> {\n        let (input, _) = alt((tag(\"type_transition\"), tag(\"name_transition\"))).parse(input)?;\n        let (input, _) = space1(input)?;\n        let (input, source) = parse_single_word(input)?;\n        let (input, _) = space1(input)?;\n        let (input, target) = parse_single_word(input)?;\n        let (input, _) = space1(input)?;\n        let (input, class) = parse_single_word(input)?;\n        let (input, _) = space1(input)?;\n        let (input, default) = parse_single_word(input)?;\n\n        if input.is_empty() {\n            return Ok((\n                input,\n                TypeTransition::new(source, target, class, default, None),\n            ));\n        }\n\n        let (input, _) = space1(input)?;\n        let (input, object) = parse_single_word(input)?;\n\n        Ok((\n            input,\n            TypeTransition::new(source, target, class, default, Some(object)),\n        ))\n    }\n}\n\nimpl<'a> SeObjectParser<'a> for TypeChange<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self> {\n        let (input, op) = alt((tag(\"type_change\"), tag(\"type_member\"))).parse(input)?;\n        let (input, _) = space1(input)?;\n        let (input, source) = parse_single_word(input)?;\n        let (input, _) = space1(input)?;\n        let (input, target) = parse_single_word(input)?;\n        let (input, _) = space1(input)?;\n        let (input, class) = parse_single_word(input)?;\n        let (input, _) = space1(input)?;\n        let (input, default) = parse_single_word(input)?;\n\n        Ok((input, TypeChange::new(op, source, target, class, default)))\n    }\n}\n\nimpl<'a> SeObjectParser<'a> for GenFsCon<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self>\n    where\n        Self: Sized,\n    {\n        let (input, _) = tag(\"genfscon\")(input)?;\n        let (input, _) = space1(input)?;\n        let (input, fs) = parse_single_word(input)?;\n        let (input, _) = space1(input)?;\n        let (input, path) = parse_single_word(input)?;\n        let (input, _) = space1(input)?;\n        let (input, context) = parse_single_word(input)?;\n        Ok((input, GenFsCon::new(fs, path, context)))\n    }\n}\n\nimpl<'a> PolicyStatement<'a> {\n    fn parse(input: &'a str) -> IResult<&'a str, Self> {\n        let (input, _) = space0(input)?;\n        let (input, statement) = alt((\n            map(NormalPerm::parse, PolicyStatement::NormalPerm),\n            map(XPerm::parse, PolicyStatement::XPerm),\n            map(TypeState::parse, PolicyStatement::TypeState),\n            map(Type::parse, PolicyStatement::Type),\n            map(TypeAttr::parse, PolicyStatement::TypeAttr),\n            map(Attr::parse, PolicyStatement::Attr),\n            map(TypeTransition::parse, PolicyStatement::TypeTransition),\n            map(TypeChange::parse, PolicyStatement::TypeChange),\n            map(GenFsCon::parse, PolicyStatement::GenFsCon),\n        ))\n        .parse(input)?;\n        let (input, _) = space0(input)?;\n        let (input, _) = take_while(|c| c == ';')(input)?;\n        let (input, _) = space0(input)?;\n        Ok((input, statement))\n    }\n}\n\nfn parse_sepolicy<'a, 'b>(input: &'b str, strict: bool) -> Result<Vec<PolicyStatement<'a>>>\nwhere\n    'b: 'a,\n{\n    let mut statements = vec![];\n\n    for line in input.split(['\\n', ';']) {\n        let trimmed_line = line.trim();\n        if trimmed_line.is_empty() || trimmed_line.starts_with('#') {\n            continue;\n        }\n        if let Ok((_, statement)) = PolicyStatement::parse(trimmed_line) {\n            statements.push(statement);\n        } else if strict {\n            bail!(\"Failed to parse policy statement: {line}\")\n        }\n    }\n    Ok(statements)\n}\n\nconst CMD_NORMAL_PERM: u32 = 1;\nconst CMD_XPERM: u32 = 2;\nconst CMD_TYPE_STATE: u32 = 3;\nconst CMD_TYPE: u32 = 4;\nconst CMD_TYPE_ATTR: u32 = 5;\nconst CMD_ATTR: u32 = 6;\nconst CMD_TYPE_TRANSITION: u32 = 7;\nconst CMD_TYPE_CHANGE: u32 = 8;\nconst CMD_GENFSCON: u32 = 9;\n\nconst SUBCMD_NORMAL_PERM_ALLOW: u32 = 1;\nconst SUBCMD_NORMAL_PERM_DENY: u32 = 2;\nconst SUBCMD_NORMAL_PERM_AUDITALLOW: u32 = 3;\nconst SUBCMD_NORMAL_PERM_DONTAUDIT: u32 = 4;\n\nconst SUBCMD_XPERM_ALLOW: u32 = 1;\nconst SUBCMD_XPERM_AUDITALLOW: u32 = 2;\nconst SUBCMD_XPERM_DONTAUDIT: u32 = 3;\n\nconst SUBCMD_TYPE_STATE_PERMISSIVE: u32 = 1;\nconst SUBCMD_TYPE_STATE_ENFORCE: u32 = 2;\n\nconst SUBCMD_TYPE_CHANGE_CHANGE: u32 = 1;\nconst SUBCMD_TYPE_CHANGE_MEMBER: u32 = 2;\n\n#[derive(Debug, Default)]\nenum PolicyObject {\n    All,\n    One(Vec<u8>),\n    #[default]\n    None,\n}\n\nimpl TryFrom<&str> for PolicyObject {\n    type Error = anyhow::Error;\n    fn try_from(s: &str) -> Result<Self> {\n        anyhow::ensure!(!s.as_bytes().contains(&0), \"policy object contains NUL\");\n        if s == \"*\" {\n            return Ok(Self::All);\n        }\n        Ok(Self::One(s.as_bytes().to_vec()))\n    }\n}\n\n/// atomic statement, such as: allow domain1 domain2:file1 read;\n/// normal statement would be expand to atomic statement, for example:\n/// allow domain1 domain2:file1 { read write }; would be expand to two atomic statement\n/// allow domain1 domain2:file1 read;allow domain1 domain2:file1 write;\n#[allow(clippy::too_many_arguments)]\n#[derive(Debug, new)]\nstruct AtomicStatement {\n    cmd: u32,\n    subcmd: u32,\n    sepol1: PolicyObject,\n    sepol2: PolicyObject,\n    sepol3: PolicyObject,\n    sepol4: PolicyObject,\n    sepol5: PolicyObject,\n    sepol6: PolicyObject,\n    sepol7: PolicyObject,\n}\n\nimpl<'a> TryFrom<&'a NormalPerm<'a>> for Vec<AtomicStatement> {\n    type Error = anyhow::Error;\n    fn try_from(perm: &'a NormalPerm<'a>) -> Result<Self> {\n        let mut result = vec![];\n        let subcmd = match perm.op {\n            \"allow\" => SUBCMD_NORMAL_PERM_ALLOW,\n            \"deny\" => SUBCMD_NORMAL_PERM_DENY,\n            \"auditallow\" => SUBCMD_NORMAL_PERM_AUDITALLOW,\n            \"dontaudit\" => SUBCMD_NORMAL_PERM_DONTAUDIT,\n            _ => 0,\n        };\n        for &s in &perm.source {\n            for &t in &perm.target {\n                for &c in &perm.class {\n                    for &p in &perm.perm {\n                        result.push(AtomicStatement {\n                            cmd: CMD_NORMAL_PERM,\n                            subcmd,\n                            sepol1: s.try_into()?,\n                            sepol2: t.try_into()?,\n                            sepol3: c.try_into()?,\n                            sepol4: p.try_into()?,\n                            sepol5: PolicyObject::None,\n                            sepol6: PolicyObject::None,\n                            sepol7: PolicyObject::None,\n                        });\n                    }\n                }\n            }\n        }\n        Ok(result)\n    }\n}\n\nimpl<'a> TryFrom<&'a XPerm<'a>> for Vec<AtomicStatement> {\n    type Error = anyhow::Error;\n    fn try_from(perm: &'a XPerm<'a>) -> Result<Self> {\n        let mut result = vec![];\n        let subcmd = match perm.op {\n            \"allowxperm\" => SUBCMD_XPERM_ALLOW,\n            \"auditallowxperm\" => SUBCMD_XPERM_AUDITALLOW,\n            \"dontauditxperm\" => SUBCMD_XPERM_DONTAUDIT,\n            _ => 0,\n        };\n        for &s in &perm.source {\n            for &t in &perm.target {\n                for &c in &perm.class {\n                    result.push(AtomicStatement {\n                        cmd: CMD_XPERM,\n                        subcmd,\n                        sepol1: s.try_into()?,\n                        sepol2: t.try_into()?,\n                        sepol3: c.try_into()?,\n                        sepol4: perm.operation.try_into()?,\n                        sepol5: perm.perm_set.try_into()?,\n                        sepol6: PolicyObject::None,\n                        sepol7: PolicyObject::None,\n                    });\n                }\n            }\n        }\n        Ok(result)\n    }\n}\n\nimpl<'a> TryFrom<&'a TypeState<'a>> for Vec<AtomicStatement> {\n    type Error = anyhow::Error;\n    fn try_from(perm: &'a TypeState<'a>) -> Result<Self> {\n        let mut result = vec![];\n        let subcmd = match perm.op {\n            \"permissive\" => SUBCMD_TYPE_STATE_PERMISSIVE,\n            \"enforce\" => SUBCMD_TYPE_STATE_ENFORCE,\n            _ => 0,\n        };\n        for &t in &perm.stype {\n            result.push(AtomicStatement {\n                cmd: CMD_TYPE_STATE,\n                subcmd,\n                sepol1: t.try_into()?,\n                sepol2: PolicyObject::None,\n                sepol3: PolicyObject::None,\n                sepol4: PolicyObject::None,\n                sepol5: PolicyObject::None,\n                sepol6: PolicyObject::None,\n                sepol7: PolicyObject::None,\n            });\n        }\n        Ok(result)\n    }\n}\n\nimpl<'a> TryFrom<&'a Type<'a>> for Vec<AtomicStatement> {\n    type Error = anyhow::Error;\n    fn try_from(perm: &'a Type<'a>) -> Result<Self> {\n        let mut result = vec![];\n        for &attr in &perm.attrs {\n            result.push(AtomicStatement {\n                cmd: CMD_TYPE,\n                subcmd: 0,\n                sepol1: perm.name.try_into()?,\n                sepol2: attr.try_into()?,\n                sepol3: PolicyObject::None,\n                sepol4: PolicyObject::None,\n                sepol5: PolicyObject::None,\n                sepol6: PolicyObject::None,\n                sepol7: PolicyObject::None,\n            });\n        }\n        Ok(result)\n    }\n}\n\nimpl<'a> TryFrom<&'a TypeAttr<'a>> for Vec<AtomicStatement> {\n    type Error = anyhow::Error;\n    fn try_from(perm: &'a TypeAttr<'a>) -> Result<Self> {\n        let mut result = vec![];\n        for &t in &perm.stype {\n            for &attr in &perm.sattr {\n                result.push(AtomicStatement {\n                    cmd: CMD_TYPE_ATTR,\n                    subcmd: 0,\n                    sepol1: t.try_into()?,\n                    sepol2: attr.try_into()?,\n                    sepol3: PolicyObject::None,\n                    sepol4: PolicyObject::None,\n                    sepol5: PolicyObject::None,\n                    sepol6: PolicyObject::None,\n                    sepol7: PolicyObject::None,\n                });\n            }\n        }\n        Ok(result)\n    }\n}\n\nimpl<'a> TryFrom<&'a Attr<'a>> for Vec<AtomicStatement> {\n    type Error = anyhow::Error;\n    fn try_from(perm: &'a Attr<'a>) -> Result<Self> {\n        let result = vec![AtomicStatement {\n            cmd: CMD_ATTR,\n            subcmd: 0,\n            sepol1: perm.name.try_into()?,\n            sepol2: PolicyObject::None,\n            sepol3: PolicyObject::None,\n            sepol4: PolicyObject::None,\n            sepol5: PolicyObject::None,\n            sepol6: PolicyObject::None,\n            sepol7: PolicyObject::None,\n        }];\n        Ok(result)\n    }\n}\n\nimpl<'a> TryFrom<&'a TypeTransition<'a>> for Vec<AtomicStatement> {\n    type Error = anyhow::Error;\n    fn try_from(perm: &'a TypeTransition<'a>) -> Result<Self> {\n        let mut result = vec![];\n        let obj = match perm.object_name {\n            Some(obj) => obj.try_into()?,\n            None => PolicyObject::None,\n        };\n        result.push(AtomicStatement {\n            cmd: CMD_TYPE_TRANSITION,\n            subcmd: 0,\n            sepol1: perm.source.try_into()?,\n            sepol2: perm.target.try_into()?,\n            sepol3: perm.class.try_into()?,\n            sepol4: perm.default_type.try_into()?,\n            sepol5: obj,\n            sepol6: PolicyObject::None,\n            sepol7: PolicyObject::None,\n        });\n        Ok(result)\n    }\n}\n\nimpl<'a> TryFrom<&'a TypeChange<'a>> for Vec<AtomicStatement> {\n    type Error = anyhow::Error;\n    fn try_from(perm: &'a TypeChange<'a>) -> Result<Self> {\n        let mut result = vec![];\n        let subcmd = match perm.op {\n            \"type_change\" => SUBCMD_TYPE_CHANGE_CHANGE,\n            \"type_member\" => SUBCMD_TYPE_CHANGE_MEMBER,\n            _ => 0,\n        };\n        result.push(AtomicStatement {\n            cmd: CMD_TYPE_CHANGE,\n            subcmd,\n            sepol1: perm.source.try_into()?,\n            sepol2: perm.target.try_into()?,\n            sepol3: perm.class.try_into()?,\n            sepol4: perm.default_type.try_into()?,\n            sepol5: PolicyObject::None,\n            sepol6: PolicyObject::None,\n            sepol7: PolicyObject::None,\n        });\n        Ok(result)\n    }\n}\n\nimpl<'a> TryFrom<&'a GenFsCon<'a>> for Vec<AtomicStatement> {\n    type Error = anyhow::Error;\n    fn try_from(perm: &'a GenFsCon<'a>) -> Result<Self> {\n        let result = vec![AtomicStatement {\n            cmd: CMD_GENFSCON,\n            subcmd: 0,\n            sepol1: perm.fs_name.try_into()?,\n            sepol2: perm.partial_path.try_into()?,\n            sepol3: perm.fs_context.try_into()?,\n            sepol4: PolicyObject::None,\n            sepol5: PolicyObject::None,\n            sepol6: PolicyObject::None,\n            sepol7: PolicyObject::None,\n        }];\n        Ok(result)\n    }\n}\n\nimpl<'a> TryFrom<&'a PolicyStatement<'a>> for Vec<AtomicStatement> {\n    type Error = anyhow::Error;\n    fn try_from(value: &'a PolicyStatement) -> Result<Self> {\n        match value {\n            PolicyStatement::NormalPerm(perm) => perm.try_into(),\n            PolicyStatement::XPerm(perm) => perm.try_into(),\n            PolicyStatement::TypeState(perm) => perm.try_into(),\n            PolicyStatement::Type(perm) => perm.try_into(),\n            PolicyStatement::TypeAttr(perm) => perm.try_into(),\n            PolicyStatement::Attr(perm) => perm.try_into(),\n            PolicyStatement::TypeTransition(perm) => perm.try_into(),\n            PolicyStatement::TypeChange(perm) => perm.try_into(),\n            PolicyStatement::GenFsCon(perm) => perm.try_into(),\n        }\n    }\n}\n\nconst fn cmd_expected_argc(cmd: u32) -> Option<usize> {\n    match cmd {\n        CMD_NORMAL_PERM | CMD_TYPE_CHANGE => Some(4),\n        CMD_XPERM | CMD_TYPE_TRANSITION => Some(5),\n        CMD_TYPE_STATE | CMD_ATTR => Some(1),\n        CMD_TYPE | CMD_TYPE_ATTR => Some(2),\n        CMD_GENFSCON => Some(3),\n        _ => None,\n    }\n}\n\nfn encode_policy_object(payload: &mut Vec<u8>, object: &PolicyObject) -> Result<()> {\n    let bytes = match object {\n        PolicyObject::None | PolicyObject::All => &[][..],\n        PolicyObject::One(value) => value.as_slice(),\n    };\n\n    let len = u32::try_from(bytes.len()).context(\"policy object too long to encode\")?;\n    payload.extend_from_slice(&len.to_ne_bytes());\n    payload.extend_from_slice(bytes);\n    payload.push(0);\n\n    Ok(())\n}\n\nfn append_atomic_statement(payload: &mut Vec<u8>, statement: &AtomicStatement) -> Result<()> {\n    let expected_argc = cmd_expected_argc(statement.cmd)\n        .ok_or_else(|| anyhow::anyhow!(\"unknown sepolicy cmd {}\", statement.cmd))?;\n\n    payload.extend_from_slice(&statement.cmd.to_ne_bytes());\n    payload.extend_from_slice(&statement.subcmd.to_ne_bytes());\n\n    let args = [\n        &statement.sepol1,\n        &statement.sepol2,\n        &statement.sepol3,\n        &statement.sepol4,\n        &statement.sepol5,\n        &statement.sepol6,\n        &statement.sepol7,\n    ];\n\n    for object in args.iter().take(expected_argc) {\n        encode_policy_object(payload, object)?;\n    }\n\n    Ok(())\n}\n\nfn serialize_atomic_statements(statements: &[AtomicStatement]) -> Result<Vec<u8>> {\n    let mut payload = vec![];\n    for statement in statements {\n        append_atomic_statement(&mut payload, statement)?;\n    }\n    Ok(payload)\n}\n\nfn flatten_atomic_statements<'a>(\n    statements: &'a [PolicyStatement<'a>],\n) -> Result<Vec<AtomicStatement>> {\n    let mut policies = vec![];\n    for statement in statements {\n        let mut expanded: Vec<AtomicStatement> = statement.try_into()?;\n        policies.append(&mut expanded);\n    }\n    Ok(policies)\n}\n\nfn apply_rules_batch<'a>(statements: &'a [PolicyStatement<'a>], strict: bool) -> Result<()> {\n    let policies = flatten_atomic_statements(statements)?;\n    if policies.is_empty() {\n        return Ok(());\n    }\n\n    let payload = serialize_atomic_statements(&policies)?;\n\n    let cmd = crate::ksucalls::SetSepolicyCmd {\n        data_len: payload.len() as u64,\n        data: payload.as_ptr() as u64,\n    };\n\n    match crate::ksucalls::set_sepolicy(&cmd) {\n        Ok(applied_count) => {\n            let applied_count = usize::try_from(applied_count)\n                .context(\"kernel returned negative sepolicy applied count\")?;\n            if applied_count < policies.len() {\n                let err = anyhow::anyhow!(\n                    \"apply sepolicy batch partially succeeded: {applied_count}/{}\",\n                    policies.len()\n                );\n                if strict {\n                    return Err(err);\n                }\n                log::warn!(\"{err}\");\n            }\n        }\n        Err(e) => {\n            log::warn!(\"apply sepolicy batch failed: {e}\");\n            if strict {\n                return Err(anyhow::anyhow!(\"apply sepolicy batch failed: {e}\"));\n            }\n        }\n    }\n\n    Ok(())\n}\n\npub fn live_patch(policy: &str) -> Result<()> {\n    let result = parse_sepolicy(policy.trim(), false)?;\n    for statement in &result {\n        println!(\"{statement:?}\");\n    }\n    apply_rules_batch(&result, false)?;\n    Ok(())\n}\n\npub fn apply_file<P: AsRef<Path>>(path: P) -> Result<()> {\n    let input = std::fs::read_to_string(path)?;\n    live_patch(&input)\n}\n\npub fn check_rule(policy: &str) -> Result<()> {\n    let path = Path::new(policy);\n    let policy = if path.exists() {\n        std::fs::read_to_string(path)?\n    } else {\n        policy.to_string()\n    };\n    parse_sepolicy(policy.trim(), true)?;\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/su.rs",
    "content": "use crate::{\n    defs,\n    utils::{self, umask},\n};\nuse anyhow::{Context, Ok, Result, bail};\nuse getopts::Options;\nuse libc::c_int;\nuse log::error;\nuse std::env;\n#[cfg(unix)]\nuse std::os::unix::process::CommandExt;\nuse std::path::PathBuf;\nuse std::{\n    ffi::{CStr, CString},\n    process::Command,\n};\n\nuse crate::ksucalls::get_wrapped_fd;\nuse rustix::{\n    process::getuid,\n    thread::{Gid, Uid, set_thread_res_gid, set_thread_res_uid},\n};\n\npub fn grant_root(global_mnt: bool) -> Result<()> {\n    crate::ksucalls::grant_root()?;\n\n    let mut command = Command::new(\"sh\");\n    let command = unsafe {\n        command.pre_exec(move || {\n            if global_mnt {\n                let _ = utils::switch_mnt_ns(1);\n            }\n            Result::Ok(())\n        })\n    };\n    // add /data/adb/ksu/bin to PATH\n    add_path_to_env(defs::BINARY_DIR)?;\n    Err(command.exec().into())\n}\n\nfn print_usage(program: &str, opts: &Options) {\n    let brief = format!(\"KernelSU\\n\\nUsage: {program} [options] [-] [user [argument...]]\");\n    print!(\"{}\", opts.usage(&brief));\n}\n\nfn set_identity(uid: u32, gid: u32, groups: &[u32]) {\n    rustix::thread::set_thread_groups(\n        groups\n            .iter()\n            .map(|g| Gid::from_raw(*g))\n            .collect::<Vec<_>>()\n            .as_ref(),\n    )\n    .ok();\n    let gid = Gid::from_raw(gid);\n    let uid = Uid::from_raw(uid);\n    set_thread_res_gid(gid, gid, gid).ok();\n    set_thread_res_uid(uid, uid, uid).ok();\n}\n\nfn wrap_tty(fd: c_int) {\n    let inner_fn = move || -> Result<()> {\n        if unsafe { libc::isatty(fd) != 1 } {\n            return Ok(());\n        }\n        let new_fd = get_wrapped_fd(fd).context(\"get_wrapped_fd\")?;\n        if unsafe { libc::dup2(new_fd, fd) } == -1 {\n            bail!(\"dup {new_fd} -> {fd} errno: {}\", unsafe {\n                *libc::__errno()\n            });\n        }\n        unsafe { libc::close(new_fd) };\n        Ok(())\n    };\n\n    if let Err(e) = inner_fn() {\n        error!(\"wrap tty {fd}: {e:?}\");\n    }\n}\n\n#[allow(clippy::similar_names)]\npub fn root_shell() -> Result<()> {\n    // we are root now, this was set in kernel!\n\n    use anyhow::anyhow;\n    let env_args: Vec<String> = env::args().collect();\n    let program = env_args[0].clone();\n    let args = env_args.iter().position(|arg| arg == \"-c\").map_or_else(\n        || env_args.clone(),\n        |i| {\n            let rest = env_args[i + 1..].to_vec();\n            let mut new_args = env_args[..i].to_vec();\n            new_args.push(\"-c\".to_string());\n            if !rest.is_empty() {\n                new_args.push(rest.join(\" \"));\n            }\n            new_args\n        },\n    );\n\n    let mut opts = Options::new();\n    opts.optopt(\n        \"c\",\n        \"command\",\n        \"pass COMMAND to the invoked shell\",\n        \"COMMAND\",\n    );\n    opts.optflag(\"h\", \"help\", \"display this help message and exit\");\n    opts.optflag(\"l\", \"login\", \"pretend the shell to be a login shell\");\n    opts.optflag(\n        \"p\",\n        \"preserve-environment\",\n        \"preserve the entire environment\",\n    );\n    opts.optopt(\n        \"s\",\n        \"shell\",\n        \"use SHELL instead of the default /system/bin/sh\",\n        \"SHELL\",\n    );\n    opts.optflag(\"v\", \"version\", \"display version number and exit\");\n    opts.optflag(\"V\", \"\", \"display version code and exit\");\n    opts.optflag(\n        \"M\",\n        \"mount-master\",\n        \"force run in the global mount namespace\",\n    );\n    opts.optopt(\"g\", \"group\", \"Specify the primary group\", \"GROUP\");\n    opts.optmulti(\n        \"G\",\n        \"supp-group\",\n        \"Specify a supplementary group. The first specified supplementary group is also used as a primary group if the option -g is not specified.\",\n        \"GROUP\",\n    );\n    opts.optflag(\"W\", \"no-wrapper\", \"don't use ksu fd wrapper\");\n\n    // Replace -cn with -z, -mm with -M for supporting getopt_long\n    let args = args\n        .into_iter()\n        .map(|e| {\n            if e == \"-mm\" {\n                \"-M\".to_string()\n            } else if e == \"-cn\" {\n                \"-z\".to_string()\n            } else {\n                e\n            }\n        })\n        .collect::<Vec<String>>();\n\n    let matches = match opts.parse(&args[1..]) {\n        Result::Ok(m) => m,\n        Err(f) => {\n            println!(\"{f}\");\n            print_usage(&program, &opts);\n            std::process::exit(-1);\n        }\n    };\n\n    if matches.opt_present(\"h\") {\n        print_usage(&program, &opts);\n        return Ok(());\n    }\n\n    if matches.opt_present(\"v\") {\n        println!(\"{}:KernelSU\", defs::VERSION_NAME);\n        return Ok(());\n    }\n\n    if matches.opt_present(\"V\") {\n        println!(\"{}\", defs::VERSION_CODE);\n        return Ok(());\n    }\n\n    let shell = matches\n        .opt_str(\"s\")\n        .unwrap_or_else(|| \"/system/bin/sh\".to_string());\n    let mut is_login = matches.opt_present(\"l\");\n    let preserve_env = matches.opt_present(\"p\");\n    let mount_master = matches.opt_present(\"M\");\n    let use_fd_wrapper = !matches.opt_present(\"W\");\n\n    let groups = matches\n        .opt_strs(\"G\")\n        .into_iter()\n        .map(|g| g.parse::<u32>().map_err(|_| anyhow!(\"Invalid GID: {g}\")))\n        .collect::<Result<Vec<_>, _>>()?;\n\n    // if -g provided, use it.\n    let mut gid = matches\n        .opt_str(\"g\")\n        .map(|g| g.parse::<u32>().map_err(|_| anyhow!(\"Invalid GID: {g}\")))\n        .transpose()?;\n\n    // otherwise, use the first gid of groups.\n    if gid.is_none() && !groups.is_empty() {\n        gid = Some(groups[0]);\n    }\n\n    // we've make sure that -c is the last option and it already contains the whole command, no need to construct it again\n    let args = matches\n        .opt_str(\"c\")\n        .map(|cmd| vec![\"-c\".to_string(), cmd])\n        .unwrap_or_default();\n\n    let mut free_idx = 0;\n    if !matches.free.is_empty() && matches.free[free_idx] == \"-\" {\n        is_login = true;\n        free_idx += 1;\n    }\n\n    // use current uid if no user specified, these has been done in kernel!\n    let mut uid = getuid().as_raw();\n    if free_idx < matches.free.len() {\n        let name = &matches.free[free_idx];\n        uid = unsafe {\n            let pw = CString::new(name.as_str())\n                .ok()\n                .and_then(|c_name| libc::getpwnam(c_name.as_ptr()).as_ref());\n\n            pw.map_or_else(|| name.parse::<u32>().unwrap_or(0), |pw| pw.pw_uid)\n        }\n    }\n\n    // if there is no gid provided, use uid.\n    let gid = gid.unwrap_or(uid);\n    // https://github.com/topjohnwu/Magisk/blob/master/native/src/su/su_daemon.cpp#L408\n    let arg0 = if is_login { \"-\" } else { &shell };\n\n    let mut command = &mut Command::new(&shell);\n\n    if !preserve_env {\n        // This is actually incorrect, i don't know why.\n        // command = command.env_clear();\n\n        let pw = unsafe { libc::getpwuid(uid).as_ref() };\n\n        if let Some(pw) = pw {\n            let home = unsafe { CStr::from_ptr(pw.pw_dir) };\n            let pw_name = unsafe { CStr::from_ptr(pw.pw_name) };\n\n            let home = home.to_string_lossy();\n            let pw_name = pw_name.to_string_lossy();\n\n            command = command\n                .env(\"HOME\", home.as_ref())\n                .env(\"USER\", pw_name.as_ref())\n                .env(\"LOGNAME\", pw_name.as_ref())\n                .env(\"SHELL\", &shell);\n        }\n    }\n\n    // add /data/adb/ksu/bin to PATH\n    add_path_to_env(defs::BINARY_DIR)?;\n\n    // when KSURC_PATH exists and ENV is not set, set ENV to KSURC_PATH\n    if PathBuf::from(defs::KSURC_PATH).exists() && env::var(\"ENV\").is_err() {\n        command = command.env(\"ENV\", defs::KSURC_PATH);\n    }\n\n    // escape from the current cgroup and become session leader\n    // WARNING!!! This cause some root shell hang forever!\n    // command = command.process_group(0);\n    command = unsafe {\n        command.pre_exec(move || {\n            umask(0o22);\n            utils::switch_cgroups();\n\n            // switch to global mount namespace\n            if mount_master {\n                let _ = utils::switch_mnt_ns(1);\n            }\n\n            if use_fd_wrapper {\n                wrap_tty(0);\n                wrap_tty(1);\n                wrap_tty(2);\n            }\n\n            set_identity(uid, gid, &groups);\n\n            Result::Ok(())\n        })\n    };\n\n    command = command.args(args).arg0(arg0);\n    Err(command.exec().into())\n}\n\nfn add_path_to_env(path: &str) -> Result<()> {\n    let mut paths =\n        env::var_os(\"PATH\").map_or(Vec::new(), |val| env::split_paths(&val).collect::<Vec<_>>());\n    let new_path = PathBuf::from(path.trim_end_matches('/'));\n    paths.push(new_path);\n    let new_path_env = env::join_paths(paths)?;\n    unsafe { env::set_var(\"PATH\", new_path_env) };\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksud/src/utils.rs",
    "content": "use anyhow::{Context, Error, Ok, Result, bail};\nuse std::{\n    fs::{File, OpenOptions, create_dir_all, remove_file, write},\n    io::{\n        ErrorKind::{AlreadyExists, NotFound},\n        Write,\n    },\n    path::Path,\n    process::Command,\n};\n\nuse crate::{assets, boot_patch, defs, ksucalls, module, restorecon};\n#[allow(unused_imports)]\nuse std::fs::{Permissions, set_permissions};\n#[cfg(unix)]\nuse std::os::unix::prelude::PermissionsExt;\n\nuse std::path::PathBuf;\n\nuse crate::boot_patch::BootRestoreArgs;\n\nuse rustix::{\n    process,\n    thread::{LinkNameSpaceType, move_into_link_name_space},\n};\n\n#[macro_export]\nmacro_rules! debug_select {\n    ($debug:expr, $release:expr) => {{\n        #[cfg(debug_assertions)]\n        {\n            $debug\n        }\n        #[cfg(not(debug_assertions))]\n        {\n            $release\n        }\n    }};\n}\n\npub fn ensure_clean_dir(dir: impl AsRef<Path>) -> Result<()> {\n    let path = dir.as_ref();\n    log::debug!(\"ensure_clean_dir: {}\", path.display());\n    if path.exists() {\n        log::debug!(\"ensure_clean_dir: {} exists, remove it\", path.display());\n        std::fs::remove_dir_all(path)?;\n    }\n    Ok(std::fs::create_dir_all(path)?)\n}\n\npub fn ensure_file_exists<T: AsRef<Path>>(file: T) -> Result<()> {\n    match File::options().write(true).create_new(true).open(&file) {\n        std::result::Result::Ok(_) => Ok(()),\n        Err(err) => {\n            if err.kind() == AlreadyExists && file.as_ref().is_file() {\n                Ok(())\n            } else {\n                Err(Error::from(err))\n                    .with_context(|| format!(\"{} is not a regular file\", file.as_ref().display()))\n            }\n        }\n    }\n}\n\npub fn ensure_dir_exists<T: AsRef<Path>>(dir: T) -> Result<()> {\n    let result = create_dir_all(&dir);\n    if dir.as_ref().is_dir() && result.is_ok() {\n        Ok(())\n    } else {\n        bail!(\"{} is not a regular directory\", dir.as_ref().display())\n    }\n}\n\npub fn ensure_binary<T: AsRef<Path>>(\n    path: T,\n    contents: &[u8],\n    ignore_if_exist: bool,\n) -> Result<()> {\n    if ignore_if_exist && path.as_ref().exists() {\n        return Ok(());\n    }\n\n    ensure_dir_exists(path.as_ref().parent().ok_or_else(|| {\n        anyhow::anyhow!(\n            \"{} does not have parent directory\",\n            path.as_ref().to_string_lossy()\n        )\n    })?)?;\n\n    if let Err(e) = remove_file(path.as_ref())\n        && e.kind() != NotFound\n    {\n        return Err(Error::from(e))\n            .with_context(|| format!(\"failed to unlink {}\", path.as_ref().display()));\n    }\n\n    write(&path, contents)?;\n    #[cfg(unix)]\n    set_permissions(&path, Permissions::from_mode(0o755))?;\n    Ok(())\n}\n\npub fn getprop(prop: &str) -> Option<String> {\n    android_properties::getprop(prop).value()\n}\n\npub fn is_safe_mode() -> bool {\n    let safemode = getprop(\"persist.sys.safemode\")\n        .filter(|prop| prop == \"1\")\n        .is_some()\n        || getprop(\"ro.sys.safemode\")\n            .filter(|prop| prop == \"1\")\n            .is_some();\n    log::info!(\"safemode: {safemode}\");\n    if safemode {\n        return true;\n    }\n    let safemode = ksucalls::check_kernel_safemode();\n    log::info!(\"kernel_safemode: {safemode}\");\n    safemode\n}\n\npub fn get_zip_uncompressed_size(zip_path: &str) -> Result<u64> {\n    let mut zip = zip::ZipArchive::new(std::fs::File::open(zip_path)?)?;\n    let total: u64 = (0..zip.len())\n        .map(|i| zip.by_index(i).unwrap().size())\n        .sum();\n    Ok(total)\n}\n\npub fn switch_mnt_ns(pid: i32) -> Result<()> {\n    use rustix::{\n        fd::AsFd,\n        fs::{Mode, OFlags, open},\n    };\n    let path = format!(\"/proc/{pid}/ns/mnt\");\n    let fd = open(path, OFlags::RDONLY, Mode::from_raw_mode(0))?;\n    let current_dir = std::env::current_dir();\n    move_into_link_name_space(fd.as_fd(), Some(LinkNameSpaceType::Mount))?;\n    if let std::result::Result::Ok(current_dir) = current_dir {\n        let _ = std::env::set_current_dir(current_dir);\n    }\n    Ok(())\n}\n\nfn switch_cgroup(grp: &str, pid: u32) {\n    let path = Path::new(grp).join(\"cgroup.procs\");\n    if !path.exists() {\n        return;\n    }\n\n    let fp = OpenOptions::new().append(true).open(path);\n    if let std::result::Result::Ok(mut fp) = fp {\n        let _ = write!(fp, \"{pid}\");\n    }\n}\n\npub fn switch_cgroups() {\n    let pid = std::process::id();\n    switch_cgroup(\"/acct\", pid);\n    switch_cgroup(\"/dev/cg2_bpf\", pid);\n    switch_cgroup(\"/sys/fs/cgroup\", pid);\n\n    if getprop(\"ro.config.per_app_memcg\")\n        .filter(|prop| prop == \"false\")\n        .is_none()\n    {\n        switch_cgroup(\"/dev/memcg/apps\", pid);\n    }\n}\n\npub fn umask(mask: u32) {\n    process::umask(rustix::fs::Mode::from_raw_mode(mask));\n}\n\npub fn has_magisk() -> bool {\n    which::which(\"magisk\").is_ok()\n}\n\nfn link_ksud_to_bin() -> Result<()> {\n    let ksu_bin = PathBuf::from(defs::DAEMON_PATH);\n    let ksu_bin_link = PathBuf::from(defs::DAEMON_LINK_PATH);\n    if ksu_bin.exists() && !ksu_bin_link.exists() {\n        std::os::unix::fs::symlink(&ksu_bin, &ksu_bin_link)?;\n    }\n    Ok(())\n}\n\npub fn install(magiskboot: Option<PathBuf>) -> Result<()> {\n    ensure_dir_exists(defs::ADB_DIR)?;\n    std::fs::copy(\n        std::env::current_exe().with_context(|| \"Failed to get self exe path\")?,\n        defs::DAEMON_PATH,\n    )?;\n    restorecon::lsetfilecon(defs::DAEMON_PATH, restorecon::ADB_CON)?;\n    // install binary assets\n    assets::ensure_binaries(false).with_context(|| \"Failed to extract assets\")?;\n\n    link_ksud_to_bin()?;\n\n    if let Some(magiskboot) = magiskboot {\n        ensure_dir_exists(defs::BINARY_DIR)?;\n        let _ = std::fs::copy(magiskboot, defs::MAGISKBOOT_PATH);\n    }\n\n    Ok(())\n}\n\npub fn uninstall(magiskboot_path: Option<PathBuf>) -> Result<()> {\n    if Path::new(defs::MODULE_DIR).exists() {\n        println!(\"- Uninstall modules..\");\n        module::uninstall_all_modules()?;\n        module::prune_modules()?;\n    }\n    println!(\"- Removing directories..\");\n    std::fs::remove_dir_all(defs::WORKING_DIR).ok();\n    std::fs::remove_file(defs::DAEMON_PATH).ok();\n    std::fs::remove_dir_all(defs::MODULE_DIR).ok();\n    println!(\"- Restore boot image..\");\n    boot_patch::restore(BootRestoreArgs {\n        boot: None,\n        flash: true,\n        magiskboot: magiskboot_path,\n        out: None,\n        out_name: None,\n    })?;\n    println!(\"- Uninstall KernelSU manager..\");\n    Command::new(\"pm\")\n        .args([\"uninstall\", \"me.weishu.kernelsu\"])\n        .spawn()?;\n    println!(\"- Rebooting in 5 seconds..\");\n    std::thread::sleep(std::time::Duration::from_secs(5));\n    Command::new(\"reboot\").spawn()?;\n    Ok(())\n}\n"
  },
  {
    "path": "userspace/ksuinit/.gitignore",
    "content": "/target\n/.idea\n.cargo\n"
  },
  {
    "path": "userspace/ksuinit/Cargo.toml",
    "content": "[package]\nname = \"ksuinit\"\nversion = \"2.0.0\"\nedition = \"2024\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\n\n# for elf parsing\ngoblin = \"0.10\"\nscroll = \"0.13\"\n\nanyhow = \"1\"\nrustix = { git = \"https://github.com/Kernel-SU/rustix.git\", rev = \"4a53fbc\", features = [\"mount\", \"fs\", \"runtime\", \"system\", \"process\"] }\n\nsyscalls = { version = \"0.7\", default-features = false, features = [\n    \"aarch64\",\n    \"x86_64\",\n] }\n\n# for kmsg logging\nlog = \"0.4\"\nkernlog = { git = \"https://github.com/kstep/kernlog.rs\" }\n\n[profile.release]\nstrip = true\nlto = true\nopt-level = \"z\"\npanic = \"abort\"\n"
  },
  {
    "path": "userspace/ksuinit/build.rs",
    "content": "fn main() {\n    // Fix getauxval linking issue for aarch64-unknown-linux-musl\n    // The compiler_builtins crate needs getauxval from libc, but due to link order\n    // issues, we need to link libc again at the end\n    let target = std::env::var(\"TARGET\").unwrap();\n\n    if target == \"aarch64-unknown-linux-musl\" || target == \"x86_64-unknown-linux-musl\" {\n        // Link libc at the end to resolve symbols from compiler_builtins\n        println!(\"cargo:rustc-link-arg=-lc\");\n    }\n}\n"
  },
  {
    "path": "userspace/ksuinit/src/init.rs",
    "content": "use std::io::{ErrorKind, Write};\n\nuse anyhow::{Context, Result};\nuse rustix::fs::{Mode, symlink, unlink};\nuse rustix::{\n    fd::AsFd,\n    fs::{Access, CWD, FileType, access, makedev, mkdir, mknodat},\n    mount::{\n        FsMountFlags, FsOpenFlags, MountAttrFlags, MoveMountFlags, UnmountFlags, fsconfig_create,\n        fsmount, fsopen, move_mount, unmount,\n    },\n};\n\nstruct AutoUmount {\n    mountpoints: Vec<String>,\n}\n\nimpl Drop for AutoUmount {\n    fn drop(&mut self) {\n        for mountpoint in self.mountpoints.iter().rev() {\n            if let Err(e) = unmount(mountpoint.as_str(), UnmountFlags::DETACH) {\n                log::error!(\"Cannot umount {}: {}\", mountpoint, e)\n            }\n        }\n    }\n}\n\nfn mount_filesystem(name: &str, mountpoint: &str) -> Result<()> {\n    mkdir(mountpoint, Mode::from_raw_mode(0o755)).or_else(|err| match err.kind() {\n        ErrorKind::AlreadyExists => Ok(()),\n        _ => Err(err),\n    })?;\n    let fs_fd = fsopen(name, FsOpenFlags::FSOPEN_CLOEXEC)?;\n    fsconfig_create(fs_fd.as_fd())?;\n    let mount_fd = fsmount(\n        fs_fd.as_fd(),\n        FsMountFlags::FSMOUNT_CLOEXEC,\n        MountAttrFlags::empty(),\n    )?;\n    move_mount(\n        mount_fd.as_fd(),\n        \"\",\n        CWD,\n        mountpoint,\n        MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH,\n    )?;\n    Ok(())\n}\n\nfn prepare_mount() -> AutoUmount {\n    let mut mountpoints = vec![];\n\n    // mount procfs\n    match mount_filesystem(\"proc\", \"/proc\") {\n        Ok(_) => mountpoints.push(\"/proc\".to_string()),\n        Err(e) => log::error!(\"Cannot mount procfs: {:?}\", e),\n    }\n\n    // mount sysfs\n    match mount_filesystem(\"sysfs\", \"/sys\") {\n        Ok(_) => mountpoints.push(\"/sys\".to_string()),\n        Err(e) => log::error!(\"Cannot mount sysfs: {:?}\", e),\n    }\n\n    AutoUmount { mountpoints }\n}\n\nfn setup_kmsg() {\n    const KMSG: &str = \"/dev/kmsg\";\n    let device = match access(KMSG, Access::EXISTS) {\n        Ok(_) => KMSG,\n        Err(_) => {\n            // try to create it\n            mknodat(\n                CWD,\n                \"/kmsg\",\n                FileType::CharacterDevice,\n                0o666.into(),\n                makedev(1, 11),\n            )\n            .ok();\n            \"/kmsg\"\n        }\n    };\n\n    let _ = kernlog::init_with_device(device);\n}\n\nfn unlimit_kmsg() {\n    // Disable kmsg rate limiting\n    if let Ok(mut rate) = std::fs::File::options()\n        .write(true)\n        .open(\"/proc/sys/kernel/printk_devkmsg\")\n    {\n        writeln!(rate, \"on\").ok();\n    }\n}\n\npub fn init() -> Result<()> {\n    // Setup kernel log first\n    setup_kmsg();\n\n    log::info!(\"Hello, KernelSU!\");\n\n    // mount /proc and /sys to access kernel interface\n    let _dontdrop = prepare_mount();\n\n    // This relies on the fact that we have /proc mounted\n    unlimit_kmsg();\n\n    if ksuinit::has_kernelsu() {\n        log::info!(\"KernelSU may be already loaded in kernel, skip!\");\n    } else {\n        log::info!(\"Loading kernelsu.ko..\");\n        if let Err(e) = load_module_from_path(\"/kernelsu.ko\") {\n            log::error!(\"Cannot load kernelsu.ko: {:?}\", e);\n        }\n    }\n\n    // And now we should prepare the real init to transfer control to it\n    unlink(\"/init\")?;\n\n    let real_init = match access(\"/init.real\", Access::EXISTS) {\n        Ok(_) => \"init.real\",\n        Err(_) => \"/system/bin/init\",\n    };\n\n    log::info!(\"init is {}\", real_init);\n    symlink(real_init, \"/init\")?;\n\n    Ok(())\n}\n\nfn load_module_from_path(path: &str) -> Result<()> {\n    anyhow::ensure!(rustix::process::getpid().is_init(), \"Invalid process\");\n    let buffer = std::fs::read(path).with_context(|| format!(\"Cannot read file {}\", path))?;\n    ksuinit::load_module(&buffer)\n}\n"
  },
  {
    "path": "userspace/ksuinit/src/lib.rs",
    "content": "use anyhow::{Context, Result};\nuse goblin::elf::{Elf, section_header, sym::Sym};\nuse rustix::{cstr, system::init_module};\nuse scroll::{Pwrite, ctx::SizeWith};\nuse std::collections::HashMap;\nuse std::fs;\n\nstruct Kptr {\n    value: String,\n}\n\nimpl Kptr {\n    pub fn new() -> Result<Self> {\n        let value = fs::read_to_string(\"/proc/sys/kernel/kptr_restrict\")?;\n        fs::write(\"/proc/sys/kernel/kptr_restrict\", \"1\")?;\n        Ok(Kptr { value })\n    }\n}\n\nimpl Drop for Kptr {\n    fn drop(&mut self) {\n        let _ = fs::write(\"/proc/sys/kernel/kptr_restrict\", self.value.as_bytes());\n    }\n}\n\nfn parse_kallsyms() -> Result<HashMap<String, u64>> {\n    let _dontdrop = Kptr::new()?;\n\n    let allsyms = fs::read_to_string(\"/proc/kallsyms\")?\n        .lines()\n        .map(|line| line.split_whitespace())\n        .filter_map(|mut splits| {\n            splits\n                .next()\n                .and_then(|addr| u64::from_str_radix(addr, 16).ok())\n                .and_then(|addr| splits.nth(1).map(|symbol| (symbol, addr)))\n        })\n        .map(|(symbol, addr)| {\n            (\n                symbol\n                    .find(\"$\")\n                    .or_else(|| symbol.find(\".llvm.\"))\n                    .map_or(symbol, |pos| &symbol[0..pos])\n                    .to_owned(),\n                addr,\n            )\n        })\n        .collect::<HashMap<_, _>>();\n\n    Ok(allsyms)\n}\n\n/// Relocate undefined symbols in an ELF kernel module buffer using /proc/kallsyms,\n/// then load it via init_module syscall.\npub fn load_module(data: &[u8]) -> Result<()> {\n    let mut buffer = data.to_vec();\n    let elf = Elf::parse(&buffer)?;\n\n    let kernel_symbols = parse_kallsyms().context(\"Cannot parse kallsyms\")?;\n\n    let mut modifications = Vec::new();\n    for (index, mut sym) in elf.syms.iter().enumerate() {\n        if index == 0 {\n            continue;\n        }\n\n        if sym.st_shndx != section_header::SHN_UNDEF as usize {\n            continue;\n        }\n\n        let Some(name) = elf.strtab.get_at(sym.st_name) else {\n            continue;\n        };\n\n        let offset = elf.syms.offset() + index * Sym::size_with(elf.syms.ctx());\n        let Some(real_addr) = kernel_symbols.get(name) else {\n            log::warn!(\"Cannot find symbol: {}\", &name);\n            continue;\n        };\n        sym.st_shndx = section_header::SHN_ABS as usize;\n        sym.st_value = *real_addr;\n        modifications.push((sym, offset));\n    }\n\n    let ctx = *elf.syms.ctx();\n    for ele in modifications {\n        buffer.pwrite_with(ele.0, ele.1, ctx)?;\n    }\n    let param = if fs::exists(\"/ksu_allow_shell\").unwrap_or(false) {\n        log::warn!(\"ksu allow shell at init!\");\n        cstr!(\"allow_shell=1\")\n    } else {\n        cstr!(\"\")\n    };\n    init_module(&buffer, param).context(\"init_module failed.\")?;\n    Ok(())\n}\n\nfn has_kernelsu_legacy() -> bool {\n    use syscalls::{Sysno, syscall};\n    let mut version = 0;\n    const CMD_GET_VERSION: i32 = 2;\n    unsafe {\n        let _ = syscall!(\n            Sysno::prctl,\n            0xDEADBEEF,\n            CMD_GET_VERSION,\n            std::ptr::addr_of_mut!(version)\n        );\n    }\n\n    log::info!(\"KernelSU version: {}\", version);\n\n    version != 0\n}\n\nfn has_kernelsu_v2() -> bool {\n    use syscalls::{Sysno, syscall};\n    const KSU_INSTALL_MAGIC1: u32 = 0xDEADBEEF;\n    const KSU_INSTALL_MAGIC2: u32 = 0xCAFEBABE;\n    const KSU_IOCTL_GET_INFO: u32 = 0x80004b02; // _IOC(_IOC_READ, 'K', 2, 0)\n\n    #[repr(C)]\n    #[derive(Default)]\n    struct GetInfoCmd {\n        version: u32,\n        flags: u32,\n        features: u32,\n    }\n\n    // Try new method: get driver fd using reboot syscall with magic numbers\n    let mut fd: i32 = -1;\n    unsafe {\n        let _ = syscall!(\n            Sysno::reboot,\n            KSU_INSTALL_MAGIC1,\n            KSU_INSTALL_MAGIC2,\n            0,\n            std::ptr::addr_of_mut!(fd)\n        );\n    }\n\n    let version = if fd >= 0 {\n        // New method: try to get version info via ioctl\n        let mut cmd = GetInfoCmd::default();\n        let version = unsafe {\n            let ret = syscall!(Sysno::ioctl, fd, KSU_IOCTL_GET_INFO, &mut cmd as *mut _);\n\n            match ret {\n                Ok(_) => cmd.version,\n                Err(_) => 0,\n            }\n        };\n\n        unsafe {\n            let _ = syscall!(Sysno::close, fd);\n        }\n\n        version\n    } else {\n        0\n    };\n\n    log::info!(\"KernelSU version: {}\", version);\n\n    version != 0\n}\n\npub fn has_kernelsu() -> bool {\n    has_kernelsu_v2() || has_kernelsu_legacy()\n}\n"
  },
  {
    "path": "userspace/ksuinit/src/main.rs",
    "content": "#![no_main]\n\nmod init;\n\nuse rustix::{cstr, runtime::execve};\n/// # Safety\n/// This is the entry point of the program\n/// We cannot use the main because rust will abort if we don't have std{in/out/err}\n/// https://github.com/rust-lang/rust/blob/3071aefdb2821439e2e6f592f41a4d28e40c1e79/library/std/src/sys/unix/mod.rs#L80\n/// So we use the C main function and call rust code from there\n#[unsafe(no_mangle)]\npub unsafe extern \"C\" fn main(_argc: i32, argv: *const *const u8, envp: *const *const u8) -> i32 {\n    let _ = init::init();\n    unsafe {\n        execve(cstr!(\"/init\"), argv, envp);\n    }\n    0\n}\n"
  },
  {
    "path": "website/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n.pnpm-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# Snowpack dependency directory (https://snowpack.dev/)\nweb_modules/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Optional stylelint cache\n.stylelintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variable files\n.env\n.env.development.local\n.env.test.local\n.env.production.local\n.env.local\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n.parcel-cache\n\n# Next.js build output\n.next\nout\n\n# Nuxt.js build / generate output\n.nuxt\ndist\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and not Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# vuepress v2.x temp and cache directory\n.temp\n.cache\n\n# Docusaurus cache and generated files\n.docusaurus\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n\n# Stores VSCode versions used for testing VSCode extensions\n.vscode-test\n\n# yarn v2\n.yarn/cache\n.yarn/unplugged\n.yarn/build-state.yml\n.yarn/install-state.gz\n.pnp.*\n\n# from: https://github.com/vuejs/vitepress/blob/main/.gitignore\n/coverage\n/src/client/shared.ts\n/src/node/shared.ts\n*.log\n*.tgz\n.DS_Store\n.idea\n.temp\n.vite_opt_cache\n.vscode\ndist\ncache\nexamples-temp\nnode_modules\npnpm-global\nTODOs.md\n\ncache"
  },
  {
    "path": "website/docs/.vitepress/config.ts",
    "content": "import { defineConfig, SiteConfig } from 'vitepress'\nimport locales from './locales'\nimport { readdir, writeFile } from 'fs/promises'\nimport { resolve } from 'path'\n\nexport default defineConfig( {\n    title: 'KernelSU',\n    locales: locales.locales,\n    head: [\n        ['script', {\n            async: 'async',\n            src: 'https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8356785667482909',\n            crossorigin: 'anonymous',\n        }],\n    ],\n    sitemap: {\n        hostname: 'https://kernelsu.org'\n    },\n    buildEnd: async (config: SiteConfig) => {\n        const templateDir = resolve(config.outDir, 'templates');\n        const templateList = resolve(templateDir, \"index.json\");\n        let files = [];\n        try {\n            files = await readdir(templateDir);\n            files = files.filter(file => !file.startsWith('.'));\n        } catch(e) {\n            // ignore\n        }\n        await writeFile(templateList, JSON.stringify(files));\n    }\n})\n"
  },
  {
    "path": "website/docs/.vitepress/locales/en.ts",
    "content": "import { createRequire } from 'module'\nimport { defineConfig } from 'vitepress'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('vitepress/package.json')\n\nexport default defineConfig({\n  lang: 'en-US',\n  description: 'A kernel-based root solution for Android GKI devices.',\n\n  themeConfig: {\n    nav: nav(),\n\n    lastUpdatedText: 'Last updated',\n\n    sidebar: {\n      '/guide/': sidebarGuide()\n    },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/tiann/KernelSU' }\n    ],\n\n    footer: {\n        message: 'Released under the GPL3 License.',\n        copyright: 'Copyright © 2022-present KernelSU developers.'\n    },\n\n    editLink: {\n        pattern: 'https://github.com/tiann/KernelSU/edit/main/website/docs/:path',\n        text: 'Edit this page on GitHub'\n    }\n  }\n})\n\nfunction nav() {\n  return [\n    { text: 'Guide', link: '/guide/what-is-kernelsu' },\n  ]\n}\n\nfunction sidebarGuide() {\n  return [\n    {\n        text: 'Guide',\n        items: [\n          { text: 'What is KernelSU?', link: '/guide/what-is-kernelsu' },\n          { text: 'Difference with Magisk', link: '/guide/difference-with-magisk' },\n          { text: 'Installation', link: '/guide/installation' },\n          { text: 'How to build', link: '/guide/how-to-build' },\n          { text: 'Intergrate for non-GKI devices', link: '/guide/how-to-integrate-for-non-gki'},\n          { text: 'Unofficially supported devices', link: '/guide/unofficially-support-devices.md' },\n          { text: 'Module guide', link: '/guide/module.md' },\n          { text: 'Metamodule', link: '/guide/metamodule.md' },\n          { text: 'Module WebUI', link: '/guide/module-webui.md' },\n          { text: 'Module Configuration', link: '/guide/module-config.md' },\n          { text: 'App Profile', link: '/guide/app-profile.md' },\n          { text: 'Rescue from bootloop', link: '/guide/rescue-from-bootloop.md' },\n          { text: 'FAQ', link: '/guide/faq' },\n          { text: 'Hidden features', link: '/guide/hidden-features' },\n        ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/docs/.vitepress/locales/id_ID.ts",
    "content": "import { createRequire } from 'module'\nimport { defineConfig } from 'vitepress'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('vitepress/package.json')\n\nexport default defineConfig({\n  lang: 'id-ID',\n  description: 'Solusi root kernel-based untuk perangkat Android GKI.',\n\n  themeConfig: {\n    nav: nav(),\n\n    lastUpdatedText: 'Update Terakhir',\n\n    sidebar: {\n      '/id_ID/guide/': sidebarGuide()\n    },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/tiann/KernelSU' }\n    ],\n\n    footer: {\n        message: 'Rilis Dibawah Lisensi GPL3.',\n        copyright: 'Copyright © 2022-Sekarang pengembang KernelSU.'\n    },\n\n    editLink: {\n        pattern: 'https://github.com/tiann/KernelSU/edit/main/website/docs/:path',\n        text: 'Edit Halaman ini di GitHub'\n    }\n  }\n})\n\nfunction nav() {\n  return [\n    { text: 'Petunjuk', link: '/id_ID/guide/what-is-kernelsu' },\n  ]\n}\n\nfunction sidebarGuide() {\n  return [\n    {\n        text: 'Petunjuk',\n        items: [\n          { text: 'Apa itu KernelSU?', link: '/id_ID/guide/what-is-kernelsu' },\n          { text: 'Instalasi', link: '/id_ID/guide/installation' },\n          { text: 'Bagaimana cara buildnya?', link: '/id_ID/guide/how-to-build' },\n          { text: 'Integrasi untuk perangkat non-GKI', link: '/id_ID/guide/how-to-integrate-for-non-gki'},\n          { text: 'Perangkat yang didukung secara tidak resmi', link: '/id_ID/guide/unofficially-support-devices.md' },\n          { text: 'Petunjuk module', link: '/id_ID/guide/module.md' },\n          { text: 'Metamodule', link: '/id_ID/guide/metamodule.md' },\n          { text: 'Konfigurasi Modul', link: '/id_ID/guide/module-config.md' },\n          { text: 'Antisipasi dari bootloop', link: '/id_ID/guide/rescue-from-bootloop.md' },\n          { text: 'FAQ', link: '/id_ID/guide/faq' },\n        ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/docs/.vitepress/locales/index.ts",
    "content": "import { defineConfig } from 'vitepress'\nimport en from './en'\nimport zh_CN from './zh_CN'\nimport zh_TW from './zh_TW'\nimport vi_VN from './vi_VN'\nimport id_ID from './id_ID'\nimport ja_JP from './ja_JP'\nimport ru_RU from './ru_RU'\nimport pt_BR from './pt_BR'\n\nexport default defineConfig({\n  locales: {\n    root: {\n      label: 'English',\n      lang: en.lang,\n      themeConfig: en.themeConfig,\n      description: en.description\n    },\n    zh_CN: {\n      label: '简体中文',\n      lang: zh_CN.lang,\n      themeConfig: zh_CN.themeConfig,\n      description: zh_CN.description\n    },\n    zh_TW: {\n      label: '繁體中文',\n      lang: zh_TW.lang,\n      themeConfig: zh_TW.themeConfig,\n      description: zh_TW.description\n    },\n    ja_JP: {\n      label: '日本語',\n      lang: ja_JP.lang,\n      themeConfig: ja_JP.themeConfig,\n      description: ja_JP.description\n    },\n    vi_VN: {\n      label: 'Tiếng Việt',\n      lang: vi_VN.lang,\n      themeConfig: vi_VN.themeConfig,\n      description: vi_VN.description\n    },\n    id_ID: {\n      label: 'Bahasa',\n      lang: id_ID.lang,\n      themeConfig: id_ID.themeConfig,\n      description: id_ID.description\n    },\n    ru_RU: {\n      label: 'Русский',\n      lang: ru_RU.lang,\n      themeConfig: ru_RU.themeConfig,\n      description: ru_RU.description\n    },\n    pt_BR: {\n      label: 'Português (Brasil)',\n      lang: pt_BR.lang,\n      themeConfig: pt_BR.themeConfig,\n      description: pt_BR.description\n    }\n  }\n})\n"
  },
  {
    "path": "website/docs/.vitepress/locales/ja_JP.ts",
    "content": "import { createRequire } from 'module'\nimport { defineConfig } from 'vitepress'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('vitepress/package.json')\n\nexport default defineConfig({\n  lang: 'ja-JP',\n  description: 'Android GKI デバイス向けのカーネルベースの root ソリューション',\n\n  themeConfig: {\n    nav: nav(),\n\n    lastUpdatedText: '最終更新',\n\n    sidebar: {\n      '/ja_JP/guide/': sidebarGuide()\n    },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/tiann/KernelSU' }\n    ],\n\n    footer: {\n      message: 'GPL3 ライセンスでリリースされています。',\n      copyright: 'Copyright © 2022-現在 KernelSU 開発者。'\n    },\n\n    editLink: {\n      pattern: 'https://github.com/tiann/KernelSU/edit/main/website/docs/:path',\n      text: 'GitHub でこのページを編集'\n    }\n  }\n})\n\nfunction nav() {\n  return [\n    { text: 'ガイド', link: '/ja_JP/guide/what-is-kernelsu' },\n  ]\n}\n\nfunction sidebarGuide() {\n  return [\n    {\n      text: 'ガイド',\n      items: [\n        { text: 'KernelSU とは?', link: '/ja_JP/guide/what-is-kernelsu' },\n        { text: 'インストール', link: '/ja_JP/guide/installation' },\n        { text: 'ビルドするには?', link: '/ja_JP/guide/how-to-build' },\n        { text: '非 GKI デバイスでの実装', link: '/ja_JP/guide/how-to-integrate-for-non-gki' },\n        { text: '非公式の対応デバイス', link: '/ja_JP/guide/unofficially-support-devices.md' },\n        { text: 'モジュールのガイド', link: '/ja_JP/guide/module.md' },\n        { text: 'メタモジュール', link: '/ja_JP/guide/metamodule.md' },\n        { text: 'モジュール設定', link: '/ja_JP/guide/module-config.md' },\n        { text: 'ブートループからの復旧', link: '/ja_JP/guide/rescue-from-bootloop.md' },\n        { text: 'よくある質問', link: '/ja_JP/guide/faq' },\n        { text: '隠し機能', link: '/ja_JP/guide/hidden-features' },\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/docs/.vitepress/locales/pt_BR.ts",
    "content": "import { createRequire } from 'module'\nimport { defineConfig } from 'vitepress'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('vitepress/package.json')\n\nexport default defineConfig({\n  lang: 'pt-BR',\n  description: 'Uma solução root baseada em kernel para dispositivos Android GKI.',\n\n  themeConfig: {\n    nav: nav(),\n\n    lastUpdatedText: 'Última atualização',\n\n    sidebar: {\n      '/pt_BR/guide/': sidebarGuide()\n    },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/tiann/KernelSU' }\n    ],\n\n    footer: {\n        message: 'Lançado sob a Licença GPL3',\n        copyright: 'Copyright © 2022-presente Desenvolvedores do KernelSU.'\n    },\n\n    editLink: {\n        pattern: 'https://github.com/tiann/KernelSU/edit/main/website/docs/:path',\n        text: 'Edite esta página no GitHub'\n    }\n  }\n})\n\nfunction nav() {\n  return [\n    { text: 'Guia', link: '/pt_BR/guide/what-is-kernelsu' },\n  ]\n}\n\nfunction sidebarGuide() {\n  return [\n    {\n        text: 'Guia',\n        items: [\n          { text: 'O que é KernelSU?', link: '/pt_BR/guide/what-is-kernelsu' },\n          { text: 'Diferenças com Magisk', link: '/pt_BR/guide/difference-with-magisk' },\n          { text: 'Instalação', link: '/pt_BR/guide/installation' },\n          { text: 'Como compilar', link: '/pt_BR/guide/how-to-build' },\n          { text: 'Integração para dispositivos não-GKI', link: '/pt_BR/guide/how-to-integrate-for-non-gki'},\n          { text: 'Dispositivos com suporte não oficial', link: '/pt_BR/guide/unofficially-support-devices.md' },\n          { text: 'Guias de módulo', link: '/pt_BR/guide/module.md' },\n          { text: 'Metamódulo', link: '/pt_BR/guide/metamodule.md' },\n          { text: 'Módulo WebUI', link: '/pt_BR/guide/module-webui.md' },\n          { text: 'Configuração de Módulo', link: '/pt_BR/guide/module-config.md' },\n          { text: 'Perfil do Aplicativo', link: '/pt_BR/guide/app-profile.md' },\n          { text: 'Resgate do bootloop', link: '/pt_BR/guide/rescue-from-bootloop.md' },\n          { text: 'Perguntas frequentes', link: '/pt_BR/guide/faq' },\n          { text: 'Recursos ocultos', link: '/pt_BR/guide/hidden-features' },\n        ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/docs/.vitepress/locales/ru_RU.ts",
    "content": "import { createRequire } from 'module'\nimport { defineConfig } from 'vitepress'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('vitepress/package.json')\n\nexport default defineConfig({\n  lang: 'ru-RU',\n  description: 'Решение на основе ядра root для устройств Android GKI.',\n\n  themeConfig: {\n    nav: nav(),\n\n    lastUpdatedText: 'последнее обновление',\n\n    sidebar: {\n      '/ru_RU/guide/': sidebarGuide()\n    },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/tiann/KernelSU' }\n    ],\n\n    footer: {\n        message: 'Выпускается под лицензией GPL3.',\n        copyright: 'Авторские права © 2022-текущее Разработчики KernelSU.'\n    },\n\n    editLink: {\n        pattern: 'https://github.com/tiann/KernelSU/edit/main/website/docs/:path',\n        text: 'Редактировать эту страницу на GitHub'\n    }\n  }\n})\n\nfunction nav() {\n  return [\n    { text: 'Руководство', link: '/ru_RU/guide/what-is-kernelsu' },\n  ]\n}\n\nfunction sidebarGuide() {\n  return [\n    {\n        text: 'Руководство',\n        items: [\n          { text: 'Что такое KernelSU?', link: '/ru_RU/guide/what-is-kernelsu' },\n          { text: 'Установка', link: '/ru_RU/guide/installation' },\n          { text: 'Как собрать?', link: '/ru_RU/guide/how-to-build' },\n          { text: 'Реализация в устройствах, не относящихся к GKI', link: '/ru_RU/guide/how-to-integrate-for-non-gki'},\n          { text: 'Неофициально поддерживаемые устройства', link: '/ru_RU/guide/unofficially-support-devices.md' },\n          { text: 'Руководство по разработке модулей', link: '/ru_RU/guide/module.md' },\n          { text: 'Метамодуль', link: '/ru_RU/guide/metamodule.md' },\n          { text: 'Конфигурация модулей', link: '/ru_RU/guide/module-config.md' },\n          { text: 'Профиль приложений', link: '/ru_RU/guide/app-profile.md' },\n          { text: 'Выход из циклической загрузки', link: '/ru_RU/guide/rescue-from-bootloop.md' },\n          { text: 'FAQ', link: '/ru_RU/guide/faq' },\n          { text: 'Скрытые возможности', link: '/ru_RU/guide/hidden-features' },\n        ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/docs/.vitepress/locales/vi_VN.ts",
    "content": "import { createRequire } from 'module'\nimport { defineConfig } from 'vitepress'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('vitepress/package.json')\n\nexport default defineConfig({\n  lang: 'vi-VN',\n  description: 'Một giải pháp root trực tiếp trên kernel dành cho các thiết bị hỗ trợ GKI.',\n\n  themeConfig: {\n    nav: nav(),\n\n    lastUpdatedText: 'cập nhật lần cuối',\n\n    sidebar: {\n      '/vi_VN/guide/': sidebarGuide()\n    },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/tiann/KernelSU' }\n    ],\n\n    footer: {\n        message: 'Phát hành dưới giấy phép GPL3.',\n        copyright: 'Bản Quyền © 2022-nay KernelSU developers.'\n    },\n\n    editLink: {\n        pattern: 'https://github.com/tiann/KernelSU/edit/main/website/docs/:path',\n        text: 'Chỉnh sửa trang này trên GitHub'\n    }\n  }\n})\n\nfunction nav() {\n  return [\n    { text: 'Hướng Dẫn', link: '/vi_VN/guide/what-is-kernelsu' },\n  ]\n}\n\nfunction sidebarGuide() {\n  return [\n    {\n        text: 'Hướng Dẫn',\n        items: [\n          { text: 'KernelSU là gì?', link: '/vi_VN/guide/what-is-kernelsu' },\n          { text: 'Cách cài đặt', link: '/vi_VN/guide/installation' },\n          { text: 'Cách để build?', link: '/vi_VN/guide/how-to-build' },\n          { text: 'Tích hợp vào thiết bị không sử dụng GKI', link: '/vi_VN/guide/how-to-integrate-for-non-gki'},\n          { text: 'Thiết bị hỗ trợ không chính thức', link: '/vi_VN/guide/unofficially-support-devices.md' },\n          { text: 'Metamodule', link: '/vi_VN/guide/metamodule.md' },\n          { text: 'Cấu hình module', link: '/vi_VN/guide/module-config.md' },\n          { text: 'FAQ - Câu hỏi thường gặp', link: '/vi_VN/guide/faq' },\n        ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/docs/.vitepress/locales/zh_CN.ts",
    "content": "import { createRequire } from 'module'\nimport { defineConfig } from 'vitepress'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('vitepress/package.json')\n\nexport default defineConfig({\n  lang: 'zh-CN',\n  description: '一个基于内核，为安卓 GKI 准备的 root 方案。',\n\n  themeConfig: {\n    nav: nav(),\n\n    lastUpdatedText: '最后更新',\n\n    sidebar: {\n      '/zh_CN/guide/': sidebarGuide()\n    },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/tiann/KernelSU' }\n    ],\n\n    footer: {\n        message: '在 GPL3 许可证下发布。',\n        copyright: 'Copyright © 2022-现在 KernelSU 开发者。'\n    },\n\n    editLink: {\n        pattern: 'https://github.com/tiann/KernelSU/edit/main/website/docs/:path',\n        text: '在 GitHub 中编辑本页'\n    }\n  }\n})\n\nfunction nav() {\n  return [\n    { text: '指南', link: '/zh_CN/guide/what-is-kernelsu' },\n  ]\n}\n\nfunction sidebarGuide() {\n  return [\n    {\n        text: 'Guide',\n        items: [\n          { text: '什么是 KernelSU？', link: '/zh_CN/guide/what-is-kernelsu' },\n          { text: 'KernelSU 模块与 Magisk 的差异', link: '/zh_CN/guide/difference-with-magisk' },\n          { text: '安装', link: '/zh_CN/guide/installation' },\n          { text: '如何构建？', link: '/zh_CN/guide/how-to-build' },\n          { text: '如何为非 GKI 设备集成 KernelSU', link: '/zh_CN/guide/how-to-integrate-for-non-gki'},\n          { text: '非官方支持设备', link: '/zh_CN/guide/unofficially-support-devices.md' },\n          { text: '模块开发指南', link: '/zh_CN/guide/module.md' },\n          { text: '元模块', link: '/zh_CN/guide/metamodule.md' },\n          { text: '模块 Web 界面', link: '/zh_CN/guide/module-webui.md' },\n          { text: '模块配置', link: '/zh_CN/guide/module-config.md' },\n          { text: 'App Profile', link: '/zh_CN/guide/app-profile.md' },\n          { text: '救砖', link: '/zh_CN/guide/rescue-from-bootloop.md' },\n          { text: '常见问题', link: '/zh_CN/guide/faq' },\n          { text: '隐藏功能', link: '/zh_CN/guide/hidden-features' },\n        ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/docs/.vitepress/locales/zh_TW.ts",
    "content": "import { createRequire } from 'module'\nimport { defineConfig } from 'vitepress'\n\nconst require = createRequire(import.meta.url)\nconst pkg = require('vitepress/package.json')\n\nexport default defineConfig({\n  lang: 'zh-TW',\n  description: '一個基於核心，適用於 Android GKI 的 Root 解決方案。',\n\n  themeConfig: {\n    nav: nav(),\n\n    lastUpdatedText: '上次更新',\n\n    sidebar: {\n      '/zh_TW/guide/': sidebarGuide()\n    },\n\n    socialLinks: [\n      { icon: 'github', link: 'https://github.com/tiann/KernelSU' }\n    ],\n\n    footer: {\n        message: '係依據 GPL3 授權發行。',\n        copyright: 'Copyright © 2022-目前 KernelSU 開發人員。'\n    },\n\n    editLink: {\n        pattern: 'https://github.com/tiann/KernelSU/edit/main/website/docs/:path',\n        text: '在 GitHub 中編輯本頁面'\n    }\n  }\n})\n\nfunction nav() {\n  return [\n    { text: '指南', link: '/zh_TW/guide/what-is-kernelsu' },\n  ]\n}\n\nfunction sidebarGuide() {\n  return [\n    {\n        text: 'Guide',\n        items: [\n          { text: '什麼是 KernelSU？', link: '/zh_TW/guide/what-is-kernelsu' },\n          { text: 'KernelSU 與 Magisk 的差異', link: '/zh_TW/guide/difference-with-magisk' },\n          { text: '安裝', link: '/zh_TW/guide/installation' },\n          { text: '如何建置？', link: '/zh_TW/guide/how-to-build' },\n          { text: '如何為非 GKI 核心整合 KernelSU', link: '/zh_TW/guide/how-to-integrate-for-non-gki'},\n          { text: '非官方支援裝置', link: '/zh_TW/guide/unofficially-support-devices.md' },\n          { text: '模組指南', link: '/zh_TW/guide/module.md' },\n          { text: '元模組', link: '/zh_TW/guide/metamodule.md' },\n          { text: '模組 WebUI', link: '/zh_TW/guide/module-webui.md' },\n          { text: '模組配置', link: '/zh_TW/guide/module-config.md' },\n          { text: 'App Profile', link: '/zh_TW/guide/app-profile.md' },\n          { text: '搶救開機迴圈', link: '/zh_TW/guide/rescue-from-bootloop.md' },\n          { text: '常見問題', link: '/zh_TW/guide/faq' },\n          { text: '隱藏功能', link: '/zh_TW/guide/hidden-features' },\n        ]\n    }\n  ]\n}\n"
  },
  {
    "path": "website/docs/guide/app-profile.md",
    "content": "# App Profile\n\nThe App Profile is a mechanism provided by KernelSU for customizing the configuration of various apps.\n\nFor apps granted root permissions (i.e., able to use `su`), the App Profile can also be referred to as the Root Profile. It allows customization of the `uid`, `gid`, `groups`, `capabilities`, and `SELinux` rules of the `su` command, thereby restricting the privileges of the root user. For example, it can grant network permissions only to firewall apps while denying file access permissions, or it can grant shell permissions instead of full root access for freeze apps: **keeping the power confined with the principle of least privilege.**\n\nFor ordinary apps without root permissions, the App Profile can control the behavior of the kernel and module system towards these apps. For instance, it can determine whether modifications resulting from modules should be addressed. The kernel and module system can make decisions based on this configuration, such as performing operations akin to \"hiding\".\n\n## Root Profile\n\n### UID, GID, and Groups\n\nLinux systems have two concepts: users and groups. Each user has a user ID (UID), and a user can belong to multiple groups, each with its own group ID (GID). These IDs are used to identify users in the system and determine which system resources they can access.\n\nUsers with a UID of 0 are known as root users, and groups with a GID of 0 are known as root groups. The root user group generally has the highest system privileges.\n\nIn the case of the Android system, each app functions as a separate user (except in cases of shared UIDs) with a unique UID. For example, `0` represents the root user, `1000` represents `system`, `2000` represents the ADB shell, and UIDs ranging from `10000` to `19999` represent ordinary apps.\n\n::: info\nHere, the UID mentioned isn't the same as the concept of multiple users or work profiles in the Android system. Work profiles are actually implemented by partitioning the UID range. For example, 10000-19999 represents the main user, while 110000-119999 represents a work profile. Each ordinary app among them has its own unique UID.\n:::\n\nEach app can have several groups, with the GID representing the primary group, which usually matches the UID. Other groups are known as supplementary groups. Certain permissions are controlled through groups, such as network access permissions or Bluetooth access.\n\nFor example, if we execute the `id` command in ADB shell, the output might look like this:\n\n```sh\noriole:/ $ id\nuid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),1078(ext_data_rw),1079(ext_obb_rw),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid),3012(readtracefs) context=u:r:shell:s0\n```\n\nHere, the UID is `2000`, and the GID (primary group ID) is also `2000`. Additionally, it belongs to several supplementary groups, such as `inet` (indicating the ability to create `AF_INET` and `AF_INET6` sockets) and `sdcard_rw` (indicating read/write permissions for the SD card).\n\nKernelSU's Root Profile allows customization of the UID, GID, and groups for the root process after executing `su`. For example, the Root Profile of a root app can set its UID to `2000`, which means that when using `su`, the app's actual permissions are at the ADB shell level. Additionally, the `inet` group can be removed, preventing the `su` command from accessing the network.\n\n::: tip NOTE\nThe App Profile only controls the permissions of the root process after using `su` and doesn't control the app's own permissions. If an app has requested network access permission, it can still access the network even without using `su`. Removing the `inet` group from `su` only prevents `su` from accessing the network.\n:::\n\nRoot Profile is enforced in the kernel and doesn't rely on the voluntary behavior of root apps, unlike switching users or groups through `su`. Granting `su` permissions is entirely controlled by the user, not the developer.\n\n### Capabilities\n\nCapabilities are a mechanism for privilege separation in Linux.\n\nFor the purpose of performing permission checks, traditional `UNIX` implementations distinguish two categories of processes: privileged processes (whose effective user ID is `0`, referred to as superuser or root) and unprivileged processes (whose effective UID is nonzero). Privileged processes bypass all kernel permission checks, while unprivileged processes are subject to full permission checking based on the process's credentials (usually: effective UID, effective GID, and supplementary group list).\n\nStarting with Linux 2.2, Linux divides the privileges traditionally associated with superuser into distinct units, known as capabilities, which can be independently enabled and disabled.\n\nEach capability represents one or more privileges. For example, `CAP_DAC_READ_SEARCH` represents the ability to bypass permission checks for file reading, as well as directory read and execute permissions. If a user with an effective UID of `0` (root user) doesn't have the `CAP_DAC_READ_SEARCH` capability or higher, this means that even as root, they cannot freely read files.\n\nKernelSU's Root Profile allows customization of the capabilities of the root process after executing `su`, thus granting partial \"root privileges\". Unlike the UID and GID mentioned above, certain root apps require a UID of `0` after using `su`. In such cases, limiting the capabilities of this root user with UID `0` can restrict the operations they're allowed to perform.\n\n::: tip STRONG RECOMMENDATION\nLinux's capability [official documentation](https://man7.org/linux/man-pages/man7/capabilities.7.html) provides detailed explanations of the abilities represented by each capability. If you intend to customize capabilities, it's strongly recommended that you read this document first.\n:::\n\n### SELinux\n\nSELinux is a powerful Mandatory Access Control (MAC) mechanism. It operates on the principle of **default deny**. Any action not explicitly allowed is denied.\n\nSELinux can run in two global modes:\n\n1. Permissive mode: Denial events are logged, but not enforced.\n2. Enforcing mode: Denial events are logged and enforced.\n\n::: warning\nModern Android systems heavily rely on SELinux to ensure overall system security. It's highly recommended not to use any custom systems running in \"Permissive mode\" since it provides no significant advantages over a completely open system.\n:::\n\nExplaining the full concept of SELinux is complex and beyond the scope of this document. It's recommended to first understand how it works through the following resources:\n\n1. [Wikipedia](https://en.wikipedia.org/wiki/Security-Enhanced_Linux)\n2. [Red Hat: What Is SELinux?](https://www.redhat.com/en/topics/linux/what-is-selinux)\n3. [ArchLinux: SELinux](https://wiki.archlinux.org/title/SELinux)\n\nKernelSU's Root Profile allows customization of the SELinux context of the root process after executing `su`. Specific access control rules can be set for this context, enabling fine-grained control over root permissions.\n\nIn typical scenarios, when an app executes `su`, it switches the process to a SELinux domain with **unrestricted access**, such as `u:r:su:s0`. Through the Root Profile, this domain can be switched to a custom domain, such as `u:r:app1:s0`, and a series of rules can be defined for this domain:\n\n```sh\ntype app1\nenforce app1\ntypeattribute app1 mlstrustedsubject\nallow app1 * * *\n```\n\nNote that the `allow app1 * * *` rule is used for demonstration purposes only. In practice, this rule shouldn't be used extensively, as it isn't much different from Permissive mode.\n\n### Escalation\n\nIf the configuration of the Root Profile isn't set properly, an escalation scenario may occur. The restrictions imposed by the Root Profile can unintentionally fail.\n\nFor example, if you grant root permission to an ADB shell user (which is a common case) and then grant root permission to a regular app, but configure its Root Profile with UID 2000 (which is the UID of the ADB shell user), the app can obtain full root access by executing the `su` command twice:\n\n1. The first execution of `su` will be subject to the App Profile and will switch to UID `2000` (ADB shell) instead of `0` (root).\n2. The second execution of `su`, since the UID is `2000` and root access has been granted to UID `2000` (ADB shell) in the configuration, the app will gain full root privileges.\n\n::: warning NOTE\nThis behavior is fully expected and isn't a bug. Therefore, we recommend the following:\n\nIf you genuinely need to grant root permissions to ADB (e.g., as a developer), it isn't advisable to change the UID to `2000` when configuring the Root Profile. Using `1000` (system) would be a better choice.\n:::\n\n## Non-root profile\n\n### Umount modules\n\nKernelSU provides a systemless mechanism to modify system partitions, achieved through the mounting of OverlayFS. However, some apps may be sensitive to this behavior. In this case, we can unload modules mounted in these apps by setting the \"Umount modules\" option.\n\nAdditionally, the KernelSU manager's settings interface provides the \"Umount modules by default\". By default, this option is **enabled**, which means that KernelSU or some modules will unload modules for this app unless additional settings are applied. If you don't prefer this setting or if it affects certain apps, you have the following options:\n\n1. Keep the \"Umount modules by default\" option enabled and individually disable the \"Umount modules\" option in the App Profile for apps requiring module loading (acting as a \"whitelist\").\n2. Disable the \"Umount modules by default\" option and individually enable the \"Umount modules\" option in the App Profile for apps requiring module loading (acting as a \"blacklist\").\n\n::: info\nIn devices running kernel version 5.10 and above, the kernel performs without any further action the unloading of modules. However, for devices running kernel versions below 5.10, this option is merely a configuration setting, and KernelSU itself doesn't take any action. If you want to use the \"Umount modules\" option in kernel versions before 5.10 you need to backport the `path_umount` function in `fs/namespace.c`. You can get more information at the end of the [Integrate for non-GKI devices](https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#how-to-backport-path_umount) page. Some modules, such as Zygisksu, may also use this option to determine if module unloading is necessary.\n:::\n"
  },
  {
    "path": "website/docs/guide/difference-with-magisk.md",
    "content": "# Difference with Magisk\n\nAlthough KernelSU and Magisk modules have many similarities, there are inevitably some differences due to their completely different implementation mechanisms. If you want your module to work on both Magisk and KernelSU, it's essential to understand these differences.\n\n## Similarities\n\n- Module file format: Both use the ZIP format to organize modules, and the module format is practically the same.\n- Module installation directory: Both are located at `/data/adb/modules`.\n- Systemless: Both support modifying `/system` in a systemless way through modules.\n- post-fs-data.sh: Execution time and semantics are exactly the same.\n- service.sh: Execution time and semantics are exactly the same.\n- system.prop: Completely the same.\n- sepolicy.rule: Completely the same.\n- BusyBox: Scripts are run in BusyBox with \"Standalone Mode\" enabled in both cases.\n\n## Differences\n\nBefore understanding the differences, it's important to know how to identify whether your module is running in KernelSU or Magisk. You can use the environment variable `KSU` to differentiate it in all places where you can run module scripts (`customize.sh`, `post-fs-data.sh`, `service.sh`). In KernelSU, this environment variable will be set to `true`.\n\nHere are some differences:\n\n- KernelSU modules cannot be installed in Recovery mode.\n- KernelSU modules don't have built-in support for Zygisk, but you can use Zygisk modules through [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext).\n- **Module mounting architecture**: KernelSU uses a [metamodule system](metamodule.md) where mounting is delegated to pluggable metamodules (e.g., `meta-overlayfs`), while Magisk has mounting built into its core. KernelSU requires installing a metamodule to enable module mounting.\n- The method for replacing or deleting files in KernelSU modules is completely different from Magisk. KernelSU doesn't support the `.replace` method. Instead, you need to create a same-named file with `mknod filename c 0 0` to delete the corresponding file.\n- The directories for BusyBox are different. The built-in BusyBox in KernelSU is located at `/data/adb/ksu/bin/busybox`, while in Magisk it is at `/data/adb/magisk/busybox`. **Note that this is an internal behavior of KernelSU and may change in the future!**\n- KernelSU doesn't support `.replace` files, but it supports the `REMOVE` and `REPLACE` variables to remove or replace files and folders.\n- KernelSU adds the `boot-completed` stage to run scripts after the boot process is finished.\n- KernelSU adds the `post-mount` stage to run scripts after module mounting is complete.\n"
  },
  {
    "path": "website/docs/guide/faq.md",
    "content": "# FAQ\n\n## Does KernelSU support my device?\n\nKernelSU supports devices running Android with an unlocked bootloader. However, official support is only for GKI Linux Kernels 5.10+ (in practice, this means your device needs to have Android 12 out-of-the-box to be supported).\n\nYou can easily check the support for your device through the KernelSU manager, which is available [here](https://github.com/tiann/KernelSU/releases). \n\nIf the app shows `Not installed`, it means your device is officially supported by KernelSU.\n\nIf the app shows `Unsupported`, it means your device isn't officially supported at present. However, you can build kernel source code and integrate KernelSU to make it work, or use [Unofficially supported devices](unofficially-support-devices).\n\n## Do I need to unlock the bootloader to use KernelSU?\n\nYes. KernelSU requires an unlocked bootloader.\n\n## Does KernelSU support modules?\n\nYes, most Magisk modules work on KernelSU. However, if your module needs to modify `/system` files, you need to install a [metamodule](metamodule.md) (such as `meta-overlayfs`). Other module features work without a metamodule. Check [Module guide](module.md) for more info.\n\n## Does KernelSU support Xposed?\n\nYes, you can use LSPosed (or other modern Xposed derivative) with [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext).\n\n## Does KernelSU support Zygisk?\n\nKernelSU has no built-in Zygisk support, but you can use a module like [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) to support it.\n\n## Is KernelSU compatible with Magisk?\n\nKernelSU's module system conflicts with Magisk's magic mount. If any module is enabled in KernelSU, Magisk will stop working entirely.\n\nHowever, if you only use the `su` of KernelSU, it will work well with Magisk. KernelSU modifies the `kernel`, while Magisk modifies the `ramdisk`, allowing both to work together.\n\n## Will KernelSU substitute Magisk?\n\nNo. Replacing Magisk isn't our goal. Magisk is already an excellent userspace root solution. KernelSU focuses on exposing kernel interfaces to users instead of supplanting Magisk.\n\n## Can KernelSU support non-GKI devices?\n\nIt's possible. But you should download the kernel source, integrate KernelSU into the source tree, and compile the kernel yourself.\n\n## Can KernelSU support devices below Android 12?\n\nIt's the device's kernel that affects KernelSU's compatibility, and it has nothing to do with the Android version. The only restriction is that devices launched with Android 12 must have a kernel version of 5.10+ (GKI devices). So:\n\n1. Devices launched with Android 12 must be supported.\n2. Devices with an older kernel (some devices with Android 12 also have the older kernel) are compatible (you should build kernel yourself).\n\n## Can KernelSU support old kernel?\n\nIt's possible. KernelSU is backported to kernel 4.14 now. For older kernels, you need to backport it manually, and PRs are always welcome!\n\n## How to integrate KernelSU for an older kernel?\n\nPlease check the [Integrate for non-GKI devices](how-to-integrate-for-non-gki) guide.\n\n## Why my Android version is 13, and the kernel shows \"android12-5.10\"?\n\nThe kernel version has nothing to do with the Android version. If you need to flash kernel, always use the kernel version; the Android version isn't as important.\n\n## I'm GKI 1.0, can I use this?\n\nGKI 1.0 is completely different from GKI 2.0, you must compile kernel by yourself.\n\n## How can I make `/system` RW?\n\nWe don't recommend that you modify the system partition directly. Please check [Module guide](module.md) to modify it systemlessly. If you insist on doing this, check [magisk_overlayfs](https://github.com/HuskyDG/magic_overlayfs).\n\n## Can KernelSU modify hosts? How can I use AdAway？\n\nOf course. But KernelSU doesn't have built-in hosts support, you can install a module like [systemless-hosts](https://github.com/symbuzzer/systemless-hosts-KernelSU-module) to do it.\n\n## Why aren't my modules working after fresh install?\n\nIf your modules need to modify `/system` files, you need to install a [metamodule](metamodule.md) to mount the `system` directory. Other module features (scripts, sepolicy, system.prop) work without a metamodule.\n\n**Solution**: See the [Metamodule Guide](metamodule.md) for installation instructions.\n\n## What is a metamodule and why do I need one?\n\nA metamodule is a special module that provides infrastructure for mounting regular modules. See the [Metamodule Guide](metamodule.md) for a complete explanation.\n"
  },
  {
    "path": "website/docs/guide/hidden-features.md",
    "content": "# Hidden features\n\n## .ksurc\n\nBy default, `/system/bin/sh` loads `/system/etc/mkshrc`.\n\nYou can make su load customized rc file by creating a `/data/adb/ksu/.ksurc` file.\n"
  },
  {
    "path": "website/docs/guide/how-to-build.md",
    "content": "# How to build\n\n::: warning\nThis document is for archival reference only and is no longer maintained.\nSince KernelSU v3.0, we have dropped official support for GKI image mode for faster iteration and build speed. It is recommended to use `Ylarod/ddk` to build LKM.\n:::\n\nFirst, you should read the official Android documentation for building kernels:\n\n1. [Build kernels](https://source.android.com/docs/setup/build/building-kernels)\n2. [GKI release builds](https://source.android.com/docs/core/architecture/kernel/gki-release-builds)\n\n::: warning\nThis page is for GKI devices; if you use an older kernel, please refer to [Integrate for non-GKI devices](how-to-integrate-for-non-gki).\n:::\n\n## Build kernel\n\n### Sync the kernel source code\n\n```sh\nrepo init -u https://android.googlesource.com/kernel/manifest\nmv <kernel_manifest.xml> .repo/manifests\nrepo init -m manifest.xml\nrepo sync\n```\n\nThe `<kernel_manifest.xml>` file is a manifest that uniquely identifies a build, allowing you to make it reproducible. To do this, you should download the manifest file from [GKI release builds](https://source.android.com/docs/core/architecture/kernel/gki-release-builds).\n\n### Build\n\nPlease check the [Building kernels](https://source.android.com/docs/setup/build/building-kernels) first.\n\nFor example, to build an `aarch64` kernel image:\n\n```sh\nLTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh\n```\n\nDon't forget to add the `LTO=thin` flag; otherwise, the build may fail if your computer has less than 24 GB of memory.\n\nStarting from Android 13, the kernel is built by `bazel`:\n\n```sh\ntools/bazel build --config=fast //common:kernel_aarch64_dist\n```\n\n::: info\nFor some Android 14 kernels, to make Wi-Fi/Bluetooth work, it might be necessary to remove all GKI protected exports:\n\n```sh\nrm common/android/abi_gki_protected_exports_*\n```\n:::\n\n## Build kernel with KernelSU\n\nIf you can successfully build the kernel, adding support for KernelSU will be relatively easy. In the root of kernel source directory, run any of the options listed below:\n\n::: code-group\n\n```sh[Latest tag (stable)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\n```sh[main branch (dev)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\n```\n\n```sh[Select tag (such as v0.5.2)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\n```\n\n:::\n\nThen, rebuild the kernel and you will get a kernel image with KernelSU!\n"
  },
  {
    "path": "website/docs/guide/how-to-integrate-for-non-gki.md",
    "content": "# Integrate for non-GKI devices\n\n::: warning\nThis document is for archival reference only and is no longer maintained.\nSince KernelSU v1.0, we have dropped official support for non-GKI devices.\n:::\n\nKernelSU can be integrated into non-GKI kernels and was backported to 4.14 and earlier versions.\n\nDue to the fragmentation of non-GKI kernels, we don't have a universal way to build them; therefore, we cannot provide a non-GKI boot.img. However, you can build the kernel with KernelSU integrated on your own.\n\nFirst, you should be able to build a bootable kernel from kernel source code. If the kernel isn't open source, then it is difficult to run KernelSU for your device.\n\nIf you're able to build a bootable kernel, there are two ways to integrate KernelSU into the kernel source code:\n\n1. Automatically with `kprobe`\n2. Manually\n\n## Integrate with kprobe\n\nKernelSU uses kprobe for its kernel hooks. If kprobe runs reliably on your kernel, we recommend integrating KernelSU this way.\n\nFirst, add KernelSU to your kernel source tree:\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.9.5\n```\n\n::: info\n[KernelSU 1.0 and later versions no longer support non-GKI kernels](https://github.com/tiann/KernelSU/issues/1705). The last supported version is `v0.9.5`, so make sure to use the correct version.\n:::\n\nThen, you should check if kprobe is enabled in your kernel config. If it isn't, add these configs to it:\n\n```txt\nCONFIG_KPROBES=y\nCONFIG_HAVE_KPROBES=y\nCONFIG_KPROBE_EVENTS=y\n```\n\nNow, when you re-build your kernel, KernelSU should work correctly.\n\nIf you find that KPROBES is still not enabled, you can try enabling `CONFIG_MODULES`. If that doesn't solve the issue, use `make menuconfig` to search for other KPROBES dependencies.\n\nHowever, if you encounter a bootloop after integrating KernelSU, this may indicate that the **kprobe is broken in your kernel**, which means that you should fix the kprobe bug or use another way.\n\n::: tip HOW TO CHECK IF KPROBE IS BROKEN？\nComment out `ksu_sucompat_init()` and `ksu_ksud_init()` in `KernelSU/kernel/ksu.c`. If the device boots normally, kprobe may be broken.\n:::\n\n::: info HOW TO GET MODULE UMOUNT FEATURE WORKING ON PRE-GKI?\nIf your kernel is older than 5.9, you should backport `path_umount` to `fs/namespace.c`. This is required to get \"Umount module\" feature work correctly. If you don't backport `path_umount`, \"Umount module\" feature won't work. You can get more info on how to achieve this at the end of this page.\n:::\n\n## Manually modify the kernel source\n\nIf kprobe doesn't work on your kernel—either because of an upstream bug or because your kernel is older than 4.8—you can try the following approach:\n\nFirst, add KernelSU to your kernel source tree:\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.9.5\n```\n\nKeep in mind that, on some devices, your defconfig may be located at `arch/arm64/configs` or in other cases, it may be at `arch/arm64/configs/vendor/your_defconfig`. Regardless of the defconfig you're using, make sure to enable `CONFIG_KSU` with `y` to enable or `n` to disable it. For example, if you choose to enable it, your defconfig should contain the following string:\n\n```txt\n# KernelSU\nCONFIG_KSU=y\n```\n\nNext, add KernelSU calls to the kernel source. Below are some patches for reference:\n\n::: code-group\n\n```diff[exec.c]\ndiff --git a/fs/exec.c b/fs/exec.c\nindex ac59664eaecf..bdd585e1d2cc 100644\n--- a/fs/exec.c\n+++ b/fs/exec.c\n@@ -1890,11 +1890,14 @@ static int __do_execve_file(int fd, struct filename *filename,\n \treturn retval;\n }\n\n+#ifdef CONFIG_KSU\n+extern bool ksu_execveat_hook __read_mostly;\n+extern int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv,\n+\t\t\tvoid *envp, int *flags);\n+extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,\n+\t\t\t\t void *argv, void *envp, int *flags);\n+#endif\n static int do_execveat_common(int fd, struct filename *filename,\n \t\t\t      struct user_arg_ptr argv,\n \t\t\t      struct user_arg_ptr envp,\n \t\t\t      int flags)\n {\n+   #ifdef CONFIG_KSU\n+\tif (unlikely(ksu_execveat_hook))\n+\t\tksu_handle_execveat(&fd, &filename, &argv, &envp, &flags);\n+\telse\n+\t\tksu_handle_execveat_sucompat(&fd, &filename, &argv, &envp, &flags);\n+   #endif\n \treturn __do_execve_file(fd, filename, argv, envp, flags, NULL);\n }\n```\n```diff[open.c]\ndiff --git a/fs/open.c b/fs/open.c\nindex 05036d819197..965b84d486b8 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -348,6 +348,8 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn ksys_fallocate(fd, mode, offset, len);\n }\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t int *flags);\n+#endif\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -355,6 +357,7 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n  */\n long do_faccessat(int dfd, const char __user *filename, int mode)\n {\n \tconst struct cred *old_cred;\n \tstruct cred *override_cred;\n \tstruct path path;\n \tstruct inode *inode;\n \tstruct vfsmount *mnt;\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n+   #ifdef CONFIG_KSU\n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n+   #endif\n \n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n```diff[read_write.c]\ndiff --git a/fs/read_write.c b/fs/read_write.c\nindex 650fc7e0f3a6..55be193913b6 100644\n--- a/fs/read_write.c\n+++ b/fs/read_write.c\n@@ -434,10 +434,14 @@ ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)\n }\n EXPORT_SYMBOL(kernel_read);\n\n+#ifdef CONFIG_KSU\n+extern bool ksu_vfs_read_hook __read_mostly;\n+extern int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,\n+\t\t\tsize_t *count_ptr, loff_t **pos);\n+#endif\n ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)\n {\n \tssize_t ret;\n+   #ifdef CONFIG_KSU \n+\tif (unlikely(ksu_vfs_read_hook))\n+\t\tksu_handle_vfs_read(&file, &buf, &count, &pos);\n+   #endif\n+\n \tif (!(file->f_mode & FMODE_READ))\n \t\treturn -EBADF;\n \tif (!(file->f_mode & FMODE_CAN_READ))\n```\n```diff[stat.c]\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 376543199b5a..82adcef03ecc 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -148,6 +148,8 @@ int vfs_statx_fd(unsigned int fd, struct kstat *stat,\n }\n EXPORT_SYMBOL(vfs_statx_fd);\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+#endif\n+\n /**\n  * vfs_statx - Get basic and extra attributes by filename\n  * @dfd: A file descriptor representing the base dir for a relative filename\n@@ -170,6 +172,7 @@ int vfs_statx(int dfd, const char __user *filename, int flags,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_AUTOMOUNT;\n\n+   #ifdef CONFIG_KSU\n+\tksu_handle_stat(&dfd, &filename, &flags);\n+   #endif\n \tif ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t       AT_EMPTY_PATH | KSTAT_QUERY_FLAGS)) != 0)\n \t\treturn -EINVAL;\n```\n\n:::\n\nYou should find the four functions in kernel source:\n\n1. `do_faccessat`, usually in `fs/open.c`\n2. `do_execveat_common`, usually in `fs/exec.c`\n3. `vfs_read`, usually in `fs/read_write.c`\n4. `vfs_statx`, usually in `fs/stat.c`\n\nIf your kernel doesn't have the `vfs_statx` function, use `vfs_fstatat` instead:\n\n```diff\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 068fdbcc9e26..5348b7bb9db2 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -87,6 +87,8 @@ int vfs_fstat(unsigned int fd, struct kstat *stat)\n }\n EXPORT_SYMBOL(vfs_fstat);\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+#endif\n int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \t\tint flag)\n {\n@@ -94,6 +96,8 @@ int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = 0;\n+   #ifdef CONFIG_KSU \n+\tksu_handle_stat(&dfd, &filename, &flag);\n+   #endif\n+\n \tif ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t      AT_EMPTY_PATH)) != 0)\n \t\tgoto out;\n```\n\nFor kernels eariler than 4.17, if you cannot find `do_faccessat`, just go to the definition of the `faccessat` syscall and place the call there:\n\n```diff\ndiff --git a/fs/open.c b/fs/open.c\nindex 2ff887661237..e758d7db7663 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -355,6 +355,9 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn error;\n }\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t        int *flags);\n+#endif\n+\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -370,6 +373,8 @@ SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n+   #ifdef CONFIG_KSU\n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n+   #endif\n+\n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n\n### Safe Mode\n\nTo enable KernelSU's built-in Safe Mode, you should modify the `input_handle_event` function in `drivers/input/input.c`:\n\n::: tip\nIt's strongly recommended to enable this feature, it's very useful for preventing bootloops!\n:::\n\n```diff\ndiff --git a/drivers/input/input.c b/drivers/input/input.c\nindex 45306f9ef247..815091ebfca4 100755\n--- a/drivers/input/input.c\n+++ b/drivers/input/input.c\n@@ -367,10 +367,13 @@ static int input_get_disposition(struct input_dev *dev,\n \treturn disposition;\n }\n\n+#ifdef CONFIG_KSU\n+extern bool ksu_input_hook __read_mostly;\n+extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value);\n+#endif\n+\n static void input_handle_event(struct input_dev *dev,\n \t\t\t       unsigned int type, unsigned int code, int value)\n {\n\tint disposition = input_get_disposition(dev, type, code, &value);\n+   #ifdef CONFIG_KSU\n+\tif (unlikely(ksu_input_hook))\n+\t\tksu_handle_input_handle_event(&type, &code, &value);\n+   #endif\n \n \tif (disposition != INPUT_IGNORE_EVENT && type != EV_SYN)\n \t\tadd_input_randomness(type, code, value);\n```\n\n::: info ENTERING SAFE MODE ACCIDENTALLY?\nIf you're using manual integration and don't disable `CONFIG_KPROBES`, the user will be able to trigger Safe Mode by pressing the volume down button after booting! Therefore, if you're using manual integration, it's necessary to disable `CONFIG_KPROBES`!\n:::\n\n### Failed to execute `pm` in terminal?\n\nYou should modify `fs/devpts/inode.c`. Reference:\n\n```diff\ndiff --git a/fs/devpts/inode.c b/fs/devpts/inode.c\nindex 32f6f1c68..d69d8eca2 100644\n--- a/fs/devpts/inode.c\n+++ b/fs/devpts/inode.c\n@@ -602,6 +602,8 @@ struct dentry *devpts_pty_new(struct pts_fs_info *fsi, int index, void *priv)\n        return dentry;\n }\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_devpts(struct inode*);\n+#endif\n+\n /**\n  * devpts_get_priv -- get private data for a slave\n  * @pts_inode: inode of the slave\n@@ -610,6 +612,7 @@ struct dentry *devpts_pty_new(struct pts_fs_info *fsi, int index, void *priv)\n  */\n void *devpts_get_priv(struct dentry *dentry)\n {\n+       #ifdef CONFIG_KSU\n+       ksu_handle_devpts(dentry->d_inode);\n+       #endif\n        if (dentry->d_sb->s_magic != DEVPTS_SUPER_MAGIC)\n                return NULL;\n        return dentry->d_fsdata;\n```\n\n### How to backport path_umount\n\nYou can make the \"Umount modules\" feature work on pre-GKI kernels by manually backporting `path_umount` from 5.9. You can use this patch as reference:\n\n```diff\n--- a/fs/namespace.c\n+++ b/fs/namespace.c\n@@ -1739,6 +1739,39 @@ static inline bool may_mandlock(void)\n }\n #endif\n\n+static int can_umount(const struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\n+\tif (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW))\n+\t\treturn -EINVAL;\n+\tif (!may_mount())\n+\t\treturn -EPERM;\n+\tif (path->dentry != path->mnt->mnt_root)\n+\t\treturn -EINVAL;\n+\tif (!check_mnt(mnt))\n+\t\treturn -EINVAL;\n+\tif (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */\n+\t\treturn -EINVAL;\n+\tif (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN))\n+\t\treturn -EPERM;\n+\treturn 0;\n+}\n+\n+int path_umount(struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\tint ret;\n+\n+\tret = can_umount(path, flags);\n+\tif (!ret)\n+\t\tret = do_umount(mnt, flags);\n+\n+\t/* we mustn't call path_put() as that would clear mnt_expiry_mark */\n+\tdput(path->dentry);\n+\tmntput_no_expire(mnt);\n+\treturn ret;\n+}\n /*\n  * Now umount can handle mount points as well as block devices.\n  * This is important for filesystems which use unnamed block devices.\n```\n\nFinally, build your kernel again, and KernelSU should work correctly.\n"
  },
  {
    "path": "website/docs/guide/installation.md",
    "content": "# Installation\n\n## Check if your device is supported\n\nDownload KernelSU manager from [GitHub Releases](https://github.com/tiann/KernelSU/releases) and install it on your device:\n\n- If the app shows `Unsupported`, it means that **you should compile the kernel yourself**, KernelSU won't and never provide a boot.img file for you to flash.\n- If the app shows `Not installed`, then your device is officially supported by KernelSU.\n\n::: info\nFor devices showing `Unsupported`, you can check the list of [Unofficially supported devices](unofficially-support-devices.md). You can compile the kernel yourself.\n:::\n\n## Backup stock boot.img\n\nBefore flashing, it's essential that you back up your stock boot.img. If you encounter any bootloop, you can always restore the system by flashing back to the stock factory boot using fastboot.\n\n::: warning\nFlashing may cause data loss. Make sure to do this step well before proceeding to the next step! You can also back up all the data on your device if necessary.\n:::\n\n## Necessary knowledge\n\n### ADB and fastboot\n\nBy default, you will use ADB and fastboot tools in this tutorial, so if you don't know them, we recommend using a search engine to learn about them first.\n\n### KMI\n\nKernel Module Interface (KMI), kernel versions with the same KMI are **compatible**, this is what \"general\" means in GKI; conversely, if the KMI is different, then these kernels aren't compatible with each other, and flashing a kernel image with a different KMI than your device may cause a bootloop.\n\nSpecifically, for GKI devices, the kernel version format should be as follows:\n\n```txt\nKernelRelease :=\nVersion.PatchLevel.SubLevel-AndroidRelease-KmiGeneration-suffix\nw      .x         .y       -zzz           -k            -something\n```\n\n`w.x-zzz-k` is the KMI version. For example, if a device kernel version is `5.10.101-android12-9-g30979850fc20`, then its KMI is `5.10-android12-9`. Theoretically, it can boot up normally with other KMI kernels.\n\n::: tip\nNote that the SubLevel in the kernel version isn't part of the KMI! This means that `5.10.101-android12-9-g30979850fc20` has the same KMI as `5.10.137-android12-9-g30979850fc20`!\n:::\n\n### Security patch level {#security-patch-level}\n\nNewer Android devices may have anti-rollback mechanisms that prevent flashing a boot image with an old security patch level. For example, if your device kernel is `5.10.101-android12-9-g30979850fc20`, the security patch level is `2023-11`; even if you flash the kernel corresponding to the KMI, if the security patch level is older than `2023-11` (such as `2023-06`), it may cause a bootloop.\n\nTherefore, kernels with latest security patch levels are preferred to maintain compatibility with the KMI.\n\n### Kernel version vs Android version\n\nPlease note: **Kernel version and Android version aren't necessarily the same!**\n\nIf you find that your kernel version is `android12-5.10.101`, but your Android system version is Android 13 or other, don't be surprised, because the version number of the Android system isn't necessarily the same as the version number of the Linux kernel. The version number of the Linux kernel is generally correspondent to the version of the Android system that comes with the **device when it is shipped**. If the Android system is upgraded later, the kernel version will generally not change. So, before flashing anything, **always refer to the kernel version!**\n\n## Introduction\n\nSince version [0.9.0](https://github.com/tiann/KernelSU/releases/tag/v0.9.0), KernelSU supports two running modes on GKI devices:\n\n1. `GKI`: Replace the original kernel of the device with the **Generic Kernel Image** (GKI) provided by KernelSU.\n2. `LKM`: Load the **Loadable Kernel Module** (LKM) into the device kernel without replacing the original kernel.\n\nThese two modes are suitable for different scenarios, and you can choose the one according to your needs.\n\n### GKI mode {#gki-mode}\n\nIn GKI mode, the original kernel of the device will be replaced with the generic kernel image provided by KernelSU. The advantages of GKI mode are:\n\n1. Strong universality, suitable for most devices. For example, Samsung has enabled KNOX devices, and LKM mode cannot work. There are also some niche modified devices that can only use GKI mode.\n2. Can be used without relying on official firmware, and there is no need to wait for official firmware updates, as long as the KMI is consistent, it can be used.\n\n### LKM mode {#lkm-mode}\n\nIn LKM mode, the original kernel of the device won't be replaced, but the loadable kernel module will be loaded into the device kernel. The advantages of LKM mode are:\n\n1. Won't replace the original kernel of the device. If you have special requirements for the original kernel of the device, or you want to use KernelSU while using a third-party kernel, you can use LKM mode.\n2. It's more convenient to upgrade and OTA. When upgrading KernelSU, you can directly install it in the manager without flashing manually. After the system OTA, you can directly install it to the second slot without manual flashing.\n3. Suitable for some special scenarios. For example, LKM can also be loaded with temporary root permissions. Since it doesn't need to replace the boot partition, it won't trigger AVB and won't cause the device to be bricked.\n4. LKM can be temporarily uninstalled. If you want to temporarily disable root access, you can uninstall LKM. This process doesn't require flashing partitions, or even rebooting the device. If you want to enable root again, just reboot the device.\n\n::: tip COEXISTENCE OF TWO MODES\nAfter opening the manager, you can see the current mode of the device on the homepage. Note that the priority of GKI mode is higher than that of LKM. For example, if you use GKI kernel to replace the original kernel, and use LKM to patch the GKI kernel, the LKM will be ignored, and the device will always run in GKI mode.\n:::\n\n### Which one to choose? {#which-one}\n\nIf your device is a mobile phone, we recommend that you prioritize LKM mode. If your device is an emulator, WSA, or Waydroid, we recommend that you prioritize GKI mode.\n\n## LKM installation\n\n### Get the official firmware\n\nTo use LKM mode, you need to get the official firmware and patch it based on the official firmware. If you use a third-party kernel, you can use the `boot.img` of the third-party kernel as the official firmware.\n\nThere are many ways to get the official firmware. If your device supports `fastboot boot`, we recommend **the most recommended and simplest** method is to use `fastboot boot` to temporarily boot the GKI kernel provided by KernelSU, then install the manager, and finally install it directly in the manager. This method doesn't require manually downloading the official firmware or manually extracting the boot.\n\nIf your device doesn't support `fastboot boot`, you may need to manually download the official firmware package and extract the boot from it.\n\nUnlike GKI mode, LKM mode modifies the `ramdisk`. Therefore, on devices with Android 13, it needs to patch the `init_boot` partition instead of the `boot` partition, while GKI mode always operates on the `boot` partition.\n\n### Use the manager\n\nOpen the manager, click the installation icon in the upper right corner, and several options will appear:\n\n1. Select a file. If your device doesn't have root privileges, you can choose this option and then select your official firmware. The manager will automatically patch it. After that, just flash this patched file to obtain root privileges permanently.\n2. Direct install. If your device is already rooted, you can choose this option. The manager will automatically get your device information, and then automatically patch the official firmware, and flash it automatically. You can consider using `fastboot boot` KernelSU's GKI kernel to get temporary root and install the manager, and then use this option. This is also the main way to upgrade KernelSU.\n3. Install to inactive slot. If your device supports A/B partition, you can choose this option. The manager will automatically patch the official firmware and install it to another partition. This method is suitable for devices after OTA, you can directly install it to another partition after OTA, and then restart the device.\n\n### Use the command line\n\nIf you don't want to use the manager, you can also use the command line to install LKM. The `ksud` tool provided by KernelSU can help you quickly patch the official firmware and then flash it.\n\nThis tool supports macOS, Linux, and Windows. You can download the corresponding version from [GitHub Release](https://github.com/tiann/KernelSU/releases).\n\nUsage: `ksud boot-patch` you can check the command line help for specific options.\n\n```sh\noriole:/ # ksud boot-patch -h\nPatch boot or init_boot images to apply KernelSU\n\nUsage: ksud boot-patch [OPTIONS]\n\nOptions:\n  -b, --boot <BOOT>              Boot image path. If not specified, it will try to find the boot image automatically\n  -k, --kernel <KERNEL>          Kernel image path to be replaced\n  -m, --module <MODULE>          LKM module path to be replaced. If not specified, the built-in module will be used\n  -i, --init <INIT>              init to be replaced\n  -u, --ota                      Will use another slot if the boot image is not specified\n  -f, --flash                    Flash it to boot partition after patch\n  -o, --out <OUT>                Output path. If not specified, the current directory will be used\n      --magiskboot <MAGISKBOOT>  magiskboot path. If not specified, the built-in version will be used\n      --kmi <KMI>                KMI version. If specified, the indicated KMI will be used\n  -h, --help                     Print help\n```\n\nA few options that need to be explained:\n\n1. The `--magiskboot` option can specify the path of magiskboot. If not specified, ksud will look for it in the environment variables. If you don’t know how to get magiskboot, you can check [here](#patch-boot-image).\n2. The `--kmi` option can specify the `KMI` version. If the kernel name of your device doesn't follow the KMI specification, you can specify it using this option.\n\nThe most common usage is:\n\n```sh\nksud boot-patch -b <boot.img> --kmi android13-5.10\n```\n\n## GKI mode installation\n\nThere are several installation methods for GKI mode, each suitable for a different scenario, so please choose accordingly:\n\n1. Install with fastboot using the boot.img provided by KernelSU.\n2. Install with a kernel flash app, such as [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases).\n3. Repair the boot.img manually and install it.\n4. Install with custom Recovery (e.g., TWRP).\n\n## Install with boot.img provided by KernelSU\n\nIf your device's `boot.img` uses a commonly used compression format, you can use the GKI images provided by KernelSU to flash it directly. This doesn't require TWRP or self-patching the image.\n\n### Find proper boot.img\n\nKernelSU provides a generic boot.img for GKI devices, and you should flash the boot.img to the device's boot partition.\n\nYou can download boot.img from [GitHub Release](https://github.com/tiann/KernelSU/releases). Please note that you should use the correct version of boot.img. If you don't know which file to download, carefully read the description of [KMI](#kmi) and [Security patch level](#security-patch-level) in this document.\n\nNormally, there are three boot files in different formats for the same KMI and security patch level. They're identical except for the kernel compression format. Please check the kernel compression format of your original boot.img. You should use the correct format, such as `lz4`, `gz`. If you use an incorrect compression format, you may encounter bootloop after flashing boot.img.\n\n::: info COMPRESSION FORMAT OF BOOT.IMG\n1. You can use magiskboot to get the compression format of your original boot.img. Alternatively, you can also ask members or developers in the community who have the same device model. Also, the compression format of the kernel usually doesn't change, so if you boot successfully with a certain compression format, you can try that format later as well.\n2. Xiaomi devices usually use `gz` or `uncompressed`.\n3. For Pixel devices, follow the instructions below:\n:::\n\n### Flash boot.img to device\n\nUse `adb` to connect your device, then execute `adb reboot bootloader` to enter fastboot mode, and use this command to flash KernelSU:\n\n```sh\nfastboot flash boot boot.img\n```\n\n::: info\nIf your device supports `fastboot boot`, you can first use `fastboot boot boot.img` to try to use boot.img to boot the system first. If something unexpected happens, restart it again to boot.\n:::\n\n### Reboot\n\nAfter the flash is completed, you should reboot your device:\n\n```sh\nfastboot reboot\n```\n\n## Install with Kernel Flasher\n\nSteps:\n\n1. Download the AnyKernel3 ZIP. If you don't know which file to download, carefully read the description of [KMI](#kmi) and [Security patch level](#security-patch-level) in this document.\n2. Open the Kernel Flasher app, grant necessary root permissions, and use the provided AnyKernel3 ZIP to flash.\n\nThis way requires the Kernel Flasher app to have root permissions. You can use the following methods to achieve this:\n\n1. Your device is rooted. For example, you have installed KernelSU and want to upgrade to the latest version or you have rooted through other methods (such as Magisk).\n2. If your device isn't rooted, but the device supports the temporary boot method of `fastboot boot boot.img`, you can use the GKI image provided by KernelSU to temporarily boot your device, obtain temporary root permissions, and then use the Kernel Flash app to obtain permanent root privileges.\n\nSome of kernel flashing apps that can be used for this:\n\n1. [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases)\n2. [Franco Kernel Manager](https://play.google.com/store/apps/details?id=com.franco.kernel)\n3. [Ex Kernel Manager](https://play.google.com/store/apps/details?id=flar2.exkernelmanager)\n\nNote: This method is more convenient when upgrading KernelSU and can be done without a computer (make a backup first).\n\n## Patch boot.img manually {#patch-boot-image}\n\nFor some devices, the boot.img format isn't as common as `lz4`, `gz`, and `uncompressed`. A typical example is the Pixel, where the boot.img is compressed in the `lz4_legacy` format, while the, ramdisk may be in `gz` or also compressed in `lz4_legacy`. Currently, if you directly flash the boot.img provided by KernelSU, the device may not be able to boot. In this case, you can manually patch the boot.img to achieve this.\n\nIt's always recommended to use `magiskboot` to patch images, there are two ways:\n\n1. [magiskboot](https://github.com/topjohnwu/Magisk/releases)\n2. [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci)\n\nThe official build of `magiskboot` can only run on Android devices, if you want to run it on PC, you can try the second option.\n\n::: tip\nAndroid-Image-Kitchen isn't recommended for now because it doesn't handle the boot metadata (such as security patch level) correctly. Therefore, it may not work on some devices.\n:::\n\n### Preparation\n\n1. Get your device's stock boot.img. You can get it from your device manufacturers. You may need [payload-dumper-go](https://github.com/ssut/payload-dumper-go).\n2. Download the AnyKernel3 ZIP file provided by KernelSU that matches the KMI version of your device. You can refer to [Install with custom Recovery](#install-with-custom-recovery).\n3. Unpack the AnyKernel3 package and get the `Image` file, which is the kernel file of KernelSU.\n\n### Using magiskboot on Android devices {#using-magiskboot-on-Android-devices}\n\n1. Download latest Magisk from [GitHub Releases](https://github.com/topjohnwu/Magisk/releases).\n2. Rename `Magisk-*(version).apk` to `Magisk-*.zip` and unzip it.\n3. Push `Magisk-*/lib/arm64-v8a/libmagiskboot.so` to your device by ADB: `adb push Magisk-*/lib/arm64-v8a/libmagiskboot.so /data/local/tmp/magiskboot`\n4. Push stock boot.img and Image in AnyKernel3 to your device.\n5. Enter ADB shell and run `cd /data/local/tmp/` directory, then `chmod +x magiskboot`\n6. Enter ADB shell and run `cd /data/local/tmp/` directory, execute `./magiskboot unpack boot.img` to unpack `boot.img`, you will get a `kernel` file, this is your stock kernel.\n7. Replace `kernel` with `Image` by running the command: `mv -f Image kernel`.\n8. Execute `./magiskboot repack boot.img` to repack boot image, and you will get a `new-boot.img` file, flash this file to device by fastboot.\n\n### Using magiskboot on Windows/macOS/Linux PC {#using-magiskboot-on-PC}\n\n1. Download the corresponding `magiskboot` binary for your OS from [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci).\n2. Prepare stock `boot.img` and `Image` in your PC.\n3. Run `chmod +x magiskboot`.\n4. Enter the corresponding directory, execute `./magiskboot unpack boot.img` to unpack `boot.img`, you will get a `kernel` file, this is your stock kernel.\n5. Replace `kernel` with `Image` by running the command: `mv -f Image kernel`.\n6. Execute `./magiskboot repack boot.img` to repack the boot image, and you will get a `new-boot.img` file, flash this file to device by fastboot.\n\n::: info\nOfficial `magiskboot` can run in `Linux` environments normally, if you're a Linux user, you can use the official build.\n:::\n\n## Install with custom Recovery {#install-with-custom-recovery}\n\nPrerequisite: Your device must have a custom Recovery, such as TWRP. If there is no custom Recovery available for your device, use another method.\n\nSteps:\n\n1. On [GitHub Releases](https://github.com/tiann/KernelSU/releases), download the ZIP package starting with `AnyKernel3` that matches your device's version. For example, if the device's kernel version is `android12-5.10.66`, then you should download the `AnyKernel3-android12-5.10.66_yyyy-MM.zip` file (where `yyyy` is the year and `MM` is the month).\n2. Reboot the device into TWRP.\n3. Use ADB to place AnyKernel3-*.zip into the device's `/sdcard` location and choose to install it in the TWRP GUI, or you can directly run `adb sideload AnyKernel-*.zip` to install.\n\nNote: This method is suitable for any installation (not limited to initial installation or subsequent upgrades), as long as you're using TWRP.\n\n## Other methods\n\nIn fact, all of these installation methods have only one main idea, which is to **replace the original kernel for the one provided by KernelSU**, as long as this can be achieved, it can be installed. The following are other possible methods:\n\n1. First, install Magisk, get root privileges through Magisk, and then use the Kernel Flasher to flash the AnyKernel3 ZIP from KernelSU.\n2. Use any flashing toolkit on PC to flash the kernel provided by KernelSU.\n\nHowever, if it doesn't work, please try `magiskboot` approach.\n\n## Post-Installation: Module Support\n\n::: warning METAMODULE FOR SYSTEM FILE MODIFICATION\nIf you want to use modules that modify `/system` files, you need to install a **metamodule** after installing KernelSU. Modules that only use scripts, sepolicy, or system.prop work without a metamodule.\n:::\n\n**For `/system` modification support**, please see the [Metamodule Guide](metamodule.md) to:\n- Understand what metamodules are and why they're needed\n- Install the official `meta-overlayfs` metamodule\n- Learn about other metamodule options\n"
  },
  {
    "path": "website/docs/guide/metamodule.md",
    "content": "# Metamodule\n\nMetamodules are a revolutionary feature in KernelSU that transfers critical module system capabilities from the core to pluggable modules. This architectural shift maintains KernelSU's stability and security while unleashing greater innovation potential for the module ecosystem.\n\n## What is a Metamodule?\n\nA metamodule is a special type of KernelSU module that provides core infrastructure functionality for the module system. Unlike regular modules that modify system files, metamodules control *how* regular modules are installed and mounted.\n\nMetamodules are a plugin-based extension mechanism that allows complete customization of KernelSU's module management infrastructure. By delegating mounting and installation logic to metamodules, KernelSU avoids being a fragile detection point while enabling diverse implementation strategies.\n\n**Key characteristics:**\n\n- **Infrastructure role**: Metamodules provide services that regular modules depend on\n- **Single instance**: Only one metamodule can be installed at a time\n- **Priority execution**: Metamodule scripts run before regular module scripts\n- **Special hooks**: Provides three hook scripts for installation, mounting, and cleanup\n\n## Why Metamodules?\n\nTraditional root solutions bake mounting logic into their core, making them easier to detect and harder to evolve. KernelSU's metamodule architecture solves these problems through separation of concerns.\n\n**Strategic advantages:**\n\n- **Reduced detection surface**: KernelSU itself doesn't perform mounts, reducing detection vectors\n- **Stability**: Core remains stable while mounting implementations can evolve\n- **Innovation**: Community can develop alternative mounting strategies without forking KernelSU\n- **Choice**: Users can select the implementation that best fits their needs\n\n**Mounting flexibility:**\n\n- **No mounting**: For users with mountless-only modules, avoid mounting overhead entirely\n- **OverlayFS mounting**: Traditional approach with read-write layer support (via `meta-overlayfs`)\n- **Magic mount**: Magisk-compatible mounting for better app compatibility\n- **Custom implementations**: FUSE-based overlays, custom VFS mounts, or entirely new approaches\n\n**Beyond mounting:**\n\n- **Extensibility**: Add features like kernel module support without modifying core KernelSU\n- **Modularity**: Update implementations independently of KernelSU releases\n- **Customization**: Create specialized solutions for specific devices or use cases\n\n::: warning IMPORTANT\nWithout a metamodule installed, modules will **NOT** be mounted. Fresh KernelSU installations require installing a metamodule (such as `meta-overlayfs`) for modules to function.\n:::\n\n## For Users\n\n### Installing a Metamodule\n\nInstall a metamodule the same way as regular modules:\n\n1. Download the metamodule ZIP file (e.g., `meta-overlayfs.zip`)\n2. Open KernelSU Manager app\n3. Tap the floating action button (➕)\n4. Select the metamodule ZIP file\n5. Reboot your device\n\nThe `meta-overlayfs` metamodule is the official reference implementation that provides traditional overlayfs-based module mounting with ext4 image support.\n\n### Checking Active Metamodule\n\nYou can check which metamodule is currently active in the KernelSU Manager app's Module page. The active metamodule will be displayed in your module list with its special designation.\n\n### Uninstalling a Metamodule\n\n::: danger WARNING\nUninstalling a metamodule will affect **ALL** modules. After removal, modules will no longer be mounted until you install another metamodule.\n:::\n\nTo uninstall:\n\n1. Open KernelSU Manager\n2. Find the metamodule in your module list\n3. Tap uninstall (you'll see a special warning)\n4. Confirm the action\n5. Reboot your device\n\nAfter uninstalling, you should install another metamodule if you want modules to continue working.\n\n### Single Metamodule Constraint\n\nOnly one metamodule can be installed at a time. If you try to install a second metamodule, KernelSU will prevent the installation to avoid conflicts.\n\nTo switch metamodules:\n\n1. Uninstall all regular modules\n2. Uninstall the current metamodule\n3. Reboot\n4. Install the new metamodule\n5. Reinstall your regular modules\n6. Reboot again\n\n## For Module Developers\n\nIf you're developing regular KernelSU modules, you don't need to worry much about metamodules. Your modules will work as long as users have a compatible metamodule installed (like `meta-overlayfs`).\n\n**What you need to know:**\n\n- **Mounting requires a metamodule**: The `system` directory in your module will only be mounted if the user has a metamodule installed that provides mounting functionality\n- **No code changes needed**: Existing modules continue to work without modification\n\n::: tip\nIf you're familiar with Magisk module development, your modules will work the same way in KernelSU when metamodule is installed, as it provides Magisk-compatible mounting.\n:::\n\n## For Metamodule Developers\n\nCreating a metamodule allows you to customize how KernelSU handles module installation, mounting, and uninstallation.\n\n### Basic Requirements\n\nA metamodule is identified by a special property in its `module.prop`:\n\n```txt\nid=meta-example\nname=My Custom Metamodule\nversion=1.0\nversionCode=1\nauthor=Your Name\ndescription=Custom module mounting implementation\nmetamodule=1\n```\n\n**Key requirements:**\n\n- The `metamodule=1` (or `metamodule=true`) property marks this as a metamodule. Without this property, the module will be treated as a regular module.\n- **Naming convention**: It is strongly recommended to name your metamodule ID starting with `meta-` (e.g., `meta-overlayfs`, `meta-magicmount`, `meta-custom`). This helps users easily identify metamodules and prevents naming conflicts with regular modules.\n\n### File Structure\n\nA metamodule structure:\n\n```txt\nmeta-example/\n├── module.prop              (must include metamodule=1)\n│\n│      *** Metamodule-specific hooks ***\n├── metamount.sh             (optional: custom mount handler)\n├── metainstall.sh           (optional: installation hook for regular modules)\n├── metauninstall.sh         (optional: cleanup hook for regular modules)\n│\n│      *** Standard module files (all optional) ***\n├── customize.sh             (installation customization)\n├── post-fs-data.sh          (post-fs-data stage script)\n├── service.sh               (late_start service script)\n├── boot-completed.sh        (boot completed script)\n├── uninstall.sh             (metamodule's own uninstallation script)\n└── [any additional files]\n```\n\nMetamodules can use all standard module features (lifecycle scripts, etc.) in addition to their special metamodule hooks.\n\n### Hook Scripts\n\nMetamodules can provide up to three special hook scripts:\n\n#### 1. metamount.sh - Mount Handler\n\n**Purpose**: Controls how modules are mounted during boot.\n\n**When executed**: [Execution Order](#execution-order) below.\n\n**Environment variables:**\n\n- `MODDIR`: The metamodule's directory path (e.g., `/data/adb/modules/meta-example`)\n- All standard KernelSU environment variables\n\n**Responsibilities:**\n\n- Mount all enabled modules systemlessly\n- Check for `skip_mount` flags\n- Handle module-specific mounting requirements\n\n::: danger CRITICAL REQUIREMENT\nWhen performing mount operations, you **MUST** set the source/device name to `\"KSU\"`. This identifies mounts as belonging to KernelSU.\n\n**Example (correct):**\n\n```sh\nmount -t overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work KSU /target\n```\n\n**For modern mount APIs**, set the source string:\n\n```rust\nfsconfig_set_string(fs, \"source\", \"KSU\")?;\n```\n\nThis is essential for KernelSU to identify and manage its mounts properly.\n:::\n\n**Example script:**\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\n\n# Example: Simple bind mount implementation\nfor module in /data/adb/modules/*; do\n    if [ -f \"$module/disable\" ] || [ -f \"$module/skip_mount\" ]; then\n        continue\n    fi\n\n    if [ -d \"$module/system\" ]; then\n        # Mount with source=KSU (REQUIRED!)\n        mount -o bind,dev=KSU \"$module/system\" /system\n    fi\ndone\n```\n\n#### 2. metainstall.sh - Installation Hook\n\n**Purpose**: Customize how regular modules are installed.\n\n**When executed**: During module installation, after files are extracted but before installation completes. This script is **sourced** (not executed) by the built-in installer, similar to how `customize.sh` works.\n\n**Environment variables and functions:**\n\nThis script inherits all variables and functions from the built-in `install.sh`:\n\n- **Variables**: `MODPATH`, `TMPDIR`, `ZIPFILE`, `ARCH`, `API`, `IS64BIT`, `KSU`, `KSU_VER`, `KSU_VER_CODE`, `BOOTMODE`, etc.\n- **Functions**:\n  - `ui_print <msg>` - Print message to console\n  - `abort <msg>` - Print error and terminate installation\n  - `set_perm <target> <owner> <group> <permission> [context]` - Set file permissions\n  - `set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]` - Set permissions recursively\n  - `install_module` - Call the built-in module installation process\n\n**Use cases:**\n\n- Process module files before or after built-in installation (call `install_module` when ready)\n- Move module files\n- Validate module compatibility\n- Set up special directory structures\n- Initialize module-specific resources\n\n**Note**: This script is **NOT** called when installing the metamodule itself.\n\n#### 3. metauninstall.sh - Cleanup Hook\n\n**Purpose**: Clean up resources when regular modules are uninstalled.\n\n**When executed**: During module uninstallation, before the module directory is removed.\n\n**Environment variables:**\n\n- `MODULE_ID`: The ID of the module being uninstalled\n\n**Use cases:**\n\n- Process files\n- Clean up symlinks\n- Free allocated resources\n- Update internal tracking\n\n**Example script:**\n\n```sh\n#!/system/bin/sh\n# Called when uninstalling regular modules\nMODULE_ID=\"$1\"\nIMG_MNT=\"/data/adb/metamodule/mnt\"\n\n# Remove module files from image\nif [ -d \"$IMG_MNT/$MODULE_ID\" ]; then\n    rm -rf \"$IMG_MNT/$MODULE_ID\"\nfi\n```\n\n### Execution Order {#execution-order}\n\nUnderstanding the boot execution order is crucial for metamodule development:\n\n```txt\npost-fs-data stage:\n  1. Common post-fs-data.d scripts execute\n  2. Prune modules, restorecon, load sepolicy.rule\n  3. Metamodule's post-fs-data.sh executes (if exists)\n  4. Regular modules' post-fs-data.sh execute\n  5. Load system.prop\n  6. Metamodule's metamount.sh executes\n     └─> Mounts all modules systemlessly\n  7. post-mount.d stage runs\n     - Common post-mount.d scripts\n     - Metamodule's post-mount.sh (if exists)\n     - Regular modules' post-mount.sh\n\nservice stage:\n  1. Common service.d scripts execute\n  2. Metamodule's service.sh executes (if exists)\n  3. Regular modules' service.sh execute\n\nboot-completed stage:\n  1. Common boot-completed.d scripts execute\n  2. Metamodule's boot-completed.sh executes (if exists)\n  3. Regular modules' boot-completed.sh execute\n```\n\n**Key points:**\n\n- `metamount.sh` runs **AFTER** all post-fs-data scripts (both metamodule and regular modules)\n- Metamodule lifecycle scripts (`post-fs-data.sh`, `service.sh`, `boot-completed.sh`) always run before regular module scripts\n- Common scripts in `.d` directories run before metamodule scripts\n- The `post-mount` stage runs after mounting is complete\n\n### Symlink Mechanism\n\nWhen a metamodule is installed, KernelSU creates a symlink:\n\n```sh\n/data/adb/metamodule -> /data/adb/modules/<metamodule_id>\n```\n\nThis provides a stable path for accessing the active metamodule, regardless of its ID.\n\n**Benefits:**\n\n- Consistent access path\n- Easy detection of active metamodule\n- Simplifies configuration\n\n### Real-World Example: meta-overlayfs\n\nThe `meta-overlayfs` metamodule is the official reference implementation. It demonstrates best practices for metamodule development.\n\n#### Architecture\n\n`meta-overlayfs` uses a **dual-directory architecture**:\n\n1. **Metadata directory**: `/data/adb/modules/`\n   - Contains `module.prop`, `disable`, `skip_mount` markers\n   - Fast to scan during boot\n   - Small storage footprint\n\n2. **Content directory**: `/data/adb/metamodule/mnt/`\n   - Contains actual module files (system, vendor, product, etc.)\n   - Stored in an ext4 image (`modules.img`)\n   - Space-optimized with ext4 features\n\n#### metamount.sh Implementation\n\nHere's how `meta-overlayfs` implements the mount handler:\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\nIMG_FILE=\"$MODDIR/modules.img\"\nMNT_DIR=\"$MODDIR/mnt\"\n\n# Mount ext4 image if not already mounted\nif ! mountpoint -q \"$MNT_DIR\"; then\n    mkdir -p \"$MNT_DIR\"\n    mount -t ext4 -o loop,rw,noatime \"$IMG_FILE\" \"$MNT_DIR\"\nfi\n\n# Set environment variables for dual-directory support\nexport MODULE_METADATA_DIR=\"/data/adb/modules\"\nexport MODULE_CONTENT_DIR=\"$MNT_DIR\"\n\n# Execute the mount binary\n# (The actual mounting logic is in a Rust binary)\n\"$MODDIR/meta-overlayfs\"\n```\n\n#### Key Features\n\n**Overlayfs mounting:**\n\n- Uses kernel overlayfs for true systemless modifications\n- Supports multiple partitions (system, vendor, product, system_ext, odm, oem)\n- Read-write layer support via `/data/adb/modules/.rw/`\n\n**Source identification:**\n\n```rust\n// From meta-overlayfs/src/mount.rs\nfsconfig_set_string(fs, \"source\", \"KSU\")?;  // REQUIRED!\n```\n\nThis sets `dev=KSU` for all overlay mounts, enabling proper identification.\n\n### Best Practices\n\nWhen developing metamodules:\n\n1. **Always set source to \"KSU\"** for mount operations - kernel umount and zygisksu umount need this to umount correctly\n2. **Handle errors gracefully** - boot processes are time-sensitive\n3. **Respect standard flags** - support `skip_mount` and `disable`\n4. **Log operations** - use `echo` or logging for debugging\n5. **Test thoroughly** - mounting errors can cause boot loops\n6. **Document behavior** - clearly explain what your metamodule does\n7. **Provide migration paths** - help users switch from other solutions\n\n### Testing Your Metamodule\n\nBefore releasing:\n\n1. **Test installation** on a clean KernelSU setup\n2. **Verify mounting** with various module types\n3. **Check compatibility** with common modules\n4. **Test uninstallation** and cleanup\n5. **Validate boot performance** (metamount.sh is blocking!)\n6. **Ensure proper error handling** to avoid boot loops\n\n## Frequently Asked Questions\n\n### Do I need a metamodule?\n\n**For users**: Only if you want to use modules that require mounting. If you only use modules that run scripts without modifying system files, you don't need a metamodule.\n\n**For module developers**: No, you develop modules normally. Users need a metamodule only if your module requires mounting.\n\n**For advanced users**: Only if you want to customize mounting behavior or create alternative mounting implementations.\n\n### Can I have multiple metamodules?\n\nNo. Only one metamodule can be installed at a time. This prevents conflicts and ensures predictable behavior.\n\n### What happens if I uninstall my only metamodule?\n\nModules will no longer be mounted. Your device will boot normally, but module modifications won't apply until you install another metamodule.\n\n### Is meta-overlayfs required?\n\nNo. It provides standard overlayfs mounting compatible with most modules. You can create your own metamodule if you need different behavior.\n\n## See Also\n\n- [Module Guide](module.md) - General module development\n- [Difference with Magisk](difference-with-magisk.md) - Comparing KernelSU and Magisk\n- [How to Build](how-to-build.md) - Building KernelSU from source\n"
  },
  {
    "path": "website/docs/guide/module-config.md",
    "content": "# Module Configuration\n\nKernelSU provides a built-in configuration system that allows modules to store persistent or temporary key-value settings. Configurations are stored in a binary format at `/data/adb/ksu/module_configs/<module_id>/` with the following characteristics:\n\n## Configuration Types\n\n- **Persist Config** (`persist.config`): Survives reboots and persists until explicitly deleted or the module is uninstalled\n- **Temp Config** (`tmp.config`): Automatically cleared during the post-fs-data stage on every boot\n\nWhen reading configurations, temporary values take priority over persistent values for the same key.\n\n## Using Configuration in Module Scripts\n\nAll module scripts (`post-fs-data.sh`, `service.sh`, `boot-completed.sh`, etc.) run with the `KSU_MODULE` environment variable set to the module ID. You can use the `ksud module config` commands to manage your module's configuration:\n\n```bash\n# Get a configuration value\nvalue=$(ksud module config get my_setting)\n\n# Set a persistent configuration value\nksud module config set my_setting \"some value\"\n\n# Set a temporary configuration value (cleared on reboot)\nksud module config set --temp runtime_state \"active\"\n\n# Set value from stdin (useful for multiline or complex data)\nksud module config set my_key <<EOF\nmultiline\ntext value\nEOF\n\n# Or pipe from command\necho \"value\" | ksud module config set my_key\n\n# Explicit stdin flag\ncat file.json | ksud module config set json_data --stdin\n\n# List all configuration entries (merged persist + temp)\nksud module config list\n\n# Delete a configuration entry\nksud module config delete my_setting\n\n# Delete a temporary configuration entry\nksud module config delete --temp runtime_state\n\n# Clear all persistent configurations\nksud module config clear\n\n# Clear all temporary configurations\nksud module config clear --temp\n```\n\n## Validation Limits\n\nThe configuration system enforces the following limits:\n\n- **Maximum key length**: 256 bytes\n- **Maximum value length**: 1MB (1048576 bytes)\n- **Maximum config entries**: 32 per module\n- **Key format**: Must match `^[a-zA-Z][a-zA-Z0-9._-]+$` (same as module ID)\n  - Must start with a letter (a-zA-Z)\n  - Can contain letters, numbers, dots (`.`), underscores (`_`), or hyphens (`-`)\n  - Minimum length: 2 characters\n- **Value format**: No restrictions - can contain any UTF-8 characters including newlines, control characters, etc.\n  - Stored in binary format with length prefix, ensuring safe handling of all data\n\n## Lifecycle\n\n- **On boot**: All temporary configurations are cleared during the post-fs-data stage\n- **On module uninstall**: All configurations (both persist and temp) are removed automatically\n- Configurations are stored in a binary format with magic number `0x4b53554d` (\"KSUM\") and version validation\n\n## Use Cases\n\nThe configuration system is ideal for:\n\n- **User preferences**: Store module settings that users configure through WebUI or action scripts\n- **Feature flags**: Enable/disable module features without reinstalling\n- **Runtime state**: Track temporary state that should reset on reboot (use temp config)\n- **Installation settings**: Remember choices made during module installation\n- **Complex data**: Store JSON, multiline text, Base64 encoded data, or any structured content (up to 1MB)\n\n::: tip BEST PRACTICES\n- Use persistent configs for user preferences that should survive reboots\n- Use temporary configs for runtime state or feature toggles that should reset on boot\n- Validate configuration values in your scripts before using them\n- Use the `ksud module config list` command to debug configuration issues\n:::\n\n## Advanced Features\n\nThe module configuration system provides special configuration keys for advanced use cases:\n\n### Overriding Module Description {#overriding-module-description}\n\nYou can dynamically override the `description` field from `module.prop` by setting the `override.description` configuration key:\n\n```bash\n# Override module description\nksud module config set override.description \"Custom description shown in the manager\"\n```\n\nWhen the module list is retrieved, if the `override.description` config exists, it will replace the original description from `module.prop`. This is useful for:\n- Displaying dynamic status information in the module description\n- Showing runtime configuration details to users\n- Updating description based on module state without reinstalling\n\n### Declaring Managed Features\n\nModules can declare which KernelSU features they manage using the `manage.<feature>` configuration pattern. The supported features correspond to KernelSU's internal `FeatureId` enum:\n\n**Supported Features:**\n- `su_compat` - SU compatibility mode\n- `kernel_umount` - Kernel automatic unmount\n\n```bash\n# Declare that this module manages SU compatibility and enables it\nksud module config set manage.su_compat true\n\n# Declare that this module manages kernel unmount and disables it\nksud module config set manage.kernel_umount false\n\n# Remove feature management (module no longer controls this feature)\nksud module config delete manage.su_compat\n```\n\n**How it works:**\n- The presence of a `manage.<feature>` key indicates the module is managing that feature\n- The value indicates the desired state: `true`/`1` for enabled, `false`/`0` (or any other value) for disabled\n- To stop managing a feature, delete the configuration key entirely\n\nManaged features are exposed through the module list API as a `managedFeatures` field (comma-separated string). This allows:\n- KernelSU manager to detect which modules manage which KernelSU features\n- Prevention of conflicts when multiple modules try to manage the same feature\n- Better coordination between modules and core KernelSU functionality\n\n::: warning SUPPORTED FEATURES ONLY\nOnly use the predefined feature names listed above (`su_compat`, `kernel_umount`). These correspond to actual KernelSU internal features. Using other feature names will not cause errors but serves no functional purpose.\n:::\n"
  },
  {
    "path": "website/docs/guide/module-webui.md",
    "content": "# Module WebUI\n\nIn addition to executing boot scripts and modifying system files, KernelSU modules can display user interfaces and interact directly with users.\n\nModules can define HTML + CSS + JavaScript pages with any web technology. KernelSU's manager displays these pages via WebView and exposes APIs for interacting with the system, such as executing shell commands.\n\n## `webroot` directory\n\nWeb resource files should be placed in the `webroot` subdirectory of the module root directory, and there **MUST** be a file named `index.html`, which is the module page entry. The simplest module structure containing a web interface is as follows:\n\n```txt\n❯ tree .\n.\n|-- module.prop\n`-- webroot\n    `-- index.html\n```\n\n::: warning\nWhen installing the module, KernelSU will automatically set the permissions and SELinux context for this directory. If you don't know what you're doing, do not set the permissions for this directory yourself!\n:::\n\nIf your page contains CSS and JavaScript, you need to place it in this directory as well.\n\n## JavaScript API\n\nIf it's just a display page, it will function like a regular web page. However, the most important thing is that KernelSU provides a series of system APIs, allowing the implementation of module-specific functions.\n\nKernelSU provides a JavaScript library, which is published on [npm](https://www.npmjs.com/package/kernelsu) and can be used in the JavaScript code of your web pages.\n\nFor example, you can execute a shell command to obtain a specific configuration or modify a property:\n\n```JavaScript\nimport { exec } from 'kernelsu';\n\nconst { errno, stdout } = exec(\"getprop ro.product.model\");\n```\n\nYou can also make the page full screen or display a toast.\n\n[API documentation](https://www.npmjs.com/package/kernelsu)\n\nIf you find that the existing API doesn't meet your needs or is inconvenient to use, you're welcome to give us suggestions [here](https://github.com/tiann/KernelSU/issues)!\n\n## Some tips\n\n1. You can use `localStorage` as usual to store some data, but keep in mind that it will be lost if the manager app is uninstalled. If you need persistent storage, you will need to manually save the data in a specific directory.\n2. For simple pages, we recommend using [parceljs](https://parceljs.org/) for packaging. It requires no initial configuration and is extremely easy to use. However, if you're a front-end expert or have your own preferences, feel free to use the tool of your choice!\n"
  },
  {
    "path": "website/docs/guide/module.md",
    "content": "# Module guide\n\nKernelSU provides a module mechanism that achieves the effect of modifying the system directory while maintaining the integrity of the system partition. This mechanism is commonly known as \"systemless\".\n\nThe module mechanism of KernelSU is almost the same as that of Magisk. If you're familiar with Magisk module development, developing KernelSU modules is very similar. You can skip the introduction of modules below and just read [Difference with Magisk](difference-with-magisk.md).\n\n::: warning METAMODULE ONLY NEEDED FOR SYSTEM FILE MODIFICATION\nKernelSU uses a [metamodule](metamodule.md) architecture for mounting the `system` directory. **Only if your module needs to modify `/system` files** (via the `system` directory) do you need to install a metamodule (such as [meta-overlayfs](https://github.com/tiann/KernelSU/releases)). Other module features like scripts, sepolicy rules, and system.prop work without a metamodule.\n:::\n\n## WebUI\n\nKernelSU's modules support displaying interfaces and interacting with users. For more details, refer to the [WebUI documentation](module-webui.md).\n\n## Module Configuration\n\nKernelSU provides a built-in configuration system that allows modules to store persistent or temporary key-value settings. For more details, refer to the [Module Configuration documentation](module-config.md).\n\n## BusyBox\n\nKernelSU ships with a feature-complete BusyBox binary (including full SELinux support). The executable is located at `/data/adb/ksu/bin/busybox`. KernelSU's BusyBox supports runtime toggle-able \"ASH Standalone Shell Mode\". What this Standalone Mode means is that when running in the `ash` shell of BusyBox, every single command will directly use the applet within BusyBox, regardless of what is set as `PATH`. For example, commands like `ls`, `rm`, `chmod` will **NOT** use what is in `PATH` (in the case of Android by default it will be `/system/bin/ls`, `/system/bin/rm`, and `/system/bin/chmod` respectively), but will instead directly call internal BusyBox applets. This makes sure that scripts always run in a predictable environment and always have the full suite of commands no matter which Android version it is running on. To force a command _not_ to use BusyBox, you have to call the executable with full paths.\n\nEvery single shell script running in the context of KernelSU will be executed in BusyBox's `ash` shell with Standalone Mode enabled. For what is relevant to 3rd party developers, this includes all boot scripts and module installation scripts.\n\nFor those who want to use this Standalone Mode feature outside of KernelSU, there are 2 ways to enable it:\n\n1. Set environment variable `ASH_STANDALONE` to `1` <br>Example: `ASH_STANDALONE=1 /data/adb/ksu/bin/busybox sh <script>`\n2. Toggle with command-line options:<br>`/data/adb/ksu/bin/busybox sh -o standalone <script>`\n\nTo make sure all subsequent `sh` shell executed also runs in Standalone Mode, option 1 is the preferred method (and this is what KernelSU and the KernelSU manager use internally) as environment variables are inherited down to child processes.\n\n::: tip DIFFERENCE WITH MAGISK\nKernelSU's BusyBox is now using the binary file compiled directly from the Magisk project. **Thanks to Magisk!** Therefore, you don't need to worry about compatibility issues between BusyBox scripts in Magisk and KernelSU, as they're exactly the same!\n:::\n\n## KernelSU modules\n\nA KernelSU module is a folder placed in `/data/adb/modules` with the structure below:\n\n```txt\n/data/adb/modules\n├── .\n├── .\n|\n├── $MODID                  <--- The folder is named with the ID of the module\n│   │\n│   │      *** Module Identity ***\n│   │\n│   ├── module.prop         <--- This file stores the metadata of the module\n│   │\n│   │      *** Main Contents ***\n│   │\n│   ├── system              <--- This folder will be mounted if skip_mount does not exist\n│   │   ├── ...\n│   │   ├── ...\n│   │   └── ...\n│   │\n│   │      *** Status Flags ***\n│   │\n│   ├── skip_mount          <--- If exists, KernelSU will NOT mount your system folder\n│   ├── disable             <--- If exists, the module will be disabled\n│   ├── remove              <--- If exists, the module will be removed next reboot\n│   │\n│   │      *** Optional Files ***\n│   │\n│   ├── post-fs-data.sh     <--- This script will be executed in post-fs-data\n│   ├── post-mount.sh       <--- This script will be executed in post-mount\n│   ├── service.sh          <--- This script will be executed in late_start service\n│   ├── boot-completed.sh   <--- This script will be executed on boot completed\n|   ├── uninstall.sh        <--- This script will be executed when KernelSU removes your module\n|   ├── action.sh           <--- This script will be executed when user click the Action button in KernelSU app\n│   ├── system.prop         <--- Properties in this file will be loaded as system properties by resetprop\n│   ├── sepolicy.rule       <--- Additional custom sepolicy rules\n│   │\n│   │      *** Auto Generated, DO NOT MANUALLY CREATE OR MODIFY ***\n│   │\n│   ├── vendor              <--- A symlink to $MODID/system/vendor\n│   ├── product             <--- A symlink to $MODID/system/product\n│   ├── system_ext          <--- A symlink to $MODID/system/system_ext\n│   │\n│   │      *** Any additional files / folders are allowed ***\n│   │\n│   ├── ...\n│   └── ...\n|\n├── another_module\n│   ├── .\n│   └── .\n├── .\n├── .\n```\n\n::: tip DIFFERENCE WITH MAGISK\nKernelSU doesn't have built-in support for Zygisk, so there is no content related to Zygisk in the module. However, you can use [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) to support Zygisk modules. In this case, the content of the Zygisk module is identical to that supported by Magisk.\n:::\n\n### module.prop\n\n`module.prop` is a configuration file for a module. In KernelSU, if a module doesn't contain this file, it won't be recognized as a module. The format of this file is as follows:\n\n```txt\nid=<string>\nname=<string>\nversion=<string>\nversionCode=<int>\nauthor=<string>\ndescription=<string>\nupdateJson=<url> (optional)\nactionIcon=<path> (optional)\nwebuiIcon=<path> (optional)\n```\n\n- `id` has to match this regular expression: `^[a-zA-Z][a-zA-Z0-9._-]+$`<br>\n  Example: ✓ `a_module`, ✓ `a.module`, ✓ `module-101`, ✗ `a module`, ✗ `1_module`, ✗ `-a-module`<br>\n  This is the **unique identifier** of your module. You should not change it once published.\n- `versionCode` has to be an **integer**. This is used to compare versions.\n- Others that were not mentioned above can be any **single line** string.\n- Make sure to use the `UNIX (LF)` line break type and not the `Windows (CR+LF)` or `Macintosh (CR)`.\n- `actionIcon` and `webuiIcon` are optional icon paths used as the default\n  icons for the module action shortcut and WebUI shortcut in the Manager. These\n  paths must be relative to the module root directory. For example,\n  `actionIcon=icon/icon.png` will be resolved as `<MODDIR>/icon/icon.png`.\n\n::: tip DYNAMIC DESCRIPTION\nThe `description` field can be dynamically overridden at runtime using the module configuration system. See [Overriding Module Description](module-config.md#overriding-module-description) for details.\n:::\n\n### Shell scripts\n\nPlease read the [Boot scripts](#boot-scripts) section to understand the difference between `post-fs-data.sh` and `service.sh`. For most module developers, `service.sh` should be good enough if you just need to run a boot script, if you need to run the script after boot completed, please use `boot-completed.sh`. If you want to do something after mounting OverlayFS, please use `post-mount.sh`.\n\nIn all scripts of your module, please use `MODDIR=${0%/*}` to get your module's base directory path; do **NOT** hardcode your module path in scripts.\n\n::: tip DIFFERENCE WITH MAGISK\nYou can use the environment variable `KSU` to determine if a script is running in KernelSU or Magisk. If running in KernelSU, this value will be set to `true`.\n:::\n\n### `system` directory\n\nThe contents of this directory will be overlaid on top of the system's `/system` partition after the system is booted. This means that:\n\n::: tip METAMODULE REQUIREMENT\nThe `system` directory is only mounted if you have a metamodule installed that provides mounting functionality (such as `meta-overlayfs`). The metamodule handles how modules are mounted. See the [Metamodule Guide](metamodule.md) for more information.\n:::\n\n1. Files with the same name as those in the corresponding directory in the system will be overwritten by the files in this directory.\n2. Folders with the same name as those in the corresponding directory in the system will be merged with the folders in this directory.\n\nIf you want to delete a file or folder in the original system directory, you need to create a file with the same name as the file/folder in the module directory using `mknod filename c 0 0`. This way, the OverlayFS system will automatically \"whiteout\" this file as if it has been deleted (the /system partition isn't actually changed).\n\nYou can also declare a variable named `REMOVE` containing a list of directories in `customize.sh` to execute removal operations, and KernelSU will automatically execute `mknod <TARGET> c 0 0` in the corresponding directories of the module. For example:\n\n```sh\nREMOVE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n```\n\nThe above list will execute `mknod $MODPATH/system/app/YouTube c 0 0` and `mknod $MODPATH/system/app/Bloatware c 0 0`, `/system/app/YouTube` and `/system/app/Bloatware` will be removed after the module takes effect.\n\nIf you want to replace a directory in the system, you need to create a directory with the same path in your module directory, and then set the attribute `setfattr -n trusted.overlay.opaque -v y <TARGET>` for this directory. This way, the OverlayFS system will automatically replace the corresponding directory in the system (without changing the /system partition).\n\nYou can declare a variable named `REPLACE` in your `customize.sh` file, which includes a list of directories to be replaced, and KernelSU will automatically perform the corresponding operations in your module directory. For example:\n\n```sh\nREPLACE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n```\n\nThis list will automatically create the directories `$MODPATH/system/app/YouTube` and `$MODPATH/system/app/Bloatware`, and then execute `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/YouTube` and `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/Bloatware`. After the module takes effect, `/system/app/YouTube` and `/system/app/Bloatware` will be replaced with empty directories.\n\n::: tip DIFFERENCE WITH MAGISK\nKernelSU uses a [metamodule architecture](metamodule.md) where mounting is delegated to pluggable metamodules. The official `meta-overlayfs` metamodule uses the kernel's OverlayFS for systemless modifications, while Magisk uses magic mount (bind mount) built directly into its core. Both achieve the same goal: modifying `/system` files without physically modifying the `/system` partition. KernelSU's approach provides more flexibility and reduces detection surface.\n:::\n\nIf you're interested in OverlayFS, it's recommended to read the Linux Kernel's [documentation on OverlayFS](https://docs.kernel.org/filesystems/overlayfs.html). For details on KernelSU's metamodule system, see the [Metamodule Guide](metamodule.md).\n\n### system.prop\n\nThis file follows the same format as `build.prop`. Each line comprises of `[key]=[value]`.\n\n### sepolicy.rule\n\nIf your module requires some additional sepolicy patches, please add those rules into this file. Each line in this file will be treated as a policy statement.\n\n## Module installer\n\nA KernelSU module installer is a KernelSU module packaged in a ZIP file that can be flashed in the KernelSU manager. The simplest KernelSU module installer is just a KernelSU module packed as a ZIP file.\n\n```txt\nmodule.zip\n│\n├── customize.sh                       <--- (Optional, more details later)\n│                                           This script will be sourced by update-binary\n├── ...\n├── ...  /* The rest of module's files */\n│\n```\n\n::: warning\nKernelSU module is **NOT** compatible for installation in a custom Recovery!\n:::\n\n### Customization\n\nIf you need to customize the module installation process, optionally you can create a script in the installer named `customize.sh`. This script will be **sourced** (not executed) by the module installer script after all files are extracted and default permissions and secontext are applied. This is very useful if your module requires additional setup based on the device ABI, or you need to set special permissions/secontext for some of your module files.\n\nIf you would like to fully control and customize the installation process, declare `SKIPUNZIP=1` in `customize.sh` to skip all default installation steps. By doing so, your `customize.sh` will be responsible to install everything by itself.\n\nThe `customize.sh` script runs in KernelSU's BusyBox `ash` shell with Standalone Mode enabled. The following variables and functions are available:\n\n#### Variables\n\n- `KSU` (bool): a variable to mark that the script is running in the KernelSU environment, and the value of this variable will always be true. You can use it to distinguish between KernelSU and Magisk.\n- `KSU_VER` (string): the version string of currently installed KernelSU (e.g. `v0.4.0`).\n- `KSU_VER_CODE` (int): the version code of currently installed KernelSU in userspace (e.g. `10672`).\n- `KSU_KERNEL_VER_CODE` (int): the version code of currently installed KernelSU in kernel space (e.g. `10672`).\n- `BOOTMODE` (bool): always be `true` in KernelSU.\n- `MODPATH` (path): the path where your module files should be installed.\n- `TMPDIR` (path): a place where you can temporarily store files.\n- `ZIPFILE` (path): your module's installation ZIP.\n- `ARCH` (string): the CPU architecture of the device. Value is either `arm`, `arm64`, `x86`, or `x64`.\n- `IS64BIT` (bool): `true` if `$ARCH` is either `arm64` or `x64`.\n- `API` (int): the API level (Android version) of the device (e.g., `23` for Android 6.0).\n\n::: warning\nIn KernelSU, `MAGISK_VER_CODE` is always `25200`, and `MAGISK_VER` is always `v25.2`. Please don't use these two variables to determine whether KernelSU is running or not.\n:::\n\n#### Functions\n\n```txt\nui_print <msg>\n    print <msg> to console\n    Avoid using 'echo' as it will not display in custom recovery's console\n\nabort <msg>\n    print error message <msg> to console and terminate the installation\n    Avoid using 'exit' as it will skip the termination cleanup steps\n\nset_perm <target> <owner> <group> <permission> [context]\n    if [context] is not set, the default is \"u:object_r:system_file:s0\"\n    this function is a shorthand for the following commands:\n       chown owner.group target\n       chmod permission target\n       chcon context target\n\nset_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]\n    if [context] is not set, the default is \"u:object_r:system_file:s0\"\n    for all files in <directory>, it will call:\n       set_perm file owner group filepermission context\n    for all directories in <directory> (including itself), it will call:\n       set_perm dir owner group dirpermission context\n```\n\n## Boot scripts\n\nIn KernelSU, scripts are divided into two types based on their running mode: post-fs-data mode and late_start service mode.\n\n- post-fs-data mode\n  - This stage is BLOCKING. The boot process is paused before execution is done or after 10 seconds.\n  - Scripts run before any modules are mounted. This allows a module developer to dynamically adjust their modules before it gets mounted.\n  - This stage happens before Zygote is started, which pretty much means everything in Android.\n  - **WARNING:** Using `setprop` will deadlock the boot process! Please use `resetprop -n <prop_name> <prop_value>` instead.\n  - **Only run scripts in this mode if necessary**.\n- late_start service mode\n  - This stage is NON-BLOCKING. Your script runs in parallel with the rest of the booting process.\n  - **This is the recommended stage to run most scripts**.\n\nIn KernelSU, startup scripts are divided into two types based on their storage location: general scripts and module scripts.\n\n- General scripts\n  - Placed in `/data/adb/post-fs-data.d`, `/data/adb/service.d`, `/data/adb/post-mount.d` or `/data/adb/boot-completed.d`.\n  - Only executed if the script is set as executable (`chmod +x script.sh`).\n  - Scripts in `post-fs-data.d` runs in post-fs-data mode, and scripts in `service.d` runs in late_start service mode.\n  - Modules should **NOT** add general scripts during installation.\n- Module scripts\n  - Placed in the module's own folder.\n  - Only executed if the module is enabled.\n  - `post-fs-data.sh` runs in post-fs-data mode, `service.sh` runs in late_start service mode, `boot-completed.sh` runs on boot completed, `post-mount.sh` runs on OverlayFS mounted.\n\nAll boot scripts will run in KernelSU's BusyBox `ash` shell with Standalone Mode enabled.\n\n### Boot scripts process explanation\n\nThe following is the relevant boot process for Android (some parts are omitted), which includes the operation of KernelSU (with leading asterisks), and can help you better understand the purpose of these module scripts:\n\n```txt\n0. Bootloader (nothing on screen)\nload patched boot.img\nload kernel:\n    - GKI mode: GKI kernel with KernelSU integrated\n    - LKM mode: stock kernel\n...\n\n1. kernel exec init (OEM logo on screen):\n    - GKI mode: stock init\n    - LKM mode: exec ksuinit, insmod kernelsu.ko, exec stock init\nmount /dev, /dev/pts, /proc, /sys, etc.\nproperty-init -> read default props\nread init.rc\n...\nearly-init -> init -> late_init\nearly-fs\n   start vold\nfs\n  mount /vendor, /system, /persist, etc.\npost-fs-data\n  *safe mode check\n  *execute general scripts in post-fs-data.d/\n  *load sepolicy.rule\n  *execute metamodule's post-fs-data.sh (if exists)\n  *execute module scripts post-fs-data.sh\n    **(Zygisk)./bin/zygisk-ptrace64 monitor\n  *(pre)load system.prop (same as resetprop -n)\n  *execute metamodule's metamount.sh (mounts all modules)\n  *execute general scripts in post-mount.d/\n  *execute metamodule's post-mount.sh (if exists)\n  *execute module scripts post-mount.sh\nzygote-start\nload_all_props_action\n  *execute resetprop (actual set props for resetprop with -n option)\n... -> boot\n  class_start core\n    start-service logd, console, vold, etc.\n  class_start main\n    start-service adb, netd (iptables), zygote, etc.\n\n2. kernel2user init (ROM animation on screen, start by service bootanim)\n*execute general scripts in service.d/\n*execute metamodule's service.sh (if exists)\n*execute module scripts service.sh\n*set props for resetprop without -p option\n  **(Zygisk) hook zygote (start zygiskd)\n  **(Zygisk) mount zygisksu/module.prop\nstart system apps (autostart)\n...\nboot complete (broadcast ACTION_BOOT_COMPLETED event)\n*execute general scripts in boot-completed.d/\n*execute metamodule's boot-completed.sh (if exists)\n*execute module scripts boot-completed.sh\n\n3. User operable (lock screen)\ninput password to decrypt /data/data\n*actual set props for resetprop with -p option\nstart user apps (autostart)\n```\n\nIf you're interested in Android Init Language, it's recommended to read its [documentation](https://android.googlesource.com/platform/system/core/+/master/init/README.md).\n\n## Late-load mode {#late-load-mode}\n\nIn addition to the standard boot flow described above, KernelSU supports a **late-load mode** for LKM (Loadable Kernel Module) scenarios. In this mode, the KernelSU kernel module is loaded **after the system has fully booted**, rather than during the init process.\n\n### When does late-load happen?\n\nLate-load is triggered by running the `ksud late-load` command. This command:\n\n1. Detects the current KMI version and loads the corresponding `kernelsu.ko` from embedded assets.\n2. Performs module initialization (SELinux rules, allowlist, features, etc.) that would normally happen during boot.\n\nSince the system is already fully running, certain boot-time mechanisms are unavailable or unnecessary.\n\n### Differences from standard boot\n\n| Behavior | Standard boot | Late-load mode |\n|----------|:---:|:---:|\n| Kernel module loaded by init (PID 1) | Yes | No (loaded after boot) |\n| kprobe hooks for ksud (execve/read/fstat/input) | Yes | Skipped |\n| Safe mode detection (volume key) | Yes | Always disabled |\n| Boot log capture (logcat/dmesg) | Yes | Skipped |\n| Magisk coexistence check | Yes | Skipped |\n| `post-fs-data` event reported to kernel | Yes | Skipped |\n| `boot-completed` event reported to kernel | Yes | Set directly during init |\n| `post-fs-data.sh` / `post-fs-data.d/` scripts | Yes | Replaced by `late-load` stage |\n| `system.prop` loading | Yes | Yes |\n| OverlayFS mount (metamodule) | Yes | Yes |\n| `post-mount.sh` / `post-mount.d/` scripts | Yes | Yes |\n| `service.sh` / `service.d/` scripts | Yes | Yes |\n| `boot-completed.sh` / `boot-completed.d/` scripts | Yes | Yes |\n| `KSU_LATE_LOAD` environment variable | Not set | Set to `1` |\n| Kernel info flag `0x4` | Not set | Set |\n\n### Script execution order\n\nIn late-load mode, the script execution order is:\n\n```txt\nksud late-load:\n  1. Load kernelsu.ko (if not already loaded)\n  2. Extract binaries, handle module updates, load SELinux rules, init features\n  3. Execute late-load.d/ and module late-load scripts (blocking)\n  4. Load system.prop (resetprop -n)\n  5. Execute metamodule mount script (OverlayFS)\n  6. Execute post-mount.d/ and module post-mount.sh (blocking)\n  7. Execute service.d/ and module service.sh (non-blocking)\n  8. Execute boot-completed.d/ and module boot-completed.sh (non-blocking)\n```\n\n### Late-load specific scripts\n\nModules can provide a `late-load.sh` script that runs **only** in late-load mode, as a replacement for `post-fs-data.sh`. This script runs before OverlayFS mounting, similar to `post-fs-data.sh` in the standard flow.\n\nAdditionally, general scripts can be placed in `/data/adb/late-load.d/` to run during this stage.\n\n### Detecting late-load mode in scripts\n\nModules can detect late-load mode by checking the `KSU_LATE_LOAD` environment variable:\n\n```sh\nif [ \"$KSU_LATE_LOAD\" = \"1\" ]; then\n    # Running in late-load mode\n    echo \"Late-load mode detected\"\nfi\n```\n\nThis allows modules to adjust their behavior accordingly, for example skipping operations that are only needed during early boot.\n"
  },
  {
    "path": "website/docs/guide/rescue-from-bootloop.md",
    "content": "# Rescue from bootloop\n\nWhen updating a device, situations may arise where the device becomes \"bricked\". In theory, if you only use fastboot to flash the boot partition or install incompatible modules that cause the device to fail during boot, it can be restored through appropriate operations. This document aims to provide emergency methods to help you recover a \"bricked\" device.\n\n## Brick by flashing boot partition\n\nIn KernelSU, the following situations may cause boot brick when flashing the boot partition:\n\n1. You flashed a boot image in the wrong format. For example, if your device's boot format is `gz`, but you flashed an image in `lz4` format, the device won't boot.\n2. Your device needs to disable AVB verification to boot correctly, which usually requires wiping all data from the device.\n3. Your kernel contains bugs or isn't compatible with your device's flash.\n\nNo matter the situation, you can recover by **flashing the stock boot image**. Therefore, at the beginning of the installation guide, we strongly recommend that you back up your stock boot before flashing. If you didn't back it up, you can obtain the original factory boot from other users with the same device or from the official firmware.\n\n## Brick by modules\n\nThe installation of modules can be one of the most common causes of bricking your device, but we must seriously warn you: **DO NOT INSTALL MODULES FROM UNKNOWN SOURCES!** Since modules have root privileges, they can cause irreversible damage to your device!\n\n### Normal modules\n\nIf you have flashed a module that has been proven to be safe but causes your device to fail to boot, then this situation is easily recoverable in KernelSU without any worries. KernelSU has built-in mechanisms to rescue your device, including the following:\n\n1. AB update\n2. Rescue by pressing Volume down button\n\n#### AB update\n\nKernelSU's module updates are based on the Android system's AB update mechanism used in OTA updates. When you install a new module or update an existing one, it won't directly modify the currently used module file. Instead, all modules are integrated into a new update image. After the system is restarted, it will attempt to boot using this new update image. If the Android system boots successfully, the modules will be effectively updated.\n\nTherefore, the simplest and most commonly used method to rescue your device is to **force a reboot**. If you're unable to start your system after flashing a module, you can press and hold the power button for more than 10 seconds, and the system will automatically reboot. After rebooting, it will roll back to the state before the module update, and any previously updated modules will be automatically disabled.\n\n#### Rescue by pressing Volume down button\n\nIf AB update hasn't yet resolved the issue, you can try using **Safe Mode**. In this mode, all modules are disabled.\n\nThere are two ways to enter Safe Mode:\n\n1. The built-in Safe Mode of some systems: Some systems have a built-in Safe Mode that can be accessed by long-pressing the Volume down button. In other systems (such as HyperOS), Safe Mode can be activated from the Recovery. When entering the system's Safe Mode, KernelSU will also enter this mode and automatically disable the modules.\n2. The built-in Safe Mode of KernelSU: In this case, the method is to **press the Volume down key continuously more than three times** after the first boot screen.\n\nAfter entering Safe Mode, all modules on the Module page in the KernelSU manager will be disabled. However, you can still perform the \"uninstall\" operation to remove any modules that may be causing issues.\n\nThe built-in Safe Mode is implemented in the kernel, so there is no possibility of missing important events due to interception. However, for non-GKI kernels, manual code integration may be required. For this, refer to the official documentation for guidance.\n\n### Malicious modules\n\nIf the above methods cannot rescue your device, it's highly likely that the module you installed has malicious operations or has damaged your device in some other way. In this case, there are only two suggestions:\n\n1. Wipe the data and flash the official system.\n2. Consult the after-sales service.\n"
  },
  {
    "path": "website/docs/guide/unofficially-support-devices.md",
    "content": "# Unofficially supported devices\n\n::: warning\nThis document is for archival reference only and is no longer maintained.\nSince KernelSU v1.0, we have dropped official support for non-GKI devices.\n:::\n\n::: warning\nIn this page, there are kernels for non-GKI devices supporting KernelSU maintained by other developers.\n:::\n\n::: warning\nThis page is intended only to help you find the source code corresponding to your device. It **DOES NOT** mean that the source code has been reviewed by KernelSU developers. You should use it at your own risk.\n:::\n\n<script setup>\nimport data from '../repos.json'\n</script>\n\n<table>\n   <thead>\n      <tr>\n         <th>Maintainer</th>\n         <th>Repository</th>\n         <th>Support devices</th>\n      </tr>\n   </thead>\n   <tbody>\n    <tr v-for=\"repo in data\" :key=\"repo.devices\">\n        <td><a :href=\"repo.maintainer_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.maintainer }}</a></td>\n        <td><a :href=\"repo.kernel_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.kernel_name }}</a></td>\n        <td>{{ repo.devices }}</td>\n    </tr>\n   </tbody>\n</table>\n"
  },
  {
    "path": "website/docs/guide/what-is-kernelsu.md",
    "content": "# What is KernelSU?\n\nKernelSU is a root solution for Android GKI devices. It works in kernel mode and grants root permission to userspace apps directly in kernel space.\n\n## Features\n\nThe main feature of KernelSU is that it's **kernel-based**. KernelSU works in kernel mode, enabling it to provide a kernel interface that we never had before. For example, it's possible to add hardware breakpoints to any process in kernel mode, access the physical memory of any process invisibly, intercept any system call (syscall) within the kernel space, among other functionalities.\n\nAdditionally, KernelSU provides a [metamodule system](metamodule.md), which is a pluggable architecture for module management. Unlike traditional root solutions that bake mounting logic into their core, KernelSU delegates this to metamodules. This allows you to install metamodules like [meta-overlayfs](https://github.com/tiann/KernelSU/tree/main/userspace/meta-overlayfs) to provide systemless modifications to the `/system` partition and other partitions.\n\n## How to use KernelSU?\n\nSee [Installation](installation.md).\n\n## How to build KernelSU?\n\nSee [How to build](how-to-build.md).\n\n## Discussion\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n"
  },
  {
    "path": "website/docs/id_ID/guide/app-profile.md",
    "content": "# App Profile\n\nApp Profile adalah mekanisme yang disediakan KernelSU untuk menyesuaikan konfigurasi beragam aplikasi.\n\nUntuk aplikasi yang diberi izin root (misalnya dapat memakai `su`), App Profile juga dapat disebut Root Profile. Ia memungkinkan kita menyesuaikan `uid`, `gid`, `groups`, `capabilities`, dan aturan `SELinux` dari perintah `su`, sehingga hak istimewa pengguna root dapat dibatasi. Contohnya, hanya memberikan izin jaringan kepada aplikasi firewall sambil menolak izin akses berkas, atau memberikan izin shell alih-alih akses root penuh untuk aplikasi pembeku: **menjaga kekuatan agar tetap terkungkung dalam prinsip least privilege.**\n\nUntuk aplikasi biasa tanpa izin root, App Profile dapat mengendalikan bagaimana kernel dan sistem modul bersikap terhadap aplikasi tersebut. Misalnya, App Profile bisa menentukan apakah perubahan yang disebabkan oleh modul harus tetap terlihat. Kernel dan sistem modul kemudian dapat membuat keputusan berdasarkan konfigurasi ini, seperti melakukan operasi serupa “menyembunyikan”.\n\n## Root Profile\n\n### UID, GID, dan Groups\n\nDi sistem Linux ada dua konsep: pengguna dan grup. Setiap pengguna memiliki user ID (UID), dan seorang pengguna bisa menjadi anggota banyak grup yang masing-masing memiliki group ID (GID). ID inilah yang dipakai untuk mengenali pengguna di sistem dan menentukan sumber daya apa yang boleh mereka akses.\n\nPengguna dengan UID 0 disebut pengguna root, dan grup dengan GID 0 disebut grup root. Grup root biasanya memiliki hak istimewa tertinggi dalam sistem.\n\nDalam sistem Android, tiap aplikasi bertindak sebagai pengguna terpisah (kecuali pada kasus shared UID) dengan UID unik. Misalnya `0` adalah root, `1000` adalah `system`, `2000` adalah shell ADB, dan UID dari `10000` hingga `19999` mewakili aplikasi biasa.\n\n::: info\nUID yang dimaksud di sini berbeda dengan konsep multi-user atau work profile di Android. Work profile sebenarnya diimplementasikan dengan mempartisi rentang UID. Contohnya, 10000-19999 adalah pengguna utama, sementara 110000-119999 adalah work profile. Setiap aplikasi biasa di dalamnya tetap mempunyai UID unik.\n:::\n\nSetiap aplikasi dapat memiliki beberapa grup, dengan GID sebagai grup utama yang biasanya sama dengan UID. Grup lain disebut grup tambahan (supplementary groups). Sejumlah izin dikendalikan oleh keanggotaan grup, misalnya izin akses jaringan atau Bluetooth.\n\nSebagai contoh, apabila kita menjalankan perintah `id` di shell ADB, hasilnya mungkin seperti ini:\n\n```sh\noriole:/ $ id\nuid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),1078(ext_data_rw),1079(ext_obb_rw),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid),3012(readtracefs) context=u:r:shell:s0\n```\n\nDi sini UID-nya `2000`, dan GID (ID grup utama) juga `2000`. Selain itu ia berada di beberapa grup tambahan seperti `inet` (boleh membuat soket `AF_INET` dan `AF_INET6`, artinya boleh mengakses jaringan) dan `sdcard_rw` (boleh baca/tulis kartu SD).\n\nRoot Profile milik KernelSU memungkinkan kita menyesuaikan UID, GID, dan grup untuk proses root setelah menjalankan `su`. Misalnya, Root Profile sebuah aplikasi root dapat mengatur UID menjadi `2000`, sehingga ketika menggunakan `su`, hak sebenarnya setara dengan shell ADB. Kita juga dapat menghapus grup `inet` agar perintah `su` tidak bisa mengakses jaringan.\n\n::: tip CATATAN\nApp Profile hanya mengendalikan hak proses root setelah menggunakan `su`, bukan izin asli aplikasi. Jika aplikasi meminta izin akses jaringan, ia tetap dapat mengakses jaringan meski tidak menggunakan `su`. Menghapus grup `inet` dari `su` hanya membuat `su` tidak bisa mengakses jaringan.\n:::\n\nRoot Profile dipaksakan oleh kernel dan tidak bergantung pada itikad aplikasi root, berbeda dengan mengganti pengguna atau grup secara manual melalui `su`. Pemberian izin `su` sepenuhnya berada di tangan pengguna, bukan pengembang.\n\n### Capabilities\n\nCapabilities adalah mekanisme pemisahan hak istimewa di Linux.\n\nUntuk keperluan pemeriksaan izin, implementasi `UNIX` tradisional membedakan dua jenis proses: proses istimewa (effective UID `0`, disebut superuser atau root) dan proses biasa (effective UID bukan nol). Proses istimewa melewati semua pemeriksaan izin kernel, sedangkan proses biasa tunduk pada pemeriksaan penuh berdasarkan kredensial proses (biasanya effective UID, effective GID, dan daftar grup tambahan).\n\nSejak Linux 2.2, hak istimewa yang biasanya dimiliki superuser dipecah menjadi unit-unit terpisah yang disebut capability, dan masing-masing dapat diaktifkan atau dinonaktifkan secara independen.\n\nSetiap capability mewakili satu atau lebih hak. Misalnya, `CAP_DAC_READ_SEARCH` mewakili kemampuan melewati pemeriksaan izin untuk membaca berkas serta izin baca dan eksekusi direktori. Jika seorang pengguna dengan effective UID `0` (root) tidak memiliki capability `CAP_DAC_READ_SEARCH` atau capability di atasnya, maka meskipun ia root ia tidak bisa sembarang membaca berkas.\n\nRoot Profile KernelSU memungkinkan kita menyesuaikan capability proses root setelah menjalankan `su`, sehingga yang diberikan hanyalah “hak root parsial”. Berbeda dengan UID dan GID di atas, beberapa aplikasi root memang membutuhkan UID `0` setelah memakai `su`. Pada kasus seperti ini, membatasi capability pengguna root dengan UID `0` dapat membatasi operasi yang boleh dilakukan.\n\n::: tip SANGAT DISARANKAN\nDokumentasi resmi capability Linux [dapat dibaca di sini](https://man7.org/linux/man-pages/man7/capabilities.7.html) dan menjelaskan detail hak yang diwakili tiap capability. Jika Anda ingin menyesuaikan capability, sangat disarankan membaca dokumen ini terlebih dahulu.\n:::\n\n### SELinux\n\nSELinux adalah mekanisme Mandatory Access Control (MAC) yang sangat kuat. Ia beroperasi berdasarkan prinsip **default deny**: tindakan apa pun yang tidak diizinkan secara eksplisit akan ditolak.\n\nSELinux memiliki dua mode global:\n\n1. Mode Permissive: penolakan hanya dicatat di log, tidak ditegakkan.\n2. Mode Enforcing: penolakan dicatat dan ditegakkan.\n\n::: warning\nAndroid modern sangat bergantung pada SELinux untuk menjaga keamanan sistem secara keseluruhan. Sangat disarankan untuk tidak memakai sistem kustom yang berjalan dalam mode “Permissive”, karena hampir tidak menawarkan keuntungan dibanding sistem yang sepenuhnya terbuka.\n:::\n\nMenjelaskan SELinux secara tuntas sangatlah kompleks dan berada di luar cakupan dokumen ini. Disarankan mempelajari cara kerjanya melalui sumber berikut:\n\n1. [Wikipedia](https://en.wikipedia.org/wiki/Security-Enhanced_Linux)\n2. [Red Hat: What Is SELinux?](https://www.redhat.com/en/topics/linux/what-is-selinux)\n3. [ArchLinux: SELinux](https://wiki.archlinux.org/title/SELinux)\n\nRoot Profile KernelSU memungkinkan kita menyesuaikan konteks SELinux dari proses root setelah menjalankan `su`. Kita bisa menetapkan aturan kontrol akses khusus untuk konteks ini sehingga hak root dapat diatur secara sangat granular.\n\nDalam skenario umum, ketika sebuah aplikasi menjalankan `su`, prosesnya berpindah ke domain SELinux dengan **akses tidak terbatas**, seperti `u:r:su:s0`. Melalui Root Profile, domain ini bisa diganti menjadi domain kustom seperti `u:r:app1:s0`, lalu serangkaian aturan dapat ditetapkan untuk domain tersebut:\n\n```sh\ntype app1\nenforce app1\ntypeattribute app1 mlstrustedsubject\nallow app1 * * *\n```\n\nPerlu dicatat, aturan `allow app1 * * *` di atas hanya untuk contoh. Dalam praktiknya aturan ini tidak boleh digunakan secara luas karena hampir sama saja dengan mode Permissive.\n\n### Eskalasi\n\nJika konfigurasi Root Profile tidak ditetapkan dengan benar, skenario eskalasi bisa terjadi. Pembatasan yang diberikan App Profile bisa gagal tanpa sengaja.\n\nSebagai contoh, apabila Anda memberikan izin root kepada pengguna shell ADB (kasus yang cukup umum) dan kemudian memberikan izin root kepada aplikasi biasa, namun mengonfigurasi Root Profile aplikasi tersebut dengan UID 2000 (UID milik shell ADB), aplikasi itu bisa mendapatkan akses root penuh dengan menjalankan perintah `su` dua kali:\n\n1. Eksekusi `su` pertama akan tunduk pada App Profile dan mengubah UID menjadi `2000` (shell ADB) alih-alih `0` (root).\n2. Eksekusi `su` kedua, karena UID-nya sudah `2000` dan UID `2000` (shell ADB) memang diberikan akses root pada konfigurasi, maka aplikasi itu akan memperoleh hak root penuh.\n\n::: warning CATATAN\nPerilaku ini sepenuhnya sesuai ekspektasi dan bukan bug. Karena itu kami menyarankan hal berikut:\n\nJika Anda benar-benar perlu memberikan izin root kepada ADB (misalnya sebagai pengembang), jangan mengubah UID menjadi `2000` saat mengonfigurasi Root Profile. Gunakan `1000` (system) agar lebih aman.\n:::\n\n## Non-root profile\n\n### Umount modules\n\nKernelSU menyediakan mekanisme systemless untuk memodifikasi partisi sistem dengan memasang OverlayFS. Namun beberapa aplikasi peka terhadap perilaku ini. Dalam kasus tersebut, kita dapat membongkar (umount) modul yang dimuat di aplikasi tertentu dengan mengaktifkan opsi “Umount modules”.\n\nSelain itu, antarmuka pengaturan KernelSU Manager menyediakan opsi “Umount modules by default”. Secara bawaan opsi ini **aktif**, artinya KernelSU atau modul tertentu akan membongkar modul untuk aplikasi ini kecuali ada pengaturan tambahan. Jika Anda tidak menginginkan perilaku ini atau jika aplikasi tertentu terpengaruh, ada dua pendekatan:\n\n1. Biarkan “Umount modules by default” tetap aktif dan nonaktifkan opsi “Umount modules” di App Profile untuk aplikasi yang memang perlu memuat modul (berperan sebagai daftar putih).\n2. Nonaktifkan “Umount modules by default” lalu aktifkan opsi “Umount modules” di App Profile hanya untuk aplikasi yang harus dibongkar modulnya (berperan sebagai daftar hitam).\n\n::: info\nPada perangkat dengan kernel versi 5.10 ke atas, kernel akan membongkar modul tanpa tindakan tambahan. Namun di perangkat dengan kernel di bawah 5.10, opsi ini hanya berupa konfigurasi dan KernelSU sendiri tidak mengambil tindakan apa pun. Jika ingin memakai opsi “Umount modules” pada kernel sebelum 5.10 Anda harus backport fungsi `path_umount` di `fs/namespace.c`. Anda bisa menemukan info lebih lanjut di bagian akhir halaman [Integrate for non-GKI devices](https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#how-to-backport-path_umount). Beberapa modul seperti Zygisksu juga memakai opsi ini untuk menentukan apakah modul perlu dibongkar.\n:::\n"
  },
  {
    "path": "website/docs/id_ID/guide/difference-with-magisk.md",
    "content": "# Perbedaan dengan Magisk\n\nMeskipun ada banyak kesamaan antara modul KernelSU dan modul Magisk, pasti ada beberapa perbedaan karena mekanisme implementasinya yang sangat berbeda. Jika Anda ingin modul Anda berjalan di Magisk dan KernelSU, Anda harus memahami perbedaan ini.\n\n## Kesamaan\n\n- Format file modul: keduanya menggunakan format zip untuk mengatur modul, dan format modulnya hampir sama\n- Direktori pemasangan modul: keduanya terletak di `/data/adb/modules`\n- Tanpa sistem: keduanya mendukung modifikasi / sistem dengan cara tanpa sistem melalui modul\n- post-fs-data.sh: waktu eksekusi dan semantiknya persis sama\n- service.sh: waktu eksekusi dan semantiknya persis sama\n- system.prop: sepenuhnya sama\n- sepolicy.rule: sama persis\n- BusyBox: skrip dijalankan di BusyBox dengan \"mode mandiri\" diaktifkan di kedua kasus\n\n## Perbedaan\n\nSebelum memahami perbedaannya, Anda perlu mengetahui cara membedakan apakah modul Anda berjalan di KernelSU atau Magisk. Anda dapat menggunakan variabel lingkungan `KSU` untuk membedakannya di semua tempat di mana Anda dapat menjalankan skrip modul (`customize.sh`, `post-fs-data.sh`, `service.sh`). Di KernelSU, variabel lingkungan ini akan disetel ke `true`.\n\nBerikut beberapa perbedaannya:\n\n- Modul KernelSU tidak dapat diinstal dalam mode Pemulihan.\n- Modul KernelSU tidak memiliki dukungan bawaan untuk Zygisk (tetapi Anda dapat menggunakan modul Zygisk melalui [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext).\n- Metode untuk mengganti atau menghapus file dalam modul KernelSU sama sekali berbeda dari Magisk. KernelSU tidak mendukung metode `.replace`. Sebagai gantinya, Anda perlu membuat file dengan nama yang sama dengan `mknod filename c 0 0` untuk menghapus file terkait.\n- Direktori untuk BusyBox berbeda. BusyBox bawaan di KernelSU terletak di `/data/adb/ksu/bin/busybox`, sedangkan di Magisk terletak di `/data/adb/magisk/busybox`. **Perhatikan bahwa ini adalah perilaku internal KernelSU dan dapat berubah di masa mendatang!**\n- KernelSU tidak mendukung file `.replace`; namun, KernelSU mendukung variabel `REMOVE` dan `REPLACE` untuk menghapus atau mengganti file dan folder.\n"
  },
  {
    "path": "website/docs/id_ID/guide/faq.md",
    "content": "# FAQ\n\n## Apakah KernelSU mendukung perangkat saya?\n\nKernelSU mendukung perangkat yang menjalankan Android dengan bootloader yang tidak terkunci. Namun, dukungan resmi hanya untuk GKI Linux Kernel 5.10+ (dalam praktiknya, ini berarti perangkat Anda harus memiliki Android 12 out-of-the-box agar didukung).\n\nAnda dapat dengan mudah memeriksa dukungan untuk perangkat Anda melalui aplikasi manajer KernelSU, yang tersedia [di sini](https://github.com/tiann/KernelSU/releases).\n\nJika aplikasi menunjukkan `Not installed`, berarti perangkat Anda secara resmi didukung oleh KernelSU.\n\nJika aplikasi menunjukkan `Unsupported`, berarti perangkat Anda tidak didukung secara resmi saat ini. Namun, Anda dapat membangun kode sumber kernel dan mengintegrasikan KernelSU untuk membuatnya bekerja, atau gunakan [Perangkat yang didukung tidak resmi](unofficially-support-devices).\n\n## Apakah KernelSU membutuhkan buka bootloader?\n\nYa, tentu saja.\n\n## Apakah KernelSU mendukung modul?\n\nYa, sebagian besar modul Magisk bekerja di KernelSU. Namun, jika modul Anda perlu memodifikasi file `/system`, Anda perlu menginstal [metamodule](metamodule.md) (seperti `meta-overlayfs`). Fitur modul lainnya bekerja tanpa metamodule. Periksa [Panduan modul](module.md) untuk info lebih lanjut.\n\n## Apakah KernelSU mendukung Xposed?\n\nYa, Anda dapat menggunakan LSPosed (atau turunan Xposed modern lainnya) dengan [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext).\n\n## Apakah KernelSU mendukung Zygisk?\n\nKernelSU tidak memiliki dukungan Zygisk bawaan, tetapi Anda dapat menggunakan modul seperti [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) untuk mendukungnya.\n\n## Apakah KernelSU kompatibel dengan Magisk?\n\nSistem modul KernelSU bertentangan dengan magic mount Magisk. Jika ada modul yang diaktifkan di KernelSU, maka seluruh Magisk akan berhenti bekerja.\n\nNamun, jika Anda hanya menggunakan `su` dari KernelSU, ini akan bekerja dengan baik dengan Magisk. KernelSU memodifikasi `kernel`, sedangkan Magisk memodifikasi `ramdisk`, memungkinkan keduanya bekerja bersama.\n\n## Akankah KernelSU menggantikan Magisk?\n\nKami percaya tidak, dan itu bukan tujuan kami. Magisk sudah cukup baik untuk solusi root userspace dan akan memiliki umur yang panjang. Tujuan KernelSU adalah untuk menyediakan antarmuka kernel kepada pengguna, bukan untuk menggantikan Magisk.\n\n## Dapatkah KernelSU mendukung perangkat non-GKI?\n\nAda kemungkinan. Tetapi Anda harus mengunduh sumber kernel dan mengintegrasikan KernelSU ke dalam source tree, dan mengkompilasi kernel sendiri.\n\n## Dapatkah KernelSU mendukung perangkat di bawah Android 12?\n\nKernel perangkat yang mempengaruhi kompatibilitas KernelSU, dan tidak ada hubungannya dengan versi Android. Satu-satunya batasan adalah bahwa perangkat yang diluncurkan dengan Android 12 harus memiliki versi kernel 5.10+ (perangkat GKI). Jadi:\n\n1. Perangkat yang diluncurkan dengan Android 12 harus didukung.\n2. Perangkat dengan kernel lama (beberapa perangkat dengan Android 12 juga memiliki kernel lama) kompatibel (Anda harus membangun kernel sendiri).\n\n## Dapatkah KernelSU mendukung kernel lama?\n\nAda kemungkinan. KernelSU sekarang telah di-backport ke kernel 4.14. Untuk kernel yang lebih lama, Anda perlu melakukan backport secara manual, dan PR selalu diterima!\n\n## Bagaimana cara mengintegrasikan KernelSU untuk kernel lama?\n\nSilakan periksa panduan [Integrasi untuk perangkat non-GKI](how-to-integrate-for-non-gki).\n\n## Mengapa versi Android saya 13, dan kernel menunjukkan \"android12-5.10\"?\n\nVersi kernel tidak ada hubungannya dengan versi Android. Jika Anda perlu mem-flash kernel, selalu gunakan versi kernel; versi Android tidak sepenting itu.\n\n## Saya GKI 1.0, bisakah saya menggunakan ini?\n\nGKI 1.0 sama sekali berbeda dari GKI 2.0, Anda harus mengkompilasi kernel sendiri.\n\n## Bagaimana cara membuat `/system` RW?\n\nKami tidak merekomendasikan Anda memodifikasi partisi sistem secara langsung. Silakan periksa [Panduan modul](module.md) untuk memodifikasinya secara systemless. Jika Anda bersikeras melakukan ini, periksa [magisk_overlayfs](https://github.com/HuskyDG/magic_overlayfs).\n\n## Bisakah KernelSU memodifikasi hosts? Bagaimana cara menggunakan AdAway?\n\nTentu saja. Tetapi KernelSU tidak memiliki dukungan hosts bawaan, Anda dapat menginstal modul seperti [systemless-hosts](https://github.com/symbuzzer/systemless-hosts-KernelSU-module) untuk melakukannya.\n\n## Mengapa modul saya tidak bekerja setelah instalasi baru?\n\nJika modul Anda perlu memodifikasi file `/system`, Anda perlu menginstal [metamodule](metamodule.md) untuk me-mount direktori `system`. Fitur modul lainnya (skrip, sepolicy, system.prop) bekerja tanpa metamodule.\n\n**Solusi**: Lihat [Panduan Metamodule](metamodule.md) untuk instruksi instalasi.\n\n## Apa itu metamodule dan mengapa saya membutuhkannya?\n\nMetamodule adalah modul khusus yang menyediakan infrastruktur untuk me-mount modul reguler. Lihat [Panduan Metamodule](metamodule.md) untuk penjelasan lengkap.\n"
  },
  {
    "path": "website/docs/id_ID/guide/hidden-features.md",
    "content": "# Fitur tersembunyi\n\n## .ksurc\n\nSecara bawaan, `/system/bin/sh` akan memuat `/system/etc/mkshrc`.\n\nAnda dapat membuat `su` memuat berkas rc khusus dengan membuat berkas `/data/adb/ksu/.ksurc`.\n"
  },
  {
    "path": "website/docs/id_ID/guide/how-to-build.md",
    "content": "# Bagaimana caranya untuk build KernelSU?\n\n::: warning\nDokumen ini hanya untuk referensi arsip dan tidak lagi diperbarui.\nSejak KernelSU v3.0, kami telah menghentikan dukungan resmi untuk mode gambar GKI demi iterasi dan kecepatan build yang lebih cepat. Disarankan untuk menggunakan `Ylarod/ddk` untuk membangun LKM.\n:::\n\nPertama, Anda harus membaca dokumen resmi Android untuk membangun kernel:\n\n1. [Building Kernels](https://source.android.com/docs/setup/build/building-kernels)\n2. [GKI Release Builds](https://source.android.com/docs/core/architecture/kernel/gki-release-builds)\n\n> Halaman ini untuk perangkat GKI, jika Anda menggunakan kernel lama, silakan lihat [cara mengintegrasikan KernelSU untuk kernel lama](how-to-integrate-for-non-gki)\n\n## Build Kernel\n\n### Menyinkronkan source code kernel\n\n```sh\nrepo init -u https://android.googlesource.com/kernel/manifest\nmv <kernel_manifest.xml> .repo/manifests\nrepo init -m manifest.xml\nrepo sync\n```\n\n`<kernel_manifest.xml>` adalah berkas manifes yang dapat menentukan build secara unik, Anda dapat menggunakan manifes tersebut untuk melakukan build yang dapat diprediksikan ulang. Anda harus mengunduh berkas manifes dari [Google GKI release builds](https://source.android.com/docs/core/architecture/kernel/gki-release-builds)\n\n### Build\n\nSilakan periksa [official docs](https://source.android.com/docs/setup/build/building-kernels) terlebih dahulu.\n\nSebagai contoh, kita perlu build image kernel aarch64:\n\n```sh\nLTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh\n```\n\nJangan lupa untuk menambahkan flag `LTO=thin`, jika tidak, maka build akan gagal jika memori komputer Anda kurang dari 24GB.\n\nMulai dari Android 13, kernel dibuild oleh `bazel`:\n\n```sh\ntools/bazel build --config=fast //common:kernel_aarch64_dist\n```\n\n## Build Kernel dengan KernelSU\n\nJika Anda dapat build kernel dengan sukses, maka build KernelSU sangatlah mudah, jalankan perintah ini di root dir kernel source:\n\n- Latest tag(stable)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\n- main branch(dev)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\n```\n\n- Select tag(Such as v0.5.2)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\n```\n\nDan kemudian build ulang kernel dan Anda akan mendapatkan image kernel dengan KernelSU!\n"
  },
  {
    "path": "website/docs/id_ID/guide/how-to-integrate-for-non-gki.md",
    "content": "# Bagaimana Caranya untuk mengintegrasikan KernelSU ke kernel non GKI?\n\n::: warning\nDokumen ini hanya untuk referensi arsip dan tidak lagi diperbarui.\nSejak KernelSU v1.0, kami telah menghentikan dukungan resmi untuk perangkat non-GKI.\n:::\n\nKernelSU dapat diintegrasikan ke kernel non GKI, dan saat ini sudah di-backport ke 4.14, dan juga dapat dijalankan pada kernel di bawah 4.14.\n\nKarena fragmentasi kernel non GKI, kami tidak memiliki cara yang seragam untuk membangunnya, sehingga kami tidak dapat menyediakan gambar boot non GKI. Tetapi Anda dapat membangun kernel sendiri dengan KernelSU yang terintegrasi.\n\nPertama, Anda harus dapat membangun kernel yang dapat di-boot dari kode sumber kernel, jika kernel tersebut tidak open source, maka akan sulit untuk menjalankan KernelSU untuk perangkat Anda.\n\nJika Anda dapat membuat kernel yang dapat di-booting, ada dua cara untuk mengintegrasikan KernelSU ke kode sumber kernel:\n\n1. Secara otomatis dengan `kprobe`\n2. Secara manual\n\n## Integrasikan dengan kprobe\n\nKernelSU menggunakan kprobe untuk melakukan hook kernel, jika *kprobe* berjalan dengan baik pada kernel Anda, maka disarankan untuk menggunakan cara ini.\n\nPertama, tambahkan KernelSU ke dalam berkas kernel source tree:\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.9.5\n```\n\n:::info\n[KernelSU 1.0 dan versi yang lebih baru tidak lagi mendukung kernel non-GKI](https://github.com/tiann/KernelSU/issues/1705). Versi terakhir yang didukung adalah `v0.9.5`, pastikan untuk menggunakan versi yang benar.\n:::\n\nKemudian, Anda harus memeriksa apakah *kprobe* diaktifkan dalam konfigurasi kernel Anda, jika tidak, tambahkan konfigurasi ini ke dalamnya:\n\n```\nCONFIG_KPROBES=y\nCONFIG_HAVE_KPROBES=y\nCONFIG_KPROBE_EVENTS=y\n```\n\nDan build kernel Anda lagi, KernelSU seharusnya bekerja dengan baik.\n\nJika Anda menemukan bahwa KPROBES masih belum diaktifkan, Anda dapat mencoba mengaktifkan `CONFIG_MODULES`. (Jika masih belum berlaku, gunakan `make menuconfig` untuk mencari ketergantungan KPROBES yang lain)\n\netapi jika Anda mengalami boot loop saat mengintegrasikan KernelSU, itu mungkin *kprobe rusak di kernel Anda*, Anda harus memperbaiki bug kprobe atau menggunakan cara kedua.\n\n## Memodifikasi sumber kernel secara manual\n\nJika kprobe tidak dapat bekerja pada kernel Anda (mungkin karena bug di upstream atau kernel di bawah 4.8), maka Anda dapat mencoba cara ini:\n\nPertama, tambahkan KernelSU ke dalam direktori kernel source tree:\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.9.5\n```\n\nKemudian, tambahkan panggilan KernelSU ke source kernel, berikut ini adalah patch yang dapat dirujuk:\n\n```diff\ndiff --git a/fs/exec.c b/fs/exec.c\nindex ac59664eaecf..bdd585e1d2cc 100644\n--- a/fs/exec.c\n+++ b/fs/exec.c\n@@ -1890,11 +1890,14 @@ static int __do_execve_file(int fd, struct filename *filename,\n \treturn retval;\n }\n \n+extern bool ksu_execveat_hook __read_mostly;\n+extern int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv,\n+\t\t\tvoid *envp, int *flags);\n+extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,\n+\t\t\t\t void *argv, void *envp, int *flags);\n static int do_execveat_common(int fd, struct filename *filename,\n \t\t\t      struct user_arg_ptr argv,\n \t\t\t      struct user_arg_ptr envp,\n \t\t\t      int flags)\n {\n+\tif (unlikely(ksu_execveat_hook))\n+\t\tksu_handle_execveat(&fd, &filename, &argv, &envp, &flags);\n+\telse\n+\t\tksu_handle_execveat_sucompat(&fd, &filename, &argv, &envp, &flags);\n \treturn __do_execve_file(fd, filename, argv, envp, flags, NULL);\n }\n```\n```diff\ndiff --git a/fs/open.c b/fs/open.c\nindex 05036d819197..965b84d486b8 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -348,6 +348,8 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn ksys_fallocate(fd, mode, offset, len);\n }\n \n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t int *flags);\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -355,6 +357,7 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n  */\n long do_faccessat(int dfd, const char __user *filename, int mode)\n {\n \tconst struct cred *old_cred;\n \tstruct cred *override_cred;\n \tstruct path path;\n \tstruct inode *inode;\n \tstruct vfsmount *mnt;\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n \n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n \n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n```diff\ndiff --git a/fs/read_write.c b/fs/read_write.c\nindex 650fc7e0f3a6..55be193913b6 100644\n--- a/fs/read_write.c\n+++ b/fs/read_write.c\n@@ -434,10 +434,14 @@ ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)\n }\n EXPORT_SYMBOL(kernel_read);\n \n+extern bool ksu_vfs_read_hook __read_mostly;\n+extern int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,\n+\t\t\tsize_t *count_ptr, loff_t **pos);\n ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)\n {\n \tssize_t ret;\n \n+\tif (unlikely(ksu_vfs_read_hook))\n+\t\tksu_handle_vfs_read(&file, &buf, &count, &pos);\n+\n \tif (!(file->f_mode & FMODE_READ))\n \t\treturn -EBADF;\n \tif (!(file->f_mode & FMODE_CAN_READ))\n```\n```diff\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 376543199b5a..82adcef03ecc 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -148,6 +148,8 @@ int vfs_statx_fd(unsigned int fd, struct kstat *stat,\n }\n EXPORT_SYMBOL(vfs_statx_fd);\n \n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+\n /**\n  * vfs_statx - Get basic and extra attributes by filename\n  * @dfd: A file descriptor representing the base dir for a relative filename\n@@ -170,6 +172,7 @@ int vfs_statx(int dfd, const char __user *filename, int flags,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_AUTOMOUNT;\n \n+\tksu_handle_stat(&dfd, &filename, &flags);\n \tif ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t       AT_EMPTY_PATH | KSTAT_QUERY_FLAGS)) != 0)\n \t\treturn -EINVAL;\n```\n\nAnda harus menemukan empat fungsi dalam kernel source:\n\n1. do_faccessat, usually in `fs/open.c`\n2. do_execveat_common, usually in `fs/exec.c`\n3. vfs_read, usually in `fs/read_write.c`\n4. vfs_statx, usually in `fs/stat.c`\n\nJika kernel anda tidak memiliki `vfs_statx`, maka gunakan `vfs_fstatat` alih-alih:\n\n```diff\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 068fdbcc9e26..5348b7bb9db2 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -87,6 +87,8 @@ int vfs_fstat(unsigned int fd, struct kstat *stat)\n }\n EXPORT_SYMBOL(vfs_fstat);\n \n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+\n int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \t\tint flag)\n {\n@@ -94,6 +96,8 @@ int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = 0;\n \n+\tksu_handle_stat(&dfd, &filename, &flag);\n+\n \tif ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t      AT_EMPTY_PATH)) != 0)\n \t\tgoto out;\n```\n\nUntuk kernel lebih awal dari 4.17, jika anda menemukan `do_faccessat`, hanya pergi ke definisi yang sama `faccessat` syscall dan tempatkan pemanggil di sini:\n\n```diff\ndiff --git a/fs/open.c b/fs/open.c\nindex 2ff887661237..e758d7db7663 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -355,6 +355,9 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn error;\n }\n \n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t        int *flags);\n+\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -370,6 +373,8 @@ SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n \n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n+\n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n\nUntuk mengaktifkan KernelSU yang dibangun dalam SafeMode, Anda juga harus memodifikasi `input_handle_event` di `drivers/input/input.c`:\n\n:::tip\nFitur ini sangat direkomendasikan, serta sangat membantu untuk memulihkan pada saat bootloop!\n:::\n\n```diff\ndiff --git a/drivers/input/input.c b/drivers/input/input.c\nindex 45306f9ef247..815091ebfca4 100755\n--- a/drivers/input/input.c\n+++ b/drivers/input/input.c\n@@ -367,10 +367,13 @@ static int input_get_disposition(struct input_dev *dev,\n \treturn disposition;\n }\n \n+extern bool ksu_input_hook __read_mostly;\n+extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value);\n+\n static void input_handle_event(struct input_dev *dev,\n \t\t\t       unsigned int type, unsigned int code, int value)\n {\n\tint disposition = input_get_disposition(dev, type, code, &value);\n+\n+\tif (unlikely(ksu_input_hook))\n+\t\tksu_handle_input_handle_event(&type, &code, &value);\n \n \tif (disposition != INPUT_IGNORE_EVENT && type != EV_SYN)\n \t\tadd_input_randomness(type, code, value);\n```\n\nTerakhir, edit `KernelSU/kernel/ksu.c` dan beri komentar pada `enable_sucompat()` lalu build kernel Anda lagi, KernelSU akan bekerja dengan baik.\n\n### How to backport path_umount\n\nAnda dapat membuat fitur \"Umount modules\" berfungsi pada kernel pra-GKI dengan membackport `path_umount` secara manual dari versi 5.9. Anda dapat menggunakan patch ini sebagai referensi:\n\n```diff\n--- a/fs/namespace.c\n+++ b/fs/namespace.c\n@@ -1739,6 +1739,39 @@ static inline bool may_mandlock(void)\n }\n #endif\n\n+static int can_umount(const struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\n+\tif (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW))\n+\t\treturn -EINVAL;\n+\tif (!may_mount())\n+\t\treturn -EPERM;\n+\tif (path->dentry != path->mnt->mnt_root)\n+\t\treturn -EINVAL;\n+\tif (!check_mnt(mnt))\n+\t\treturn -EINVAL;\n+\tif (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */\n+\t\treturn -EINVAL;\n+\tif (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN))\n+\t\treturn -EPERM;\n+\treturn 0;\n+}\n+\n+int path_umount(struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\tint ret;\n+\n+\tret = can_umount(path, flags);\n+\tif (!ret)\n+\t\tret = do_umount(mnt, flags);\n+\n+\t/* we mustn't call path_put() as that would clear mnt_expiry_mark */\n+\tdput(path->dentry);\n+\tmntput_no_expire(mnt);\n+\treturn ret;\n+}\n /*\n  * Now umount can handle mount points as well as block devices.\n  * This is important for filesystems which use unnamed block devices.\n```\n\nTerakhir, build kembali kernel Anda, dan KernelSU akan berfungsi dengan benar.\n"
  },
  {
    "path": "website/docs/id_ID/guide/installation.md",
    "content": "# Instalasi\n\n## Periksa apakah perangkat Anda didukung\n\nUnduh manajer KernelSU dari [GitHub Releases](https://github.com/tiann/KernelSU/releases) dan instal ke perangkat Anda:\n\n- Jika aplikasi menunjukkan `Unsupported`, itu berarti **Anda harus mengkompilasi kernel sendiri**, KernelSU tidak akan dan tidak pernah menyediakan file boot.img untuk Anda flash.\n- Jika aplikasi menunjukkan `Not installed`, maka perangkat Anda secara resmi didukung oleh KernelSU.\n\n::: info\nUntuk perangkat yang menunjukkan `Unsupported`, Anda dapat memeriksa daftar [Perangkat yang didukung tidak resmi](unofficially-support-devices.md). Anda dapat mengkompilasi kernel sendiri.\n:::\n\n## Cadangkan boot.img stok\n\nSebelum flashing, sangat penting untuk mencadangkan boot.img stok Anda. Jika Anda mengalami bootloop, Anda selalu dapat memulihkan sistem dengan mem-flash kembali ke boot pabrik stok menggunakan fastboot.\n\n::: warning\nFlashing dapat menyebabkan kehilangan data. Pastikan untuk melakukan langkah ini dengan baik sebelum melanjutkan ke langkah berikutnya! Anda juga dapat mencadangkan semua data di perangkat Anda jika diperlukan.\n:::\n\n## Pengetahuan yang diperlukan\n\n### ADB dan fastboot\n\nSecara default, Anda akan menggunakan alat ADB dan fastboot dalam tutorial ini, jadi jika Anda tidak mengetahuinya, kami sarankan menggunakan mesin pencari untuk mempelajarinya terlebih dahulu.\n\n### KMI\n\nKernel Module Interface (KMI), versi kernel dengan KMI yang sama **kompatibel**, inilah yang dimaksud dengan \"general\" dalam GKI; sebaliknya, jika KMI berbeda, maka kernel ini tidak kompatibel satu sama lain, dan mem-flash image kernel dengan KMI yang berbeda dari perangkat Anda dapat menyebabkan bootloop.\n\nSecara khusus, untuk perangkat GKI, format versi kernel harus sebagai berikut:\n\n```txt\nKernelRelease :=\nVersion.PatchLevel.SubLevel-AndroidRelease-KmiGeneration-suffix\nw      .x         .y       -zzz           -k            -something\n```\n\n`w.x-zzz-k` adalah versi KMI. Misalnya, jika versi kernel perangkat adalah `5.10.101-android12-9-g30979850fc20`, maka KMI-nya adalah `5.10-android12-9`. Secara teoritis, dapat boot secara normal dengan kernel KMI lainnya.\n\n::: tip\nPerhatikan bahwa SubLevel dalam versi kernel bukan bagian dari KMI! Ini berarti `5.10.101-android12-9-g30979850fc20` memiliki KMI yang sama dengan `5.10.137-android12-9-g30979850fc20`!\n:::\n\n### Tingkat patch keamanan {#security-patch-level}\n\nPerangkat Android yang lebih baru mungkin memiliki mekanisme anti-rollback yang mencegah flashing image boot dengan tingkat patch keamanan lama. Misalnya, jika kernel perangkat Anda adalah `5.10.101-android12-9-g30979850fc20`, tingkat patch keamanan adalah `2023-11`; bahkan jika Anda mem-flash kernel yang sesuai dengan KMI, jika tingkat patch keamanan lebih lama dari `2023-11` (seperti `2023-06`), ini dapat menyebabkan bootloop.\n\nOleh karena itu, kernel dengan tingkat patch keamanan terbaru lebih disukai untuk menjaga kompatibilitas dengan KMI.\n\n### Versi kernel vs versi Android\n\nHarap dicatat: **Versi kernel dan versi Android tidak harus sama!**\n\nJika Anda menemukan bahwa versi kernel Anda adalah `android12-5.10.101`, tetapi versi sistem Android Anda adalah Android 13 atau lainnya, jangan heran, karena nomor versi sistem Android tidak harus sama dengan nomor versi kernel Linux. Nomor versi kernel Linux umumnya sesuai dengan versi sistem Android yang disertakan dengan **perangkat saat dikirim**. Jika sistem Android diupgrade nanti, versi kernel umumnya tidak akan berubah. Jadi, sebelum mem-flash apa pun, **selalu rujuk versi kernel!**\n\n## Pendahuluan\n\nSejak versi [0.9.0](https://github.com/tiann/KernelSU/releases/tag/v0.9.0), KernelSU mendukung dua mode berjalan pada perangkat GKI:\n\n1. `GKI`: Ganti kernel asli perangkat dengan **Generic Kernel Image** (GKI) yang disediakan oleh KernelSU.\n2. `LKM`: Muat **Loadable Kernel Module** (LKM) ke dalam kernel perangkat tanpa mengganti kernel asli.\n\nKedua mode ini cocok untuk skenario yang berbeda, dan Anda dapat memilih salah satu sesuai kebutuhan Anda.\n\n### Mode GKI {#gki-mode}\n\nDalam mode GKI, kernel asli perangkat akan diganti dengan image kernel generik yang disediakan oleh KernelSU. Keuntungan mode GKI adalah:\n\n1. Universalitas yang kuat, cocok untuk sebagian besar perangkat. Misalnya, Samsung telah mengaktifkan perangkat KNOX, dan mode LKM tidak dapat berfungsi. Ada juga beberapa perangkat yang dimodifikasi khusus yang hanya dapat menggunakan mode GKI.\n2. Dapat digunakan tanpa bergantung pada firmware resmi, dan tidak perlu menunggu pembaruan firmware resmi, selama KMI konsisten, dapat digunakan.\n\n### Mode LKM {#lkm-mode}\n\nDalam mode LKM, kernel asli perangkat tidak akan diganti, tetapi loadable kernel module akan dimuat ke dalam kernel perangkat. Keuntungan mode LKM adalah:\n\n1. Tidak akan mengganti kernel asli perangkat. Jika Anda memiliki persyaratan khusus untuk kernel asli perangkat, atau Anda ingin menggunakan KernelSU sambil menggunakan kernel pihak ketiga, Anda dapat menggunakan mode LKM.\n2. Lebih nyaman untuk upgrade dan OTA. Saat mengupgrade KernelSU, Anda dapat langsung menginstalnya di manajer tanpa flashing manual. Setelah OTA sistem, Anda dapat langsung menginstalnya ke slot kedua tanpa flashing manual.\n3. Cocok untuk beberapa skenario khusus. Misalnya, LKM juga dapat dimuat dengan izin root sementara. Karena tidak perlu mengganti partisi boot, tidak akan memicu AVB dan tidak akan menyebabkan perangkat menjadi brick.\n4. LKM dapat di-uninstall sementara. Jika Anda ingin menonaktifkan akses root sementara, Anda dapat meng-uninstall LKM. Proses ini tidak memerlukan flashing partisi, atau bahkan me-reboot perangkat. Jika Anda ingin mengaktifkan root lagi, cukup reboot perangkat.\n\n::: tip KOEKSISTENSI DUA MODE\nSetelah membuka manajer, Anda dapat melihat mode perangkat saat ini di beranda. Perhatikan bahwa prioritas mode GKI lebih tinggi daripada LKM. Misalnya, jika Anda menggunakan kernel GKI untuk mengganti kernel asli, dan menggunakan LKM untuk mem-patch kernel GKI, LKM akan diabaikan, dan perangkat akan selalu berjalan dalam mode GKI.\n:::\n\n### Yang mana yang harus dipilih? {#which-one}\n\nJika perangkat Anda adalah ponsel, kami sarankan Anda memprioritaskan mode LKM. Jika perangkat Anda adalah emulator, WSA, atau Waydroid, kami sarankan Anda memprioritaskan mode GKI.\n\n## Instalasi LKM\n\n### Dapatkan firmware resmi\n\nUntuk menggunakan mode LKM, Anda perlu mendapatkan firmware resmi dan mem-patch-nya berdasarkan firmware resmi. Jika Anda menggunakan kernel pihak ketiga, Anda dapat menggunakan `boot.img` dari kernel pihak ketiga sebagai firmware resmi.\n\nAda banyak cara untuk mendapatkan firmware resmi. Jika perangkat Anda mendukung `fastboot boot`, kami merekomendasikan **metode yang paling direkomendasikan dan paling sederhana** adalah menggunakan `fastboot boot` untuk boot sementara kernel GKI yang disediakan oleh KernelSU, lalu instal manajer, dan akhirnya instal langsung di manajer. Metode ini tidak memerlukan pengunduhan firmware resmi secara manual atau ekstraksi boot secara manual.\n\nJika perangkat Anda tidak mendukung `fastboot boot`, Anda mungkin perlu mengunduh paket firmware resmi secara manual dan mengekstrak boot darinya.\n\nTidak seperti mode GKI, mode LKM memodifikasi `ramdisk`. Oleh karena itu, pada perangkat dengan Android 13, perlu mem-patch partisi `init_boot` alih-alih partisi `boot`, sedangkan mode GKI selalu beroperasi pada partisi `boot`.\n\n### Gunakan manajer\n\nBuka manajer, klik ikon instalasi di sudut kanan atas, dan beberapa opsi akan muncul:\n\n1. Pilih file. Jika perangkat Anda tidak memiliki hak root, Anda dapat memilih opsi ini lalu pilih firmware resmi Anda. Manajer akan secara otomatis mem-patch-nya. Setelah itu, flash file yang di-patch ini untuk mendapatkan hak root secara permanen.\n2. Instalasi langsung. Jika perangkat Anda sudah di-root, Anda dapat memilih opsi ini. Manajer akan secara otomatis mendapatkan informasi perangkat Anda, lalu secara otomatis mem-patch firmware resmi, dan mem-flash-nya secara otomatis. Anda dapat mempertimbangkan menggunakan `fastboot boot` kernel GKI KernelSU untuk mendapatkan root sementara dan menginstal manajer, lalu gunakan opsi ini. Ini juga merupakan cara utama untuk mengupgrade KernelSU.\n3. Instal ke slot yang tidak aktif. Jika perangkat Anda mendukung partisi A/B, Anda dapat memilih opsi ini. Manajer akan secara otomatis mem-patch firmware resmi dan menginstalnya ke partisi lain. Metode ini cocok untuk perangkat setelah OTA, Anda dapat langsung menginstalnya ke partisi lain setelah OTA, lalu restart perangkat.\n\n### Gunakan baris perintah\n\nJika Anda tidak ingin menggunakan manajer, Anda juga dapat menggunakan baris perintah untuk menginstal LKM. Alat `ksud` yang disediakan oleh KernelSU dapat membantu Anda mem-patch firmware resmi dengan cepat lalu mem-flash-nya.\n\nAlat ini mendukung macOS, Linux, dan Windows. Anda dapat mengunduh versi yang sesuai dari [GitHub Release](https://github.com/tiann/KernelSU/releases).\n\nPenggunaan: `ksud boot-patch` Anda dapat memeriksa bantuan baris perintah untuk opsi spesifik.\n\n```sh\noriole:/ # ksud boot-patch -h\nPatch boot or init_boot images to apply KernelSU\n\nUsage: ksud boot-patch [OPTIONS]\n\nOptions:\n  -b, --boot <BOOT>              Boot image path. If not specified, it will try to find the boot image automatically\n  -k, --kernel <KERNEL>          Kernel image path to be replaced\n  -m, --module <MODULE>          LKM module path to be replaced. If not specified, the built-in module will be used\n  -i, --init <INIT>              init to be replaced\n  -u, --ota                      Will use another slot if the boot image is not specified\n  -f, --flash                    Flash it to boot partition after patch\n  -o, --out <OUT>                Output path. If not specified, the current directory will be used\n      --magiskboot <MAGISKBOOT>  magiskboot path. If not specified, the built-in version will be used\n      --kmi <KMI>                KMI version. If specified, the indicated KMI will be used\n  -h, --help                     Print help\n```\n\nBeberapa opsi yang perlu dijelaskan:\n\n1. Opsi `--magiskboot` dapat menentukan path magiskboot. Jika tidak ditentukan, ksud akan mencarinya di variabel lingkungan. Jika Anda tidak tahu cara mendapatkan magiskboot, Anda dapat memeriksa [di sini](#patch-boot-image).\n2. Opsi `--kmi` dapat menentukan versi `KMI`. Jika nama kernel perangkat Anda tidak mengikuti spesifikasi KMI, Anda dapat menentukannya menggunakan opsi ini.\n\nPenggunaan paling umum adalah:\n\n```sh\nksud boot-patch -b <boot.img> --kmi android13-5.10\n```\n\n## Instalasi mode GKI\n\nAda beberapa metode instalasi untuk mode GKI, masing-masing cocok untuk skenario yang berbeda, jadi pilih sesuai:\n\n1. Instal dengan fastboot menggunakan boot.img yang disediakan oleh KernelSU.\n2. Instal dengan aplikasi flash kernel, seperti [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases).\n3. Perbaiki boot.img secara manual dan instal.\n4. Instal dengan Recovery kustom (misalnya, TWRP).\n\n## Instal dengan boot.img yang disediakan oleh KernelSU\n\nJika `boot.img` perangkat Anda menggunakan format kompresi yang umum digunakan, Anda dapat menggunakan image GKI yang disediakan oleh KernelSU untuk mem-flash-nya langsung. Ini tidak memerlukan TWRP atau self-patching image.\n\n### Temukan boot.img yang tepat\n\nKernelSU menyediakan boot.img generik untuk perangkat GKI, dan Anda harus mem-flash boot.img ke partisi boot perangkat.\n\nAnda dapat mengunduh boot.img dari [GitHub Release](https://github.com/tiann/KernelSU/releases). Harap dicatat bahwa Anda harus menggunakan versi boot.img yang benar. Jika Anda tidak tahu file mana yang harus diunduh, baca dengan cermat deskripsi [KMI](#kmi) dan [Tingkat patch keamanan](#security-patch-level) dalam dokumen ini.\n\nBiasanya, ada tiga file boot dalam format berbeda untuk KMI dan tingkat patch keamanan yang sama. Mereka identik kecuali format kompresi kernel. Harap periksa format kompresi kernel dari boot.img asli Anda. Anda harus menggunakan format yang benar, seperti `lz4`, `gz`. Jika Anda menggunakan format kompresi yang salah, Anda mungkin mengalami bootloop setelah mem-flash boot.img.\n\n::: info FORMAT KOMPRESI BOOT.IMG\n1. Anda dapat menggunakan magiskboot untuk mendapatkan format kompresi dari boot.img asli Anda. Atau, Anda juga dapat bertanya kepada anggota atau pengembang di komunitas yang memiliki model perangkat yang sama. Juga, format kompresi kernel biasanya tidak berubah, jadi jika Anda boot sukses dengan format kompresi tertentu, Anda dapat mencoba format itu nanti juga.\n2. Perangkat Xiaomi biasanya menggunakan `gz` atau `uncompressed`.\n3. Untuk perangkat Pixel, ikuti instruksi di bawah ini:\n:::\n\n### Flash boot.img ke perangkat\n\nGunakan `adb` untuk menghubungkan perangkat Anda, lalu jalankan `adb reboot bootloader` untuk masuk ke mode fastboot, dan gunakan perintah ini untuk mem-flash KernelSU:\n\n```sh\nfastboot flash boot boot.img\n```\n\n::: info\nJika perangkat Anda mendukung `fastboot boot`, Anda dapat terlebih dahulu menggunakan `fastboot boot boot.img` untuk mencoba menggunakan boot.img untuk boot sistem terlebih dahulu. Jika ada yang tidak terduga terjadi, restart lagi untuk boot.\n:::\n\n### Reboot\n\nSetelah flash selesai, Anda harus me-reboot perangkat Anda:\n\n```sh\nfastboot reboot\n```\n\n## Instal dengan Kernel Flasher\n\nLangkah-langkah:\n\n1. Unduh ZIP AnyKernel3. Jika Anda tidak tahu file mana yang harus diunduh, baca dengan cermat deskripsi [KMI](#kmi) dan [Tingkat patch keamanan](#security-patch-level) dalam dokumen ini.\n2. Buka aplikasi Kernel Flasher, berikan izin root yang diperlukan, dan gunakan ZIP AnyKernel3 yang disediakan untuk mem-flash.\n\nDengan cara ini memerlukan aplikasi Kernel Flasher memiliki izin root. Anda dapat menggunakan metode berikut untuk mencapai ini:\n\n1. Perangkat Anda telah di-root. Misalnya, Anda telah menginstal KernelSU dan ingin mengupgrade ke versi terbaru atau Anda telah di-root melalui metode lain (seperti Magisk).\n2. Jika perangkat Anda tidak di-root, tetapi perangkat mendukung metode boot sementara `fastboot boot boot.img`, Anda dapat menggunakan image GKI yang disediakan oleh KernelSU untuk boot sementara perangkat Anda, mendapatkan izin root sementara, lalu gunakan aplikasi Kernel Flash untuk mendapatkan hak root permanen.\n\nBeberapa aplikasi flashing kernel yang dapat digunakan untuk ini:\n\n1. [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases)\n2. [Franco Kernel Manager](https://play.google.com/store/apps/details?id=com.franco.kernel)\n3. [Ex Kernel Manager](https://play.google.com/store/apps/details?id=flar2.exkernelmanager)\n\nCatatan: Metode ini lebih nyaman saat mengupgrade KernelSU dan dapat dilakukan tanpa komputer (buat cadangan terlebih dahulu).\n\n## Patch boot.img secara manual {#patch-boot-image}\n\nUntuk beberapa perangkat, format boot.img tidak seperti `lz4`, `gz`, dan `uncompressed` yang umum. Contoh khas adalah Pixel, di mana boot.img dikompresi dalam format `lz4_legacy`, sedangkan ramdisk mungkin dalam `gz` atau juga dikompresi dalam `lz4_legacy`. Saat ini, jika Anda langsung mem-flash boot.img yang disediakan oleh KernelSU, perangkat mungkin tidak dapat boot. Dalam hal ini, Anda dapat mem-patch boot.img secara manual untuk mencapai ini.\n\nSelalu disarankan untuk menggunakan `magiskboot` untuk mem-patch image, ada dua cara:\n\n1. [magiskboot](https://github.com/topjohnwu/Magisk/releases)\n2. [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci)\n\nBuild resmi `magiskboot` hanya dapat berjalan di perangkat Android, jika Anda ingin menjalankannya di PC, Anda dapat mencoba opsi kedua.\n\n::: tip\nAndroid-Image-Kitchen tidak direkomendasikan untuk saat ini karena tidak menangani metadata boot (seperti tingkat patch keamanan) dengan benar. Oleh karena itu, mungkin tidak berfungsi pada beberapa perangkat.\n:::\n\n### Persiapan\n\n1. Dapatkan boot.img stok perangkat Anda. Anda bisa mendapatkannya dari produsen perangkat Anda. Anda mungkin memerlukan [payload-dumper-go](https://github.com/ssut/payload-dumper-go).\n2. Unduh file ZIP AnyKernel3 yang disediakan oleh KernelSU yang cocok dengan versi KMI perangkat Anda. Anda dapat merujuk ke [Instal dengan Recovery kustom](#install-with-custom-recovery).\n3. Unpack paket AnyKernel3 dan dapatkan file `Image`, yang merupakan file kernel KernelSU.\n\n### Menggunakan magiskboot di perangkat Android {#using-magiskboot-on-Android-devices}\n\n1. Unduh Magisk terbaru dari [GitHub Releases](https://github.com/topjohnwu/Magisk/releases).\n2. Ubah nama `Magisk-*(version).apk` menjadi `Magisk-*.zip` dan unzip.\n3. Push `Magisk-*/lib/arm64-v8a/libmagiskboot.so` ke perangkat Anda melalui ADB: `adb push Magisk-*/lib/arm64-v8a/libmagiskboot.so /data/local/tmp/magiskboot`\n4. Push stok boot.img dan Image di AnyKernel3 ke perangkat Anda.\n5. Masuk ke shell ADB dan jalankan direktori `cd /data/local/tmp/`, lalu `chmod +x magiskboot`\n6. Masuk ke shell ADB dan jalankan direktori `cd /data/local/tmp/`, jalankan `./magiskboot unpack boot.img` untuk unpack `boot.img`, Anda akan mendapatkan file `kernel`, ini adalah kernel stok Anda.\n7. Ganti `kernel` dengan `Image` dengan menjalankan perintah: `mv -f Image kernel`.\n8. Jalankan `./magiskboot repack boot.img` untuk repack image boot, dan Anda akan mendapatkan file `new-boot.img`, flash file ini ke perangkat melalui fastboot.\n\n### Menggunakan magiskboot di Windows/macOS/Linux PC {#using-magiskboot-on-PC}\n\n1. Unduh binary `magiskboot` yang sesuai untuk OS Anda dari [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci).\n2. Siapkan stok `boot.img` dan `Image` di PC Anda.\n3. Jalankan `chmod +x magiskboot`.\n4. Masuk ke direktori yang sesuai, jalankan `./magiskboot unpack boot.img` untuk unpack `boot.img`, Anda akan mendapatkan file `kernel`, ini adalah kernel stok Anda.\n5. Ganti `kernel` dengan `Image` dengan menjalankan perintah: `mv -f Image kernel`.\n6. Jalankan `./magiskboot repack boot.img` untuk repack image boot, dan Anda akan mendapatkan file `new-boot.img`, flash file ini ke perangkat melalui fastboot.\n\n::: info\n`magiskboot` resmi dapat berjalan di lingkungan `Linux` secara normal, jika Anda pengguna Linux, Anda dapat menggunakan build resmi.\n:::\n\n## Instal dengan Recovery kustom {#install-with-custom-recovery}\n\nPrasyarat: Perangkat Anda harus memiliki Recovery kustom, seperti TWRP. Jika tidak ada Recovery kustom yang tersedia untuk perangkat Anda, gunakan metode lain.\n\nLangkah-langkah:\n\n1. Di [GitHub Releases](https://github.com/tiann/KernelSU/releases), unduh paket ZIP yang dimulai dengan `AnyKernel3` yang cocok dengan versi perangkat Anda. Misalnya, jika versi kernel perangkat adalah `android12-5.10.66`, maka Anda harus mengunduh file `AnyKernel3-android12-5.10.66_yyyy-MM.zip` (di mana `yyyy` adalah tahun dan `MM` adalah bulan).\n2. Reboot perangkat ke TWRP.\n3. Gunakan ADB untuk menempatkan AnyKernel3-*.zip ke lokasi `/sdcard` perangkat dan pilih untuk menginstalnya di GUI TWRP, atau Anda dapat langsung menjalankan `adb sideload AnyKernel-*.zip` untuk menginstal.\n\nCatatan: Metode ini cocok untuk instalasi apa pun (tidak terbatas pada instalasi awal atau upgrade selanjutnya), selama Anda menggunakan TWRP.\n\n## Metode lain\n\nFaktanya, semua metode instalasi ini hanya memiliki satu ide utama, yaitu **mengganti kernel asli dengan yang disediakan oleh KernelSU**, selama ini dapat dicapai, dapat diinstal. Berikut adalah metode lain yang mungkin:\n\n1. Pertama, instal Magisk, dapatkan hak root melalui Magisk, lalu gunakan Kernel Flasher untuk mem-flash AnyKernel3 ZIP dari KernelSU.\n2. Gunakan toolkit flashing apa pun di PC untuk mem-flash kernel yang disediakan oleh KernelSU.\n\nNamun, jika tidak berhasil, coba pendekatan `magiskboot`.\n\n## Pasca-Instalasi: Dukungan Modul\n\n::: warning METAMODULE UNTUK MODIFIKASI FILE SISTEM\nJika Anda ingin menggunakan modul yang memodifikasi file `/system`, Anda perlu menginstal **metamodule** setelah menginstal KernelSU. Modul yang hanya menggunakan skrip, sepolicy, atau system.prop bekerja tanpa metamodule.\n:::\n\n**Untuk dukungan modifikasi `/system`**, silakan lihat [Panduan Metamodule](metamodule.md) untuk:\n- Memahami apa itu metamodule dan mengapa diperlukan\n- Menginstal metamodule `meta-overlayfs` resmi\n- Pelajari tentang opsi metamodule lainnya\n"
  },
  {
    "path": "website/docs/id_ID/guide/metamodule.md",
    "content": "# Metamodul\n\nMetamodul adalah fitur revolusioner di KernelSU yang mentransfer kemampuan sistem modul yang penting dari daemon inti ke modul yang dapat dipasang. Pergeseran arsitektur ini mempertahankan stabilitas dan keamanan KernelSU sambil melepaskan potensi inovasi yang lebih besar untuk ekosistem modul.\n\n## Apa itu Metamodul?\n\nMetamodul adalah jenis modul KernelSU khusus yang menyediakan fungsi infrastruktur inti untuk sistem modul. Tidak seperti modul biasa yang memodifikasi file sistem, metamodul mengontrol *bagaimana* modul biasa diinstal dan dipasang.\n\nMetamodul adalah mekanisme ekstensi berbasis plugin yang memungkinkan kustomisasi lengkap infrastruktur manajemen modul KernelSU. Dengan mendelegasikan logika pemasangan dan instalasi ke metamodul, KernelSU menghindari menjadi titik deteksi yang rapuh sambil memungkinkan strategi implementasi yang beragam.\n\n**Karakteristik utama:**\n\n- **Peran infrastruktur**: Metamodul menyediakan layanan yang diandalkan modul biasa\n- **Instans tunggal**: Hanya satu metamodul yang dapat diinstal pada satu waktu\n- **Eksekusi prioritas**: Skrip metamodul berjalan sebelum skrip modul biasa\n- **Hook khusus**: Menyediakan tiga skrip hook untuk instalasi, pemasangan, dan pembersihan\n\n## Mengapa Metamodul?\n\nSolusi root tradisional memasukkan logika pemasangan ke dalam inti mereka, membuat mereka lebih mudah dideteksi dan lebih sulit untuk berkembang. Arsitektur metamodul KernelSU memecahkan masalah ini melalui pemisahan perhatian.\n\n**Keunggulan strategis:**\n\n- **Mengurangi permukaan deteksi**: KernelSU sendiri tidak melakukan pemasangan, mengurangi vektor deteksi\n- **Stabilitas**: Daemon inti tetap stabil sementara implementasi pemasangan dapat berkembang\n- **Inovasi**: Komunitas dapat mengembangkan strategi pemasangan alternatif tanpa mem-fork KernelSU\n- **Pilihan**: Pengguna dapat memilih implementasi yang paling sesuai dengan kebutuhan mereka\n\n**Fleksibilitas pemasangan:**\n\n- **Tanpa pemasangan**: Untuk pengguna dengan modul tanpa pemasangan saja, hindari overhead pemasangan sepenuhnya\n- **Pemasangan OverlayFS**: Pendekatan tradisional dengan dukungan lapisan baca-tulis (melalui `meta-overlayfs`)\n- **Magic mount**: Pemasangan kompatibel Magisk untuk kompatibilitas aplikasi yang lebih baik\n- **Implementasi kustom**: Overlay berbasis FUSE, pemasangan VFS kustom, atau pendekatan yang sama sekali baru\n\n**Melampaui pemasangan:**\n\n- **Ekstensibilitas**: Tambahkan fitur seperti dukungan modul kernel tanpa memodifikasi inti KernelSU\n- **Modularitas**: Perbarui implementasi secara independen dari rilis KernelSU\n- **Kustomisasi**: Buat solusi khusus untuk perangkat atau kasus penggunaan tertentu\n\n::: warning PENTING\nTanpa metamodul yang diinstal, modul **TIDAK** akan dipasang. Instalasi KernelSU yang baru memerlukan pemasangan metamodul (seperti `meta-overlayfs`) agar modul berfungsi.\n:::\n\n## Untuk Pengguna\n\n### Menginstal Metamodul\n\nInstal metamodul dengan cara yang sama seperti modul biasa:\n\n1. Unduh file ZIP metamodul (misalnya, `meta-overlayfs.zip`)\n2. Buka aplikasi KernelSU Manager\n3. Ketuk tombol tindakan mengambang (➕)\n4. Pilih file ZIP metamodul\n5. Reboot perangkat Anda\n\nMetamodul `meta-overlayfs` adalah implementasi referensi resmi yang menyediakan pemasangan modul berbasis overlayfs tradisional dengan dukungan image ext4.\n\n### Memeriksa Metamodul Aktif\n\nAnda dapat memeriksa metamodul mana yang saat ini aktif di halaman Modul aplikasi KernelSU Manager. Metamodul aktif akan ditampilkan di daftar modul Anda dengan penunjukan khususnya.\n\n### Menghapus Instalasi Metamodul\n\n::: danger PERINGATAN\nMenghapus instalasi metamodul akan memengaruhi **SEMUA** modul. Setelah dihapus, modul tidak akan lagi dipasang hingga Anda menginstal metamodul lain.\n:::\n\nUntuk menghapus instalasi:\n\n1. Buka KernelSU Manager\n2. Temukan metamodul di daftar modul Anda\n3. Ketuk hapus instalasi (Anda akan melihat peringatan khusus)\n4. Konfirmasi tindakan\n5. Reboot perangkat Anda\n\nSetelah menghapus instalasi, Anda harus menginstal metamodul lain jika Anda ingin modul terus berfungsi.\n\n### Batasan Metamodul Tunggal\n\nHanya satu metamodul yang dapat diinstal pada satu waktu. Jika Anda mencoba menginstal metamodul kedua, KernelSU akan mencegah instalasi untuk menghindari konflik.\n\nUntuk beralih metamodul:\n\n1. Hapus instalasi semua modul biasa\n2. Hapus instalasi metamodul saat ini\n3. Reboot\n4. Instal metamodul baru\n5. Instal ulang modul biasa Anda\n6. Reboot lagi\n\n## Untuk Pengembang Modul\n\nJika Anda mengembangkan modul KernelSU biasa, Anda tidak perlu terlalu khawatir tentang metamodul. Modul Anda akan berfungsi selama pengguna memiliki metamodul yang kompatibel (seperti `meta-overlayfs`) yang diinstal.\n\n**Yang perlu Anda ketahui:**\n\n- **Pemasangan memerlukan metamodul**: Direktori `system` di modul Anda hanya akan dipasang jika pengguna memiliki metamodul yang diinstal yang menyediakan fungsi pemasangan\n- **Tidak perlu perubahan kode**: Modul yang ada terus berfungsi tanpa modifikasi\n\n::: tip\nJika Anda terbiasa dengan pengembangan modul Magisk, modul Anda akan berfungsi dengan cara yang sama di KernelSU ketika metamodul diinstal, karena menyediakan pemasangan kompatibel Magisk.\n:::\n\n## Untuk Pengembang Metamodul\n\nMembuat metamodul memungkinkan Anda untuk menyesuaikan bagaimana KernelSU menangani instalasi modul, pemasangan, dan penghapusan instalasi.\n\n### Persyaratan Dasar\n\nMetamodul diidentifikasi oleh properti khusus di `module.prop`:\n\n```txt\nid=my_metamodule\nname=My Custom Metamodule\nversion=1.0\nversionCode=1\nauthor=Your Name\ndescription=Custom module mounting implementation\nmetamodule=1\n```\n\nProperti `metamodule=1` (atau `metamodule=true`) menandai ini sebagai metamodul. Tanpa properti ini, modul akan diperlakukan sebagai modul biasa.\n\n### Struktur File\n\nStruktur metamodul:\n\n```txt\nmy_metamodule/\n├── module.prop              (harus menyertakan metamodule=1)\n│\n│      *** Hook khusus metamodul ***\n├── metamount.sh             (opsional: handler mount kustom)\n├── metainstall.sh           (opsional: hook instalasi untuk modul biasa)\n├── metauninstall.sh         (opsional: hook pembersihan untuk modul biasa)\n│\n│      *** File modul standar (semua opsional) ***\n├── customize.sh             (kustomisasi instalasi)\n├── post-fs-data.sh          (skrip tahap post-fs-data)\n├── service.sh               (skrip late_start service)\n├── boot-completed.sh        (skrip boot selesai)\n├── uninstall.sh             (skrip penghapusan instalasi metamodul sendiri)\n├── system/                  (modifikasi systemless, jika diperlukan)\n└── [file tambahan apa pun]\n```\n\nMetamodul dapat menggunakan semua fitur modul standar (skrip siklus hidup, dll.) selain hook metamodul khusus mereka.\n\n### Skrip Hook\n\nMetamodul dapat menyediakan hingga tiga skrip hook khusus:\n\n#### 1. metamount.sh - Handler Mount\n\n**Tujuan**: Mengontrol bagaimana modul dipasang selama boot.\n\n**Kapan dieksekusi**: Selama tahap `post-fs-data`, sebelum skrip modul apa pun berjalan.\n\n**Variabel lingkungan:**\n\n- `MODDIR`: Path direktori metamodul (misalnya, `/data/adb/modules/my_metamodule`)\n- Semua variabel lingkungan KernelSU standar\n\n**Tanggung jawab:**\n\n- Pasang semua modul yang diaktifkan secara systemless\n- Periksa flag `skip_mount`\n- Tangani persyaratan pemasangan khusus modul\n\n::: danger PERSYARATAN KRITIS\nSaat melakukan operasi mount, Anda **HARUS** mengatur nama sumber/perangkat ke `\"KSU\"`. Ini mengidentifikasi mount sebagai milik KernelSU.\n\n**Contoh (benar):**\n\n```sh\nmount -t overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work KSU /target\n```\n\n**Untuk API mount modern**, atur string sumber:\n\n```rust\nfsconfig_set_string(fs, \"source\", \"KSU\")?;\n```\n\nIni penting agar KernelSU mengidentifikasi dan mengelola mount-nya dengan benar.\n:::\n\n**Contoh skrip:**\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\n\n# Contoh: Implementasi bind mount sederhana\nfor module in /data/adb/modules/*; do\n    if [ -f \"$module/disable\" ] || [ -f \"$module/skip_mount\" ]; then\n        continue\n    fi\n\n    if [ -d \"$module/system\" ]; then\n        # Mount dengan source=KSU (DIPERLUKAN!)\n        mount -o bind,dev=KSU \"$module/system\" /system\n    fi\ndone\n```\n\n#### 2. metainstall.sh - Hook Instalasi\n\n**Tujuan**: Sesuaikan bagaimana modul biasa diinstal.\n\n**Kapan dieksekusi**: Selama instalasi modul, setelah file diekstrak tetapi sebelum instalasi selesai. Skrip ini **di-source** (tidak dieksekusi) oleh installer bawaan, mirip dengan cara kerja `customize.sh`.\n\n**Variabel lingkungan dan fungsi:**\n\nSkrip ini mewarisi semua variabel dan fungsi dari `install.sh` bawaan:\n\n- **Variabel**: `MODPATH`, `TMPDIR`, `ZIPFILE`, `ARCH`, `API`, `IS64BIT`, `KSU`, `KSU_VER`, `KSU_VER_CODE`, `BOOTMODE`, dll.\n- **Fungsi**:\n  - `ui_print <msg>` - Cetak pesan ke konsol\n  - `abort <msg>` - Cetak error dan hentikan instalasi\n  - `set_perm <target> <owner> <group> <permission> [context]` - Atur izin file\n  - `set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]` - Atur izin secara rekursif\n  - `install_module` - Panggil proses instalasi modul bawaan\n\n**Kasus penggunaan:**\n\n- Proses file modul sebelum atau sesudah instalasi bawaan (panggil `install_module` ketika siap)\n- Pindahkan file modul\n- Validasi kompatibilitas modul\n- Siapkan struktur direktori khusus\n- Inisialisasi sumber daya khusus modul\n\n**Catatan**: Skrip ini **TIDAK** dipanggil saat menginstal metamodul itu sendiri.\n\n#### 3. metauninstall.sh - Hook Pembersihan\n\n**Tujuan**: Bersihkan sumber daya ketika modul biasa dihapus instalasi.\n\n**Kapan dieksekusi**: Selama penghapusan instalasi modul, sebelum direktori modul dihapus.\n\n**Variabel lingkungan:**\n\n- `MODULE_ID`: ID modul yang sedang dihapus instalasi\n\n**Kasus penggunaan:**\n\n- Proses file\n- Bersihkan symlink\n- Bebaskan sumber daya yang dialokasikan\n- Perbarui pelacakan internal\n\n**Contoh skrip:**\n\n```sh\n#!/system/bin/sh\n# Dipanggil saat menghapus instalasi modul biasa\nMODULE_ID=\"$1\"\nIMG_MNT=\"/data/adb/metamodule/mnt\"\n\n# Hapus file modul dari image\nif [ -d \"$IMG_MNT/$MODULE_ID\" ]; then\n    rm -rf \"$IMG_MNT/$MODULE_ID\"\nfi\n```\n\n### Urutan Eksekusi\n\nMemahami urutan eksekusi boot sangat penting untuk pengembangan metamodul:\n\n```txt\ntahap post-fs-data:\n  1. Skrip post-fs-data.d umum dieksekusi\n  2. Prune modul, restorecon, muat sepolicy.rule\n  3. post-fs-data.sh metamodul dieksekusi (jika ada)\n  4. post-fs-data.sh modul biasa dieksekusi\n  5. Muat system.prop\n  6. metamount.sh metamodul dieksekusi\n     └─> Pasang semua modul secara systemless\n  7. Tahap post-mount.d berjalan\n     - Skrip post-mount.d umum\n     - post-mount.sh metamodul (jika ada)\n     - post-mount.sh modul biasa\n\ntahap service:\n  1. Skrip service.d umum dieksekusi\n  2. service.sh metamodul dieksekusi (jika ada)\n  3. service.sh modul biasa dieksekusi\n\ntahap boot-completed:\n  1. Skrip boot-completed.d umum dieksekusi\n  2. boot-completed.sh metamodul dieksekusi (jika ada)\n  3. boot-completed.sh modul biasa dieksekusi\n```\n\n**Poin penting:**\n\n- `metamount.sh` berjalan **SETELAH** semua skrip post-fs-data (baik metamodul maupun modul biasa)\n- Skrip siklus hidup metamodul (`post-fs-data.sh`, `service.sh`, `boot-completed.sh`) selalu berjalan sebelum skrip modul biasa\n- Skrip umum di direktori `.d` berjalan sebelum skrip metamodul\n- Tahap `post-mount` berjalan setelah pemasangan selesai\n\n### Mekanisme Symlink\n\nKetika metamodul diinstal, KernelSU membuat symlink:\n\n```sh\n/data/adb/metamodule -> /data/adb/modules/<metamodule_id>\n```\n\nIni menyediakan path yang stabil untuk mengakses metamodul aktif, terlepas dari ID-nya.\n\n**Manfaat:**\n\n- Path akses yang konsisten\n- Deteksi mudah metamodul aktif\n- Menyederhanakan konfigurasi\n\n### Contoh Dunia Nyata: meta-overlayfs\n\nMetamodul `meta-overlayfs` adalah implementasi referensi resmi. Ini menunjukkan praktik terbaik untuk pengembangan metamodul.\n\n#### Arsitektur\n\n`meta-overlayfs` menggunakan **arsitektur dual-directory**:\n\n1. **Direktori metadata**: `/data/adb/modules/`\n   - Berisi `module.prop`, `disable`, penanda `skip_mount`\n   - Cepat untuk dipindai selama boot\n   - Jejak penyimpanan kecil\n\n2. **Direktori konten**: `/data/adb/metamodule/mnt/`\n   - Berisi file modul aktual (system, vendor, product, dll.)\n   - Disimpan dalam image ext4 (`modules.img`)\n   - Dioptimalkan ruang dengan fitur ext4\n\n#### Implementasi metamount.sh\n\nBerikut adalah cara `meta-overlayfs` mengimplementasikan handler mount:\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\nIMG_FILE=\"$MODDIR/modules.img\"\nMNT_DIR=\"$MODDIR/mnt\"\n\n# Pasang image ext4 jika belum dipasang\nif ! mountpoint -q \"$MNT_DIR\"; then\n    mkdir -p \"$MNT_DIR\"\n    mount -t ext4 -o loop,rw,noatime \"$IMG_FILE\" \"$MNT_DIR\"\nfi\n\n# Atur variabel lingkungan untuk dukungan dual-directory\nexport MODULE_METADATA_DIR=\"/data/adb/modules\"\nexport MODULE_CONTENT_DIR=\"$MNT_DIR\"\n\n# Eksekusi binary mount\n# (Logika pemasangan aktual ada di binary Rust)\n\"$MODDIR/meta-overlayfs\"\n```\n\n#### Fitur Utama\n\n**Pemasangan Overlayfs:**\n\n- Menggunakan overlayfs kernel untuk modifikasi systemless yang sebenarnya\n- Mendukung beberapa partisi (system, vendor, product, system_ext, odm, oem)\n- Dukungan lapisan baca-tulis melalui `/data/adb/modules/.rw/`\n\n**Identifikasi sumber:**\n\n```rust\n// Dari meta-overlayfs/src/mount.rs\nfsconfig_set_string(fs, \"source\", \"KSU\")?;  // DIPERLUKAN!\n```\n\nIni mengatur `dev=KSU` untuk semua mount overlay, memungkinkan identifikasi yang tepat.\n\n### Praktik Terbaik\n\nSaat mengembangkan metamodul:\n\n1. **Selalu atur sumber ke \"KSU\"** untuk operasi mount - umount kernel dan umount zygisksu memerlukan ini untuk umount dengan benar\n2. **Tangani error dengan baik** - proses boot sensitif terhadap waktu\n3. **Hormati flag standar** - dukung `skip_mount` dan `disable`\n4. **Log operasi** - gunakan `echo` atau logging untuk debugging\n5. **Tes secara menyeluruh** - error pemasangan dapat menyebabkan boot loop\n6. **Dokumentasikan perilaku** - jelaskan dengan jelas apa yang dilakukan metamodul Anda\n7. **Sediakan jalur migrasi** - bantu pengguna beralih dari solusi lain\n\n### Menguji Metamodul Anda\n\nSebelum merilis:\n\n1. **Uji instalasi** pada pengaturan KernelSU yang bersih\n2. **Verifikasi pemasangan** dengan berbagai jenis modul\n3. **Periksa kompatibilitas** dengan modul umum\n4. **Uji penghapusan instalasi** dan pembersihan\n5. **Validasi kinerja boot** (metamount.sh memblokir!)\n6. **Pastikan penanganan error yang tepat** untuk menghindari boot loop\n\n## Pertanyaan yang Sering Diajukan\n\n### Apakah saya memerlukan metamodul?\n\n**Untuk pengguna**: Hanya jika Anda ingin menggunakan modul yang memerlukan pemasangan. Jika Anda hanya menggunakan modul yang menjalankan skrip tanpa memodifikasi file sistem, Anda tidak memerlukan metamodul.\n\n**Untuk pengembang modul**: Tidak, Anda mengembangkan modul secara normal. Pengguna memerlukan metamodul hanya jika modul Anda memerlukan pemasangan.\n\n**Untuk pengguna lanjutan**: Hanya jika Anda ingin menyesuaikan perilaku pemasangan atau membuat implementasi pemasangan alternatif.\n\n### Bisakah saya memiliki beberapa metamodul?\n\nTidak. Hanya satu metamodul yang dapat diinstal pada satu waktu. Ini mencegah konflik dan memastikan perilaku yang dapat diprediksi.\n\n### Apa yang terjadi jika saya menghapus instalasi satu-satunya metamodul saya?\n\nModul tidak akan lagi dipasang. Perangkat Anda akan boot secara normal, tetapi modifikasi modul tidak akan diterapkan hingga Anda menginstal metamodul lain.\n\n### Apakah meta-overlayfs diperlukan?\n\nTidak. Ini menyediakan pemasangan overlayfs standar yang kompatibel dengan sebagian besar modul. Anda dapat membuat metamodul Anda sendiri jika Anda memerlukan perilaku yang berbeda.\n\n## Lihat Juga\n\n- [Panduan Modul](module.md) - Pengembangan modul umum\n- [Perbedaan dengan Magisk](difference-with-magisk.md) - Membandingkan KernelSU dan Magisk\n- [Cara Membangun](how-to-build.md) - Membangun KernelSU dari sumber\n"
  },
  {
    "path": "website/docs/id_ID/guide/module-config.md",
    "content": "# Konfigurasi Modul\n\nKernelSU menyediakan sistem konfigurasi bawaan yang memungkinkan modul menyimpan pengaturan key-value persisten atau sementara. Konfigurasi disimpan dalam format biner di `/data/adb/ksu/module_configs/<module_id>/` dengan karakteristik berikut:\n\n## Tipe Konfigurasi\n\n- **Konfigurasi Persisten** (`persist.config`): Bertahan setelah reboot hingga dihapus secara eksplisit atau modul di-uninstall\n- **Konfigurasi Sementara** (`tmp.config`): Otomatis dihapus selama tahap post-fs-data pada setiap boot\n\nSaat membaca konfigurasi, nilai sementara lebih diprioritaskan daripada nilai persisten untuk key yang sama.\n\n## Menggunakan Konfigurasi dalam Skrip Modul\n\nSemua skrip modul (`post-fs-data.sh`, `service.sh`, `boot-completed.sh`, dll.) berjalan dengan variabel lingkungan `KSU_MODULE` diatur ke ID modul. Anda dapat menggunakan perintah `ksud module config` untuk mengelola konfigurasi modul Anda:\n\n```bash\n# Mendapatkan nilai konfigurasi\nvalue=$(ksud module config get my_setting)\n\n# Mengatur nilai konfigurasi persisten\nksud module config set my_setting \"some value\"\n\n# Mengatur nilai konfigurasi sementara (dihapus setelah reboot)\nksud module config set --temp runtime_state \"active\"\n\n# Mengatur nilai dari stdin (berguna untuk teks multiline atau data kompleks)\nksud module config set my_key <<EOF\nteks multiline\nnilai\nEOF\n\n# Atau alirkan dari perintah\necho \"value\" | ksud module config set my_key\n\n# Bendera stdin eksplisit\ncat file.json | ksud module config set json_data --stdin\n\n# Daftar semua entri konfigurasi (gabungan persisten dan sementara)\nksud module config list\n\n# Menghapus entri konfigurasi\nksud module config delete my_setting\n\n# Menghapus entri konfigurasi sementara\nksud module config delete --temp runtime_state\n\n# Menghapus semua konfigurasi persisten\nksud module config clear\n\n# Menghapus semua konfigurasi sementara\nksud module config clear --temp\n```\n\n## Batasan Validasi\n\nSistem konfigurasi memberlakukan batasan berikut:\n\n- **Panjang key maksimum**: 256 byte\n- **Panjang nilai maksimum**: 1MB (1048576 byte)\n- **Jumlah entri konfigurasi maksimum**: 32 per modul\n- **Format key**: Harus cocok dengan `^[a-zA-Z][a-zA-Z0-9._-]+$` (seperti ID modul)\n  - Harus dimulai dengan huruf\n  - Dapat berisi huruf, angka, titik, garis bawah, atau tanda hubung\n  - Panjang minimum: 2 karakter\n- **Format nilai**: Tanpa batasan - dapat berisi karakter UTF-8 apa pun, termasuk jeda baris dan karakter kontrol\n  - Disimpan dalam format biner dengan awalan panjang untuk penanganan data yang aman\n\n## Siklus Hidup\n\n- **Saat boot**: Semua konfigurasi sementara dihapus selama tahap post-fs-data\n- **Saat uninstall modul**: Semua konfigurasi (persisten dan sementara) otomatis dihapus\n- Konfigurasi disimpan dalam format biner dengan magic number `0x4b53554d` (\"KSUM\") dan validasi versi\n\n## Kasus Penggunaan\n\nSistem konfigurasi ideal untuk:\n\n- **Preferensi pengguna**: Menyimpan pengaturan modul yang dikonfigurasi pengguna melalui WebUI atau skrip action\n- **Flag fitur**: Mengaktifkan/menonaktifkan fitur modul tanpa menginstal ulang\n- **Status runtime**: Melacak status sementara yang harus direset saat reboot (gunakan konfigurasi sementara)\n- **Pengaturan instalasi**: Mengingat pilihan yang dibuat saat instalasi modul\n- **Data kompleks**: Menyimpan JSON, teks multiline, data terenkode Base64, atau konten terstruktur apa pun (hingga 1MB)\n\n::: tip PRAKTIK TERBAIK\n- Gunakan konfigurasi persisten untuk preferensi pengguna yang harus bertahan setelah reboot\n- Gunakan konfigurasi sementara untuk status runtime atau flag fitur yang harus direset saat boot\n- Validasi nilai konfigurasi dalam skrip Anda sebelum menggunakannya\n- Gunakan perintah `ksud module config list` untuk men-debug masalah konfigurasi\n:::\n\n## Fitur Lanjutan\n\nSistem konfigurasi modul menyediakan kunci konfigurasi khusus untuk kasus penggunaan lanjutan:\n\n### Mengganti Deskripsi Modul {#overriding-module-description}\n\nAnda dapat mengganti field `description` dari `module.prop` secara dinamis dengan mengatur kunci konfigurasi `override.description`:\n\n```bash\n# Mengganti deskripsi modul\nksud module config set override.description \"Deskripsi kustom yang ditampilkan di pengelola\"\n```\n\nSaat mengambil daftar modul, jika konfigurasi `override.description` ada, itu akan menggantikan deskripsi asli dari `module.prop`. Ini berguna untuk:\n- Menampilkan informasi status dinamis dalam deskripsi modul\n- Menunjukkan detail konfigurasi runtime kepada pengguna\n- Memperbarui deskripsi berdasarkan status modul tanpa menginstal ulang\n\n### Mendeklarasikan Fitur yang Dikelola\n\nModul dapat mendeklarasikan fitur KernelSU mana yang mereka kelola menggunakan pola konfigurasi `manage.<feature>`. Fitur yang didukung sesuai dengan enum internal `FeatureId` KernelSU:\n\n**Fitur yang Didukung:**\n- `su_compat` - Mode kompatibilitas SU\n- `kernel_umount` - Unmount otomatis kernel\n\n```bash\n# Mendeklarasikan bahwa modul ini mengelola kompatibilitas SU dan mengaktifkannya\nksud module config set manage.su_compat true\n\n# Mendeklarasikan bahwa modul ini mengelola unmount kernel dan menonaktifkannya\nksud module config set manage.kernel_umount false\n\n# Menghapus pengelolaan fitur (modul tidak lagi mengontrol fitur ini)\nksud module config delete manage.su_compat\n```\n\n**Cara kerjanya:**\n- Keberadaan kunci `manage.<feature>` menunjukkan bahwa modul mengelola fitur tersebut\n- Nilai menunjukkan status yang diinginkan: `true`/`1` untuk diaktifkan, `false`/`0` (atau nilai lainnya) untuk dinonaktifkan\n- Untuk berhenti mengelola fitur, hapus kunci konfigurasi sepenuhnya\n\nFitur yang dikelola diekspos melalui API daftar modul sebagai field `managedFeatures` (string yang dipisahkan koma). Ini memungkinkan:\n- Pengelola KernelSU mendeteksi modul mana yang mengelola fitur KernelSU mana\n- Pencegahan konflik ketika beberapa modul mencoba mengelola fitur yang sama\n- Koordinasi yang lebih baik antara modul dan fungsionalitas inti KernelSU\n\n::: warning HANYA FITUR YANG DIDUKUNG\nGunakan hanya nama fitur yang telah ditentukan sebelumnya yang tercantum di atas (`su_compat`, `kernel_umount`). Ini sesuai dengan fitur internal KernelSU yang sebenarnya. Menggunakan nama fitur lain tidak akan menyebabkan error, tetapi tidak memiliki tujuan fungsional.\n:::\n"
  },
  {
    "path": "website/docs/id_ID/guide/module-webui.md",
    "content": "# Module WebUI\n\nSelain menjalankan skrip boot dan memodifikasi berkas sistem, modul KernelSU dapat menampilkan antarmuka dan berinteraksi langsung dengan pengguna.\n\nModul dapat mendefinisikan halaman HTML + CSS + JavaScript dengan teknologi web apa pun. Manajer KernelSU menampilkan halaman tersebut lewat WebView dan menyediakan API untuk berinteraksi dengan sistem, misalnya menjalankan perintah shell.\n\n## Direktori `webroot`\n\nBerkas sumber daya web harus ditempatkan di subdirektori `webroot` di direktori root modul, dan **HARUS** ada berkas bernama `index.html` sebagai pintu masuk halaman modul. Struktur modul paling sederhana yang mempunyai antarmuka web adalah sebagai berikut:\n\n```txt\n❯ tree .\n.\n|-- module.prop\n`-- webroot\n    `-- index.html\n```\n\n::: warning\nSaat modul dipasang, KernelSU otomatis mengatur izin serta konteks SELinux untuk direktori ini. Jika Anda tidak paham benar apa yang dilakukan, jangan mengubah izin direktori ini sendiri!\n:::\n\nJika halaman Anda memiliki CSS atau JavaScript, letakkan juga di direktori ini.\n\n## JavaScript API\n\nJika hanya berupa halaman tampilan, ia akan berfungsi layaknya halaman web biasa. Namun yang paling penting, KernelSU menyediakan serangkaian API sistem sehingga modul bisa mewujudkan fungsi khususnya.\n\nKernelSU menyediakan pustaka JavaScript yang dirilis di [npm](https://www.npmjs.com/package/kernelsu) dan bisa dipakai pada kode JavaScript halaman Anda.\n\nSebagai contoh, Anda bisa menjalankan perintah shell untuk memperoleh konfigurasi tertentu atau mengubah suatu properti:\n\n```JavaScript\nimport { exec } from 'kernelsu';\n\nconst { errno, stdout } = exec(\"getprop ro.product.model\");\n```\n\nAnda juga dapat membuat halaman menjadi layar penuh atau menampilkan toast.\n\n[Dokumentasi API](https://www.npmjs.com/package/kernelsu)\n\nJika API yang ada belum memenuhi kebutuhan atau kurang nyaman digunakan, silakan beri kami masukan [di sini](https://github.com/tiann/KernelSU/issues)!\n\n## Beberapa tips\n\n1. Anda dapat menggunakan `localStorage` seperti biasa untuk menyimpan data, namun ingat bahwa data akan hilang jika aplikasi manager dihapus. Jika butuh penyimpanan permanen, simpan data secara manual pada direktori tertentu.\n2. Untuk halaman sederhana kami menyarankan menggunakan [parceljs](https://parceljs.org/) untuk proses bundling. Ia tidak butuh konfigurasi awal dan sangat mudah dipakai. Namun jika Anda ahli front-end atau punya preferensi sendiri, silakan gunakan alat pilihan Anda!\n"
  },
  {
    "path": "website/docs/id_ID/guide/module.md",
    "content": "# Panduan module\n\nKernelSU menyediakan mekanisme modul yang mencapai efek memodifikasi direktori sistem dengan tetap menjaga integritas partisi sistem. Mekanisme ini umumnya dikenal sebagai \"tanpa sistem\".\n\nMekanisme modul KernelSU hampir sama dengan Magisk. Jika Anda terbiasa dengan pengembangan modul Magisk, mengembangkan modul KernelSU sangat mirip. Anda dapat melewati pengenalan modul di bawah ini dan hanya perlu membaca [difference-with-magisk](difference-with-magisk.md).\n\n::: warning METAMODULE HANYA DIPERLUKAN UNTUK MODIFIKASI FILE SISTEM\nKernelSU menggunakan arsitektur [metamodule](metamodule.md) untuk me-mount direktori `system`. **Hanya jika modul Anda perlu memodifikasi file `/system`** (melalui direktori `system`) Anda perlu menginstal metamodule (seperti [meta-overlayfs](https://github.com/tiann/KernelSU/releases)). Fitur modul lainnya seperti skrip, aturan sepolicy, dan system.prop bekerja tanpa metamodule.\n:::\n\n## WebUI\n\nModul KernelSU mendukung tampilan antarmuka dan interaksi dengan pengguna. Lihat [WebUI documentation](module-webui.md) untuk detailnya.\n\n## Konfigurasi Modul\n\nKernelSU menyediakan sistem konfigurasi bawaan yang memungkinkan modul menyimpan pengaturan key-value persisten atau sementara. Untuk detail lebih lanjut, lihat [dokumentasi Konfigurasi Modul](module-config.md).\n\n## Busybox\n\nKernelSU dikirimkan dengan fitur biner BusyBox yang lengkap (termasuk dukungan penuh SELinux). Eksekusi terletak di `/data/adb/ksu/bin/busybox`. BusyBox KernelSU mendukung \"Mode Shell Standalone Shell\" yang dapat dialihkan waktu proses. Apa yang dimaksud dengan mode mandiri ini adalah bahwa ketika dijalankan di shell `ash` dari BusyBox, setiap perintah akan langsung menggunakan applet di dalam BusyBox, terlepas dari apa yang ditetapkan sebagai `PATH`. Misalnya, perintah seperti `ls`, `rm`, `chmod` **TIDAK** akan menggunakan apa yang ada di `PATH` (dalam kasus Android secara default akan menjadi `/system/bin/ls`, ` /system/bin/rm`, dan `/system/bin/chmod` masing-masing), tetapi akan langsung memanggil applet BusyBox internal. Ini memastikan bahwa skrip selalu berjalan di lingkungan yang dapat diprediksi dan selalu memiliki rangkaian perintah lengkap, apa pun versi Android yang menjalankannya. Untuk memaksa perintah _not_ menggunakan BusyBox, Anda harus memanggil yang dapat dieksekusi dengan path lengkap.\n\nSetiap skrip shell tunggal yang berjalan dalam konteks KernelSU akan dieksekusi di shell `ash` BusyBox dengan mode mandiri diaktifkan. Untuk apa yang relevan dengan pengembang pihak ke-3, ini termasuk semua skrip boot dan skrip instalasi modul.\n\nBagi yang ingin menggunakan fitur “Standalone Mode” ini di luar KernelSU, ada 2 cara untuk mengaktifkannya:\n\n1. Tetapkan variabel lingkungan `ASH_STANDALONE` ke `1`<br>Contoh: `ASH_STANDALONE=1 /data/adb/ksu/bin/busybox sh <script>`\n2. Beralih dengan opsi baris perintah:<br>`/data/adb/ksu/bin/busybox sh -o mandiri <script>`\n\nUntuk memastikan semua shell `sh` selanjutnya dijalankan juga dalam mode mandiri, opsi 1 adalah metode yang lebih disukai (dan inilah yang digunakan secara internal oleh KernelSU dan manajer KernelSU) karena variabel lingkungan diwariskan ke proses anak.\n\n::: perbedaan tip dengan Magisk\n\nBusyBox KernelSU sekarang menggunakan file biner yang dikompilasi langsung dari proyek Magisk. **Berkat Magisk!** Oleh karena itu, Anda tidak perlu khawatir tentang masalah kompatibilitas antara skrip BusyBox di Magisk dan KernelSU karena keduanya persis sama!\n:::\n\n## KernelSU module\n\nModul KernelSU adalah folder yang ditempatkan di `/data/adb/modules` dengan struktur di bawah ini:\n\n```txt\n/data/adb/modules\n├── .\n├── .\n|\n├── $MODID                  <--- The folder is named with the ID of the module\n│   │\n│   │      *** Module Identity ***\n│   │\n│   ├── module.prop         <--- This file stores the metadata of the module\n│   │\n│   │      *** Main Contents ***\n│   │\n│   ├── system              <--- This folder will be mounted if skip_mount does not exist\n│   │   ├── ...\n│   │   ├── ...\n│   │   └── ...\n│   │\n│   │      *** Status Flags ***\n│   │\n│   ├── skip_mount          <--- If exists, KernelSU will NOT mount your system folder\n│   ├── disable             <--- If exists, the module will be disabled\n│   ├── remove              <--- If exists, the module will be removed next reboot\n│   │\n│   │      *** Optional Files ***\n│   │\n│   ├── post-fs-data.sh     <--- This script will be executed in post-fs-data\n│   ├── service.sh          <--- This script will be executed in late_start service\n|   ├── uninstall.sh        <--- This script will be executed when KernelSU removes your module\n│   ├── system.prop         <--- Properties in this file will be loaded as system properties by resetprop\n│   ├── sepolicy.rule       <--- Additional custom sepolicy rules\n│   │\n│   │      *** Auto Generated, DO NOT MANUALLY CREATE OR MODIFY ***\n│   │\n│   ├── vendor              <--- A symlink to $MODID/system/vendor\n│   ├── product             <--- A symlink to $MODID/system/product\n│   ├── system_ext          <--- A symlink to $MODID/system/system_ext\n│   │\n│   │      *** Any additional files / folders are allowed ***\n│   │\n│   ├── ...\n│   └── ...\n|\n├── another_module\n│   ├── .\n│   └── .\n├── .\n├── .\n```\n\n::: perbedaan tip dengan Magisk\nKernelSU tidak memiliki dukungan bawaan untuk Zygisk, jadi tidak ada konten terkait Zygisk dalam modul. Namun, Anda dapat menggunakan [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) untuk mendukung modul Zygisk. Dalam hal ini, konten modul Zygisk identik dengan yang didukung oleh Magisk.\n:::\n\n### module.prop\n\nmodule.prop adalah file konfigurasi untuk sebuah modul. Di KernelSU, jika modul tidak berisi file ini, maka tidak akan dikenali sebagai modul. Format file ini adalah sebagai berikut:\n\n```txt\nid=<string>\nname=<string>\nversion=<string>\nversioncode=<int>\nauthor=<string>\ndescription=<string>\nupdateJson=<url> (opsional)\nactionIcon=<path> (opsional)\nwebuiIcon=<path> (opsional)\n```\n\n- `id` harus cocok dengan ekspresi reguler ini: `^[a-zA-Z][a-zA-Z0-9._-]+$`<br>\n   contoh: ✓ `a_module`, ✓ `a.module`, ✓ `module-101`, ✗ `a module`, ✗ `1_module`, ✗ `-a-module`<br>\n   Ini adalah **pengidentifikasi unik** modul Anda. Anda tidak boleh mengubahnya setelah dipublikasikan.\n- `versionCode` harus berupa **integer**. Ini digunakan untuk membandingkan versi.\n- Lainnya yang tidak disebutkan di atas dapat berupa string **satu baris**.\n- Pastikan untuk menggunakan tipe jeda baris `UNIX (LF)` dan bukan `Windows (CR+LF)` atau `Macintosh (CR)`.\n- `actionIcon` dan `webuiIcon` adalah path ikon opsional yang digunakan sebagai\n  ikon default untuk pintasan aksi modul dan pintasan WebUI modul di aplikasi\n  pengelola. Path ini harus relatif terhadap direktori root modul. Contohnya,\n  `actionIcon=icon/icon.png` akan dipetakan ke `<MODDIR>/icon/icon.png`.\n\n::: tip DESKRIPSI DINAMIS\nField `description` dapat diganti secara dinamis saat runtime menggunakan sistem konfigurasi modul. Lihat [Mengganti Deskripsi Modul](module-config.md#overriding-module-description) untuk detailnya.\n:::\n\n### Shell skrip\n\nHarap baca bagian [Boot Scripts](#boot-scripts) untuk memahami perbedaan antara `post-fs-data.sh` dan `service.sh`. Untuk sebagian besar pengembang modul, `service.sh` sudah cukup baik jika Anda hanya perlu menjalankan skrip boot.\n\nDi semua skrip modul Anda, harap gunakan `MODDIR=${0%/*}` untuk mendapatkan jalur direktori dasar modul Anda; lakukan **TIDAK** hardcode jalur modul Anda dalam skrip.\n\n::: perbedaan tip dengan Magisk\nAnda dapat menggunakan variabel lingkungan KSU untuk menentukan apakah skrip berjalan di KernelSU atau Magisk. Jika berjalan di KernelSU, nilai ini akan disetel ke true.\n:::\n\n### `system` directory\n\nIsi direktori ini akan dihamparkan di atas partisi sistem /sistem setelah sistem di-boot. Ini berarti bahwa:\n\n::: tip PERSYARATAN METAMODULE\nDirektori `system` hanya di-mount jika Anda telah menginstal metamodule yang menyediakan fungsionalitas mounting (seperti `meta-overlayfs`). Metamodule menangani bagaimana modul di-mount. Lihat [Panduan Metamodule](metamodule.md) untuk informasi lebih lanjut.\n:::\n\n1. File dengan nama yang sama dengan yang ada di direktori terkait di sistem akan ditimpa oleh file di direktori ini.\n2. Folder dengan nama yang sama dengan yang ada di direktori terkait di sistem akan digabungkan dengan folder di direktori ini.\n\nJika Anda ingin menghapus file atau folder di direktori sistem asli, Anda perlu membuat file dengan nama yang sama dengan file/folder di direktori modul menggunakan `mknod filename c 0 0`. Dengan cara ini, sistem overlayfs akan secara otomatis \"memutihkan\" file ini seolah-olah telah dihapus (partisi / sistem sebenarnya tidak diubah).\n\nAnda juga dapat mendeklarasikan variabel bernama `REMOVE` yang berisi daftar direktori di `customize.sh` untuk menjalankan operasi penghapusan, dan KernelSU akan secara otomatis mengeksekusi `mknod <TARGET> c 0 0` di direktori modul yang sesuai. Misalnya:\n\n``` sh\nHAPUS = \"\n/sistem/aplikasi/YouTube\n/system/app/Bloatware\n\"\n```\n\nDaftar di atas akan mengeksekusi `mknod $MODPATH/system/app/YouTuBe c 0 0` dan `mknod $MODPATH/system/app/Bloatware c 0 0`; dan `/system/app/YouTube` dan `/system/app/Bloatware` akan dihapus setelah modul berlaku.\n\nJika Anda ingin mengganti direktori di sistem, Anda perlu membuat direktori dengan jalur yang sama di direktori modul Anda, lalu atur atribut `setfattr -n trusted.overlay.opaque -v y <TARGET>` untuk direktori ini. Dengan cara ini, sistem overlayfs akan secara otomatis mengganti direktori terkait di sistem (tanpa mengubah partisi /sistem).\n\nAnda dapat mendeklarasikan variabel bernama `REPLACE` di file `customize.sh` Anda, yang menyertakan daftar direktori yang akan diganti, dan KernelSU akan secara otomatis melakukan operasi yang sesuai di direktori modul Anda. Misalnya:\n\nREPLACE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n\nDaftar ini akan secara otomatis membuat direktori `$MODPATH/system/app/YouTube` dan `$MODPATH/system/app/Bloatware`, lalu jalankan `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/ YouTube` dan `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/Bloatware`. Setelah modul berlaku, `/system/app/YouTube` dan `/system/app/Bloatware` akan diganti dengan direktori kosong.\n\n::: perbedaan tip dengan Magisk\n\nKernelSU menggunakan arsitektur [metamodule](metamodule.md) di mana mounting didelegasikan ke metamodule yang dapat dipasang. Metamodule `meta-overlayfs` resmi menggunakan OverlayFS kernel untuk modifikasi systemless, sedangkan Magisk menggunakan magic mount (bind mount) yang dibangun langsung ke dalam intinya. Keduanya mencapai tujuan yang sama: memodifikasi file `/system` tanpa memodifikasi partisi `/system` secara fisik. Pendekatan KernelSU memberikan lebih banyak fleksibilitas dan mengurangi permukaan deteksi.\n:::\n\nJika Anda tertarik dengan overlayfs, disarankan untuk membaca [dokumentasi overlayfs](https://docs.kernel.org/filesystems/overlayfs.html) Kernel Linux.\n\n### system.prop\n\nFile ini mengikuti format yang sama dengan `build.prop`. Setiap baris terdiri dari `[kunci]=[nilai]`.\n\n### sepolicy.rule\n\nJika modul Anda memerlukan beberapa tambalan sepolicy tambahan, harap tambahkan aturan tersebut ke dalam file ini. Setiap baris dalam file ini akan diperlakukan sebagai pernyataan kebijakan.\n\n## Pemasangan module\n\nPenginstal modul KernelSU adalah modul KernelSU yang dikemas dalam file zip yang dapat di-flash di aplikasi pengelola KernelSU. Pemasang modul KernelSU yang paling sederhana hanyalah modul KernelSU yang dikemas sebagai file zip.\n\n```txt\nmodule.zip\n│\n├── customize.sh                       <--- (Optional, more details later)\n│                                           This script will be sourced by update-binary\n├── ...\n├── ...  /* The rest of module's files */\n│\n```\n\n:::peringatan\nModul KernelSU TIDAK didukung untuk diinstal dalam pemulihan kustom!!\n:::\n\n### Kostumisasi\n\nJika Anda perlu menyesuaikan proses penginstalan modul, secara opsional Anda dapat membuat skrip di penginstal bernama `customize.sh`. Skrip ini akan _sourced_ (tidak dijalankan!) oleh skrip penginstal modul setelah semua file diekstrak dan izin default serta konteks sekon diterapkan. Ini sangat berguna jika modul Anda memerlukan penyiapan tambahan berdasarkan ABI perangkat, atau Anda perlu menyetel izin khusus/konteks kedua untuk beberapa file modul Anda.\n\nJika Anda ingin sepenuhnya mengontrol dan menyesuaikan proses penginstalan, nyatakan `SKIPUNZIP=1` di `customize.sh` untuk melewati semua langkah penginstalan default. Dengan melakukannya, `customize.sh` Anda akan bertanggung jawab untuk menginstal semuanya dengan sendirinya.\n\nSkrip `customize.sh` berjalan di shell BusyBox `ash` KernelSU dengan \"Mode Mandiri\" diaktifkan. Variabel dan fungsi berikut tersedia:\n\n#### Variable\n\n- `KSU` (bool): variabel untuk menandai bahwa skrip berjalan di lingkungan KernelSU, dan nilai variabel ini akan selalu benar. Anda dapat menggunakannya untuk membedakan antara KernelSU dan Magisk.\n- `KSU_VER` (string): string versi dari KernelSU yang diinstal saat ini (mis. `v0.4.0`)\n- `KSU_VER_CODE` (int): kode versi KernelSU yang terpasang saat ini di ruang pengguna (mis. `10672`)\n- `KSU_KERNEL_VER_CODE` (int): kode versi KernelSU yang terpasang saat ini di ruang kernel (mis. `10672`)\n- `BOOTMODE` (bool): selalu `true` di KernelSU\n- `MODPATH` (jalur): jalur tempat file modul Anda harus diinstal\n- `TMPDIR` (jalur): tempat di mana Anda dapat menyimpan file untuk sementara\n- `ZIPFILE` (jalur): zip instalasi modul Anda\n- `ARCH` (string): arsitektur CPU perangkat. Nilainya adalah `arm`, `arm64`, `x86`, atau `x64`\n- `IS64BIT` (bool): `true` jika `$ARCH` adalah `arm64` atau `x64`\n- `API` (int): level API (versi Android) perangkat (mis. `23` untuk Android 6.0)\n\n::: peringatan\nDi KernelSU, MAGISK_VER_CODE selalu 25200 dan MAGISK_VER selalu v25.2. Tolong jangan gunakan kedua variabel ini untuk menentukan apakah itu berjalan di KernelSU atau tidak.\n:::\n\n#### Fungsi\n\n```txt\nui_print <msg>\n    print <msg> to console\n    Avoid using 'echo' as it will not display in custom recovery's console\n\nabort <msg>\n    print error message <msg> to console and terminate the installation\n    Avoid using 'exit' as it will skip the termination cleanup steps\n\nset_perm <target> <owner> <group> <permission> [context]\n    if [context] is not set, the default is \"u:object_r:system_file:s0\"\n    this function is a shorthand for the following commands:\n       chown owner.group target\n       chmod permission target\n       chcon context target\n\nset_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]\n    if [context] is not set, the default is \"u:object_r:system_file:s0\"\n    for all files in <directory>, it will call:\n       set_perm file owner group filepermission context\n    for all directories in <directory> (including itself), it will call:\n       set_perm dir owner group dirpermission context\n```\n\n## Boot scripts\n\nDi KernelSU, skrip dibagi menjadi dua jenis berdasarkan mode operasinya: mode post-fs-data dan mode layanan late_start:\n\n- mode pasca-fs-data\n   - Tahap ini adalah BLOKIR. Proses boot dijeda sebelum eksekusi selesai, atau 10 detik telah berlalu.\n   - Skrip dijalankan sebelum modul apa pun dipasang. Ini memungkinkan pengembang modul untuk menyesuaikan modul mereka secara dinamis sebelum dipasang.\n   - Tahap ini terjadi sebelum Zygote dimulai, yang berarti segalanya di Android\n   - **PERINGATAN:** menggunakan `setprop` akan menghentikan proses booting! Silakan gunakan `resetprop -n <prop_name> <prop_value>` sebagai gantinya.\n   - **Hanya jalankan skrip dalam mode ini jika perlu.**\n- mode layanan late_start\n   - Tahap ini NON-BLOCKING. Skrip Anda berjalan paralel dengan proses booting lainnya.\n   - **Ini adalah tahap yang disarankan untuk menjalankan sebagian besar skrip.**\n\nDi KernelSU, skrip startup dibagi menjadi dua jenis berdasarkan lokasi penyimpanannya: skrip umum dan skrip modul:\n\n- Skrip Umum\n   - Ditempatkan di `/data/adb/post-fs-data.d` atau `/data/adb/service.d`\n   - Hanya dieksekusi jika skrip disetel sebagai dapat dieksekusi (`chmod +x script.sh`)\n   - Skrip di `post-fs-data.d` berjalan dalam mode post-fs-data, dan skrip di `service.d` berjalan di mode layanan late_start.\n   - Modul seharusnya **TIDAK** menambahkan skrip umum selama instalasi\n- Skrip Modul\n   - Ditempatkan di folder modul itu sendiri\n   - Hanya dijalankan jika modul diaktifkan\n   - `post-fs-data.sh` berjalan dalam mode post-fs-data, dan `service.sh` berjalan dalam mode layanan late_start.\n\nSemua skrip boot akan berjalan di shell BusyBox `ash` KernelSU dengan \"Mode Mandiri\" diaktifkan.\n\n## Mode late-load {#late-load-mode}\n\nSelain alur boot standar yang dijelaskan di atas, KernelSU mendukung **mode late-load** untuk skenario LKM (Loadable Kernel Module). Dalam mode ini, modul kernel KernelSU dimuat **setelah sistem sepenuhnya boot**, bukan selama proses init.\n\n### Kapan late-load terjadi?\n\nLate-load dipicu dengan menjalankan perintah `ksud late-load`. Perintah ini:\n\n1. Mendeteksi versi KMI saat ini dan memuat `kernelsu.ko` yang sesuai dari aset tertanam.\n2. Melakukan inisialisasi modul (aturan SELinux, daftar izin, fitur, dll.) yang biasanya terjadi saat boot.\n\nKarena sistem sudah sepenuhnya berjalan, mekanisme tertentu saat boot tidak tersedia atau tidak diperlukan.\n\n### Perbedaan dari boot standar\n\n| Perilaku | Boot standar | Mode late-load |\n|----------|:---:|:---:|\n| Modul kernel dimuat oleh init (PID 1) | Ya | Tidak (dimuat setelah boot) |\n| Hook kprobe ksud (execve/read/fstat/input) | Ya | Dilewati |\n| Deteksi mode aman (tombol volume) | Ya | Selalu dinonaktifkan |\n| Pengambilan log boot (logcat/dmesg) | Ya | Dilewati |\n| Pemeriksaan koeksistensi Magisk | Ya | Dilewati |\n| Event `post-fs-data` dilaporkan ke kernel | Ya | Dilewati |\n| Event `boot-completed` dilaporkan ke kernel | Ya | Diatur langsung saat init |\n| Skrip `post-fs-data.sh` / `post-fs-data.d/` | Ya | Digantikan oleh tahap `late-load` |\n| Pemuatan `system.prop` | Ya | Ya |\n| Mount OverlayFS (metamodule) | Ya | Ya |\n| Skrip `post-mount.sh` / `post-mount.d/` | Ya | Ya |\n| Skrip `service.sh` / `service.d/` | Ya | Ya |\n| Skrip `boot-completed.sh` / `boot-completed.d/` | Ya | Ya |\n| Variabel lingkungan `KSU_LATE_LOAD` | Tidak diatur | Diatur ke `1` |\n| Flag info kernel `0x4` | Tidak diatur | Diatur |\n\n### Urutan eksekusi skrip\n\nDalam mode late-load, urutan eksekusi skrip adalah:\n\n```txt\nksud late-load:\n  1. Muat kernelsu.ko (jika belum dimuat)\n  2. Ekstrak biner, tangani pembaruan modul, muat aturan SELinux, inisialisasi fitur\n  3. Jalankan skrip late-load.d/ dan skrip late-load modul (blocking)\n  4. Muat system.prop (resetprop -n)\n  5. Jalankan skrip mount metamodule (OverlayFS)\n  6. Jalankan skrip post-mount.d/ dan post-mount.sh modul (blocking)\n  7. Jalankan skrip service.d/ dan service.sh modul (non-blocking)\n  8. Jalankan skrip boot-completed.d/ dan boot-completed.sh modul (non-blocking)\n```\n\n### Skrip khusus late-load\n\nModul dapat menyediakan skrip `late-load.sh` yang dijalankan **hanya** dalam mode late-load, sebagai pengganti `post-fs-data.sh`. Skrip ini berjalan sebelum mount OverlayFS, mirip dengan `post-fs-data.sh` dalam alur standar.\n\nSelain itu, skrip umum dapat ditempatkan di `/data/adb/late-load.d/` untuk dijalankan pada tahap ini.\n\n### Mendeteksi mode late-load dalam skrip\n\nModul dapat mendeteksi mode late-load dengan memeriksa variabel lingkungan `KSU_LATE_LOAD`:\n\n```sh\nif [ \"$KSU_LATE_LOAD\" = \"1\" ]; then\n    # Berjalan dalam mode late-load\n    echo \"Late-load mode detected\"\nfi\n```\n\nIni memungkinkan modul menyesuaikan perilakunya, misalnya melewatkan operasi yang hanya diperlukan saat boot awal.\n"
  },
  {
    "path": "website/docs/id_ID/guide/rescue-from-bootloop.md",
    "content": "# Recovery dari bootloop\n\nSaat mem-flash perangkat, kami mungkin menghadapi situasi di mana perangkat menjadi \"bata\". Secara teori, jika Anda hanya menggunakan fastboot untuk mem-flash partisi boot atau menginstal modul yang tidak sesuai yang menyebabkan perangkat gagal melakukan booting, ini dapat dipulihkan dengan operasi yang sesuai. Dokumen ini bertujuan untuk memberikan beberapa metode darurat untuk membantu Anda pulih dari perangkat \"bricked\".\n\n## Brick saat memflash partisi boot\n\nDi KernelSU, situasi berikut dapat menyebabkan bata boot saat mem-flash partisi boot:\n\n1. Anda mem-flash image boot dalam format yang salah. Misalnya, jika format booting ponsel Anda adalah `gz`, tetapi Anda mem-flash image berformat `lz4`, maka ponsel tidak akan dapat melakukan booting.\n2. Ponsel Anda perlu menonaktifkan verifikasi AVB agar dapat boot dengan benar (biasanya perlu menghapus semua data di ponsel).\n3. Kernel Anda memiliki beberapa bug atau tidak cocok untuk flash ponsel Anda.\n\nApa pun situasinya, Anda dapat memulihkannya dengan **mem-flash gambar boot stok**. Oleh karena itu, di awal tutorial instalasi, kami sangat menyarankan Anda untuk mem-backup stock boot Anda sebelum melakukan flashing. Jika Anda belum mencadangkan, Anda dapat memperoleh boot pabrik asli dari pengguna lain dengan perangkat yang sama dengan Anda atau dari firmware resmi.\n\n## Brick disebabkan modul\n\nMemasang modul dapat menjadi penyebab yang lebih umum dari bricking perangkat Anda, tetapi kami harus memperingatkan Anda dengan serius: **Jangan memasang modul dari sumber yang tidak dikenal**! Karena modul memiliki hak akses root, mereka berpotensi menyebabkan kerusakan permanen pada perangkat Anda!\n\n### Module normal\n\nJika Anda telah mem-flash modul yang telah terbukti aman tetapi menyebabkan perangkat Anda gagal booting, maka situasi ini dapat dipulihkan dengan mudah di KernelSU tanpa rasa khawatir. KernelSU memiliki mekanisme bawaan untuk menyelamatkan perangkat Anda, termasuk yang berikut:\n\n1. Pembaruan AB\n2. Selamatkan dengan menekan Volume Turun\n\n#### Pembaruan AB\n\nPembaruan modul KernelSU menarik inspirasi dari mekanisme pembaruan AB sistem Android yang digunakan dalam pembaruan OTA. Jika Anda menginstal modul baru atau memperbarui modul yang sudah ada, itu tidak akan langsung mengubah file modul yang sedang digunakan. Sebagai gantinya, semua modul akan dibangun ke gambar pembaruan lainnya. Setelah sistem dimulai ulang, sistem akan mencoba untuk mulai menggunakan gambar pembaruan ini. Jika sistem Android berhasil melakukan booting, modul akan benar-benar diperbarui.\n\nOleh karena itu, metode paling sederhana dan paling umum digunakan untuk menyelamatkan perangkat Anda adalah dengan **memaksa reboot**. Jika Anda tidak dapat memulai sistem Anda setelah mem-flash modul, Anda dapat menekan dan menahan tombol daya selama lebih dari 10 detik, dan sistem akan melakukan reboot secara otomatis; setelah mem-boot ulang, itu akan kembali ke keadaan sebelum memperbarui modul, dan modul yang diperbarui sebelumnya akan dinonaktifkan secara otomatis.\n\n#### Recovery dengan menekan Volume Bawah\n\nJika pembaruan AB masih tidak dapat menyelesaikan masalah, Anda dapat mencoba menggunakan **Safe Mode**. Dalam Safe Mode, semua modul dinonaktifkan.\n\nAda dua cara untuk masuk ke Safe Mode:\n\n1. Mode Aman bawaan dari beberapa sistem; beberapa sistem memiliki Safe Mode bawaan yang dapat diakses dengan menekan lama tombol volume turun, sementara yang lain (seperti MIUI) dapat mengaktifkan Safe Mode di Recovery. Saat memasuki Safe Mode sistem, KernelSU juga akan masuk ke Safe Mode dan secara otomatis menonaktifkan modul.\n2. Safe Mode bawaan dari KernelSU; metode pengoperasiannya adalah **tekan tombol volume turun secara terus-menerus selama lebih dari tiga kali** setelah layar boot pertama. Perhatikan bahwa ini adalah rilis pers, rilis pers, rilis pers, bukan tekan dan tahan.\n\nSetelah memasuki mode aman, semua modul pada halaman modul KernelSU Manager dinonaktifkan, tetapi Anda dapat melakukan operasi \"uninstall\" untuk menghapus semua modul yang mungkin menyebabkan masalah.\n\nMode aman bawaan diimplementasikan di kernel, jadi tidak ada kemungkinan peristiwa penting yang hilang karena intersepsi. Namun, untuk kernel non-GKI, integrasi kode secara manual mungkin diperlukan, dan Anda dapat merujuk ke dokumentasi resmi untuk mendapatkan panduan.\n\n### Module berbahaya\n\nJika metode di atas tidak dapat menyelamatkan perangkat Anda, kemungkinan besar modul yang Anda instal memiliki operasi jahat atau telah merusak perangkat Anda melalui cara lain. Dalam hal ini, hanya ada dua saran:\n\n1. Hapus data dan flash sistem resmi.\n2. Konsultasikan layanan purna jual.\n"
  },
  {
    "path": "website/docs/id_ID/guide/unofficially-support-devices.md",
    "content": "# Perangkat Yang Didukung Tidak Resmi\n\n::: warning\nDokumen ini hanya untuk referensi arsip dan tidak lagi diperbarui.\nSejak KernelSU v1.0, kami telah menghentikan dukungan resmi untuk perangkat non-GKI.\n:::\n\n:::peringatan\n\ndi halaman ini, terdapat kernel untuk perangkat non-GKI yang mendukung KernelSU yang dikelola oleh pengembang lain.\n\n:::\n\n:::peringatan\n\nHalaman ini hanya untuk Anda yang ingin menemukan kode sumber yang sesuai dengan perangkat Anda, itu **BUKAN** berarti kode sumber telah ditinjau oleh _KernelSU Developers_. Anda harus menggunakannya dengan risiko Anda sendiri.\n\n:::\n\n<script setup>\nimport data from '../../repos.json'\n</script>\n\n<table>\n   <thead>\n      <tr>\n         <th>Pengelola</th>\n         <th>Repository</th>\n         <th>Perangkat yang didukung</th>\n      </tr>\n   </thead>\n   <tbody>\n    <tr v-for=\"repo in data\" :key=\"repo.devices\">\n        <td><a :href=\"repo.maintainer_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.maintainer }}</a></td>\n        <td><a :href=\"repo.kernel_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.kernel_name }}</a></td>\n        <td>{{ repo.devices }}</td>\n    </tr>\n   </tbody>\n</table>\n"
  },
  {
    "path": "website/docs/id_ID/guide/what-is-kernelsu.md",
    "content": "# Apa itu KernelSU?\n\nKernelSU adalah solusi root untuk perangkat GKI Android, ia bekerja dalam mode kernel dan memberikan izin root ke aplikasi userspace secara langsung di ruang kernel.\n\n## Fitur\n\nFitur utama dari KernelSU adalah **berbasis kernel**. KernelSU bekerja dalam mode kernel, sehingga dapat menyediakan antarmuka kernel yang belum pernah kita miliki sebelumnya. Sebagai contoh, kita dapat menambahkan breakpoint perangkat keras ke proses apa pun dalam mode kernel; Kita dapat mengakses memori fisik dari proses apa pun tanpa diketahui oleh siapa pun; Kita dapat mencegat syscall apa pun di ruang kernel; dll.\n\nSelain itu, KernelSU menyediakan [sistem metamodule](metamodule.md), yang merupakan arsitektur yang dapat dipasang untuk manajemen modul. Tidak seperti solusi root tradisional yang mengintegrasikan logika mount ke dalam intinya, KernelSU mendelegasikan ini ke metamodules. Ini memungkinkan Anda untuk memasang metamodules (seperti [meta-overlayfs](https://github.com/tiann/KernelSU/tree/main/userspace/meta-overlayfs)) untuk menyediakan modifikasi systemless pada partisi `/system` dan partisi lainnya.\n\n## Bagaimana cara menggunakannya\n\nSilakan merujuk ke: [Installation](installation)\n\n## Bagaimana cara men-buildnya\n\n[How to build](how-to-build)\n\n## Diskusi\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n"
  },
  {
    "path": "website/docs/id_ID/index.md",
    "content": "---\nlayout: home\ntitle: Sebuah solusi root kernel-based untuk Android\n\nhero:\n  name: KernelSU\n  text: Sebuah solusi root kernel-based untuk Android\n  tagline: \"\"\n  image:\n    src: /logo.png\n    alt: KernelSU\n  actions:\n    - theme: brand\n      text: Permulaan\n      link: /id_ID/guide/what-is-kernelsu\n    - theme: alt\n      text: Lihat di GitHub\n      link: https://github.com/tiann/KernelSU\n\nfeatures:\n  - title: Kernel-based\n    details: KernelSU bekerja dalam mode Linux kernel, dan mempunyai kelebihan diatas aplikasi userspace.\n  - title: Kontrol akses daftar putih\n    details: Hanya aplikasi yang diberikan izin root yang bisa mengakses `su`, aplikasi lain tidak bisa mengakses su.\n  - title: Sistem Metamodule\n    details: Infrastruktur modul yang dapat dipasang memungkinkan modifikasi systemless pada /system. Pasang metamodule seperti meta-overlayfs untuk mengaktifkan pemasangan modul.\n  - title: Sumber terbuka\n    details: KernelSU adalah projek sumber terbuka dibawah lisensi GPL-3.\n"
  },
  {
    "path": "website/docs/index.md",
    "content": "---\nlayout: home\ntitle: Home\n\nhero:\n  name: KernelSU\n  text: A kernel-based root solution for Android\n  tagline: \"\"\n  image:\n    src: /logo.png\n    alt: KernelSU\n  actions:\n    - theme: brand\n      text: Get started\n      link: /guide/what-is-kernelsu\n    - theme: alt\n      text: View on GitHub\n      link: https://github.com/tiann/KernelSU\n\nfeatures:\n  - title: Kernel-based\n    details: As the name suggests, KernelSU runs inside the Linux kernel, giving it more control over userspace apps.\n  - title: Root access control\n    details: Only permitted apps can access or see su; all other apps remain unaware of it.\n  - title: Customizable root privileges\n    details: KernelSU allows customization of su's uid, gid, groups, capabilities, and SELinux rules, hardening root privileges.\n  - title: Metamodule system\n    details: Pluggable module infrastructure allows systemless /system modifications. Install a metamodule like meta-overlayfs to enable module mounting.\n"
  },
  {
    "path": "website/docs/ja_JP/guide/app-profile.md",
    "content": "# App Profile\n\nApp Profile は KernelSU が提供する仕組みで、さまざまなアプリの設定を柔軟にカスタマイズできます。\n\nroot 権限（`su` を利用できること）を付与したアプリの場合、App Profile は Root Profile とも呼べます。`su` コマンドの `uid`、`gid`、`groups`、`capabilities`、`SELinux` ルールを調整できるため、root ユーザーの特権を細かく制限できます。たとえばファイアウォールアプリにだけネットワーク権限を与えてファイルアクセスを禁止したり、凍結アプリには完全な root の代わりに shell 権限のみを与えたりできます。**すなわち最小権限の原則で力を箱の中に閉じ込める**ことができます。\n\nroot 権限を持たない通常のアプリに対しても、App Profile は kernel やモジュールシステムがそのアプリをどのように扱うかを制御できます。モジュールによる変更をアプリで見せるべきかなどを決められ、設定に基づいて kernel／モジュールシステムは「隠す」といった挙動を選択できます。\n\n## Root Profile\n\n### UID・GID・グループ\n\nLinux にはユーザーとグループの 2 つの概念があります。各ユーザーにはユーザー ID (UID) があり、ユーザーは複数のグループに所属できます。それぞれのグループにはグループ ID (GID) があり、これらの ID がシステム内でのユーザー識別やアクセス可能なリソースの判断に使われます。\n\nUID が 0 のユーザーは root ユーザー、GID が 0 のグループは root グループと呼ばれます。root グループは一般的にシステムでもっとも強い権限を持ちます。\n\nAndroid では各アプリ（shared UID を除く）が独立したユーザーとして動作し、固有の UID を持ちます。たとえば `0` は root、`1000` は `system`、`2000` は ADB shell、`10000`〜`19999` は一般アプリに割り当てられます。\n\n::: info\nここでいう UID は Android のマルチユーザーやワークプロファイルとは別の概念です。ワークプロファイルは UID の範囲を分割することで実装されています。たとえば 10000-19999 がメインユーザー、110000-119999 がワークプロファイルを表し、その中の各アプリは独自の UID を持っています。\n:::\n\n各アプリは複数のグループに所属でき、GID はそのうちのプライマリグループを表します（多くの場合 UID と同じです）。その他のグループは補助グループです。ネットワークアクセスや Bluetooth など、いくつかの権限はグループで管理されています。\n\n例として、ADB shell で `id` コマンドを実行すると次のような結果になります。\n\n```sh\noriole:/ $ id\nuid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),1078(ext_data_rw),1079(ext_obb_rw),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid),3012(readtracefs) context=u:r:shell:s0\n```\n\nここでは UID が `2000`、GID（プライマリグループ ID）も `2000` です。さらに `inet`（`AF_INET` や `AF_INET6` を作成できる、つまりネットワークアクセスが可能）、`sdcard_rw`（SD カードの読み書きが可能）といった補助グループにも所属しています。\n\nKernelSU の Root Profile を使うと、`su` 実行後の root プロセスの UID・GID・所属グループをカスタマイズできます。たとえばある root アプリの Root Profile で UID を `2000` に設定すれば、そのアプリが `su` を使っても実際の権限は ADB shell 相当になります。またグループから `inet` を外せば、`su` コマンドがネットワークにアクセスできなくなります。\n\n::: tip 注意\nApp Profile が制御するのは `su` 使用後の root プロセスの権限だけであり、アプリ自身の権限ではありません。アプリがネットワーク権限を要求して許可されていれば、`su` を使わなくてもネットワークにアクセスできます。`su` から `inet` グループを外すのは、`su` にネットワークアクセスをさせないためだけの措置です。\n:::\n\nRoot Profile は kernel によって強制され、`su` でユーザーやグループを切り替えるようなアプリ側の自発的な動作には依存しません。`su` 権限を与えるかどうかは開発者ではなくユーザー自身が完全にコントロールできます。\n\n### Capabilities\n\nCapability は Linux における特権分離の仕組みです。\n\n従来の `UNIX` 実装では、権限チェックのためにプロセスを 2 種類に分けます。effective UID が `0` の特権プロセス（superuser/root）と、それ以外の非特権プロセスです。特権プロセスは kernel の権限チェックをすべてバイパスし、非特権プロセスはプロセスの資格情報（effective UID・effective GID・補助グループ一覧など）に基づいた完全なチェックを受けます。\n\nLinux 2.2 以降は、従来 root が一括して持っていた権限を capability と呼ばれる単位に分割し、それぞれを独立して有効化・無効化できるようになりました。\n\n各 capability は 1 つ以上の権限を表します。たとえば `CAP_DAC_READ_SEARCH` は、ファイル読み取りやディレクトリの読み取り・実行に必要なチェックをバイパスできる権限です。effective UID が `0` のユーザー（root）であっても、`CAP_DAC_READ_SEARCH` などを持っていなければ自由にファイルを読めません。\n\nKernelSU の Root Profile では `su` 実行後の root プロセスの capability もカスタマイズでき、いわば「部分的な root 権限」を与えられます。上で触れた UID / GID と異なり、`su` 後も UID `0` が必要な root アプリもあります。その場合は UID `0` のまま capability を制限することで、許可される操作を限定できます。\n\n::: tip 強く推奨\nLinux の capability については[公式ドキュメント](https://man7.org/linux/man-pages/man7/capabilities.7.html)に詳細がまとまっています。capability をカスタマイズする予定があるなら、まずこのドキュメントを読んでください。\n:::\n\n### SELinux\n\nSELinux は強力な Mandatory Access Control (MAC) 仕組みで、**default deny**（明示的に許可されていない操作はすべて拒否）という原則で動作します。\n\nSELinux には 2 つのグローバルモードがあります。\n\n1. Permissive モード: 拒否イベントを記録しますが強制しません。\n2. Enforcing モード: 拒否イベントを記録し、かつ強制します。\n\n::: warning\n近年の Android は SELinux に大きく依存してシステム全体の安全性を保っています。何のメリットもないので、「Permissive モード」で動作するカスタムシステムの利用は強く非推奨です。\n:::\n\nSELinux の全容を説明するのは非常に複雑で、このドキュメントの範囲を超えています。まずは以下の資料で仕組みを理解することをおすすめします。\n\n1. [Wikipedia](https://en.wikipedia.org/wiki/Security-Enhanced_Linux)\n2. [Red Hat: What Is SELinux?](https://www.redhat.com/en/topics/linux/what-is-selinux)\n3. [ArchLinux: SELinux](https://wiki.archlinux.org/title/SELinux)\n\nKernelSU の Root Profile では、`su` 実行後の root プロセスの SELinux コンテキストもカスタマイズできます。特定のコンテキストに対してアクセス制御ルールを定義し、root 権限をきめ細かく制御できます。\n\n一般的なシナリオでは、アプリが `su` を実行すると `u:r:su:s0` のような**制限のない** SELinux ドメインに切り替わります。Root Profile を介して `u:r:app1:s0` のようなカスタムドメインに切り替え、そのドメイン向けに次のようなルールを定義できます。\n\n```sh\ntype app1\nenforce app1\ntypeattribute app1 mlstrustedsubject\nallow app1 * * *\n```\n\n`allow app1 * * *` というルールは例示目的であり、実際にはあまり使うべきではありません。Permissive モードとほとんど変わらなくなってしまうからです。\n\n### エスカレーション\n\nRoot Profile の設定が適切でないと、意図せず制限を回避される恐れがあります。\n\nたとえば ADB shell ユーザーに root 権限を与えていて（よくある構成です）、さらに通常アプリにも root 権限を付与したあと、そのアプリの Root Profile で UID 2000（ADB shell の UID）を設定したとします。この場合、アプリは `su` を 2 回実行するだけで完全な root を得られます。\n\n1. 1 回目の `su` は App Profile の制限を受け、UID を `0` ではなく `2000`（ADB shell）に変更します。\n2. 2 回目の `su` は、既に UID が `2000` であり、その UID に root 権限を許可しているため、完全な root 権限を取得します。\n\n::: warning 注意\nこの挙動は仕様でありバグではありません。したがって以下を推奨します。\n\n本当に ADB に root 権限が必要な（開発者などの）場合でも、Root Profile で UID を `2000` に変更するのは避けてください。`1000`（system）を使う方が安全です。\n:::\n\n## 非 root プロファイル\n\n### モジュールのアンマウント\n\nKernelSU は OverlayFS をマウントすることで systemless な形でシステムパーティションを変更します。しかしこの挙動に敏感なアプリもあります。そのような場合は「Umount modules」オプションを設定して、対象アプリではモジュールをアンマウントさせることができます。\n\nKernelSU マネージャーの設定画面には「Umount modules by default」という項目もあります。デフォルトではこのオプションは**有効**で、追加設定をしない限り KernelSU や一部モジュールはそのアプリでモジュールをアンマウントします。この挙動を望まない、あるいは一部アプリに影響する場合は、次のいずれかの方法を取ってください。\n\n1. 「Umount modules by default」を有効のままにし、モジュールを読み込みたいアプリの App Profile では個別に「Umount modules」を無効にする（ホワイトリスト方式）。\n2. 「Umount modules by default」を無効にし、アンマウントしたいアプリでのみ個別に「Umount modules」を有効にする（ブラックリスト方式）。\n\n::: info\nカーネル 5.10 以降を実行しているデバイスでは、カーネルが追加の処理なしにモジュールをアンマウントします。一方 5.10 未満のデバイスでは、このオプションは設定値を示すだけで KernelSU は何もしません。5.10 より前のカーネルで「Umount modules」を使いたい場合は、`fs/namespace.c` にある `path_umount` 関数をバックポートする必要があります。詳細は [Integrate for non-GKI devices](https://kernelsu.org/guide/how-to-integrate-for-non-gki.html#how-to-backport-path_umount) ページの末尾を参照してください。Zygisksu など一部モジュールも、モジュールをアンマウントすべきかどうかを判断するためにこのオプションを参照します。\n:::\n"
  },
  {
    "path": "website/docs/ja_JP/guide/difference-with-magisk.md",
    "content": "# Magisk との違い\n\nKernelSU モジュールと Magisk モジュールには多くの共通点がありますが、実装の仕組みが全く異なるため、必然的にいくつかの相違点が存在します。Magisk と KernelSU の両方でモジュールを動作させたい場合、これらの違いを理解する必要があります。\n\n## 似ているところ\n\n- モジュールファイルの形式：どちらもzip形式でモジュールを整理しており、モジュールの形式はほぼ同じです。\n- モジュールのインストールディレクトリ：どちらも `/data/adb/modules` に配置されます。\n- システムレス：どちらもモジュールによるシステムレスな方法で /system を変更できます。\n- post-fs-data.sh: 実行時間と意味は全く同じです。\n- service.sh: 実行時間と意味は全く同じです。\n- system.prop：全く同じです。\n- sepolicy.rule：全く同じです。\n- BusyBox：スクリプトは BusyBox で実行され、どちらの場合も「スタンドアロンモード」が有効です。\n\n## 違うところ\n\n違いを理解する前に、モジュールが KernelSU で動作しているか Magisk で動作しているかを区別する方法を知っておく必要があります。環境変数 `KSU` を使うとモジュールスクリプトを実行できるすべての場所 (`customize.sh`, `post-fs-data.sh`, `service.sh`) で区別できます。KernelSU では、この環境変数に `true` が設定されます。\n\n以下は違いです：\n\n- KernelSU モジュールは、リカバリーモードではインストールできません。\n- KernelSU モジュールには Zygisk のサポートが組み込まれていません（ただし[ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext)を使うと Zygisk モジュールを使用できます）。\n- **モジュールマウントアーキテクチャ**：KernelSU は [metamodule システム](metamodule.md) を使用し、マウントをプラグ可能な metamodule (`meta-overlayfs`など) に委任します。一方、Magisk はマウントをコアに組み込んでいます。KernelSU はモジュールマウントを有効にするために metamodule のインストールが必要です。\n- KernelSU モジュールにおけるファイルの置換や削除の方法は、Magisk とは全く異なります。KernelSU は `.replace` メソッドをサポートしていません。その代わり、`mknod filename c 0 0` で同名のファイルを作成し、対応するファイルを削除する必要があります。\n- BusyBox 用のディレクトリが違います。KernelSU の組み込み BusyBox は `/data/adb/ksu/bin/busybox` に、Magisk では `/data/adb/magisk/busybox` に配置されます。**これは KernelSU の内部動作であり、将来的に変更される可能性があることに注意してください!**\n- KernelSU は `.replace` ファイルをサポートしていません。しかし、KernelSU はファイルやフォルダを削除したり置き換えたりするための `REMOVE` と `REPLACE` 変数をサポートしています。\n- KernelSU は `boot-completed.sh` スクリプトを追加し、Android システムの起動完了後にタスクを実行できます。\n- KernelSU は `post-mount.sh` スクリプトを追加し、モジュールマウント完了後にタスクを実行できます。\n"
  },
  {
    "path": "website/docs/ja_JP/guide/faq.md",
    "content": "# よくある質問\n\n## 私のデバイスは KernelSU に対応していますか?\n\nまず、お使いのデバイスがブートローダーのロックを解除できる必要があります。もしできないのであれば、サポート外です。\n\nもし KernelSU アプリで「非対応」と表示されたら、そのデバイスは最初からサポートされていないことになりますが、カーネルソースをビルドして KernelSU を組み込むか、[非公式の対応デバイス](unofficially-support-devices)で動作させることが可能です。\n\n## KernelSU を使うにはブートローダーのロックを解除する必要がありますか？\n\nはい。\n\n## KernelSU はモジュールに対応していますか?\n\nはい。ほとんどの Magisk モジュールは KernelSU で動作します。ただし、モジュールが `/system` ファイルを変更する必要がある場合は、[metamodule](metamodule.md) (`meta-overlayfs`など) をインストールする必要があります。他のモジュール機能は metamodule なしで動作します。詳細は [モジュールガイド](module.md) をご覧ください。\n\n## KernelSU は Xposed に対応していますか?\n\nはい。[Dreamland](https://github.com/canyie/Dreamland) や [TaiChi](https://taichi.cool) が動作します。LSPosed については、[ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) を使うと動作するようにできます。\n\n## KernelSU は Zygisk に対応していますか?\n\nKernelSU は Zygisk サポートを内蔵していません。[ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) を使ってください。\n\n## KernelSU は Magisk と互換性がありますか?\n\nKernelSU のモジュールシステムは Magisk のマジックマウントと競合しており、KernelSU で有効になっているモジュールがある場合、Magisk 全体が動作しなくなります。\n\nしかし、KernelSU の `su` だけを使うのであれば、Magisk とうまく連携することができます。KernelSU は `kernel` を、Magisk は `ramdisk` を修正するため、両者は共存できます。\n\n## KernelSU は Magisk の代わりになりますか？\n\n私たちはそうは思っていませんし、それが目標でもありません。Magisk はユーザ空間の root ソリューションとして十分であり、長く使われ続けるでしょう。KernelSU の目標は、ユーザーにカーネルインターフェースを提供することであり、Magisk の代用ではありません。\n\n## KernelSU は GKI 以外のデバイスに対応できますか？\n\n可能です。ただしカーネルソースをダウンロードし、KernelSU をソースツリーに統合して、自分でカーネルをビルドする必要があります。\n\n## KernelSU は Android 12 以下のデバイスに対応できますか？\n\nKernelSU の互換性に影響を与えるのはデバイスのカーネルであり、Android のバージョンとは無関係です。唯一の制限は、Android 12 で発売されたデバイスはカーネル5.10以上（GKI デバイス）でなければならないことです：\n\n1. Android 12 をプリインストールして発売された端末は対応しているはずです。\n2. カーネルが古い端末（一部の Android 12 端末はカーネルも古い）は対応可能ですが、カーネルは自分でビルドする必要があります。\n\n## KernelSU は古いカーネルに対応できますか？\n\nKernelSU は現在カーネル4.14にバックポートされていますが、それ以前のカーネルについては手動でバックポートする必要があります。プルリクエスト歓迎です！\n\n## 古いカーネルに KernelSU を組み込むには？\n\n[ガイド](../../guide/how-to-integrate-for-non-gki) を参考にしてください。\n\n## Android のバージョンが13なのに、カーネルは「android12-5.10」と表示されるのはなぜ？\n\nカーネルのバージョンは Android のバージョンと関係ありません。カーネルを書き込む必要がある場合は、常にカーネルのバージョンを使用してください。Android のバージョンはそれほど重要ではありません。\n\n## KernelSU に-mount-master/global のマウント名前空間はありますか？\n\n今はまだありませんが（将来的にはあるかもしれません）、グローバルマウントの名前空間に手動で切り替える方法は、以下のようにたくさんあります：\n\n1. `nsenter -t 1 -m sh` でシェルをグローバル名前空間にします。\n2. `nsenter --mount=/proc/1/ns/mnt` を実行したいコマンドに追加すればグローバル名前空間で実行されます。 KernelSU は [このような使い方](https://github.com/tiann/KernelSU/blob/77056a710073d7a5f7ee38f9e77c9fd0b3256576/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt#L115) もできます。\n\n## GKI 1.0 なのですが、使えますか？\n\nGKI1 は GKI2 と全く異なるため、カーネルは自分でビルドする必要があります。\n\n## 新規インストール後にモジュールが動作しないのはなぜですか？\n\nモジュールが `/system` ファイルを変更する必要がある場合は、`system` ディレクトリをマウントするために [metamodule](metamodule.md) をインストールする必要があります。他のモジュール機能（スクリプト、sepolicy、system.prop）は metamodule なしで動作します。\n\n**解決策**：インストール手順については [Metamodule ガイド](metamodule.md) をご覧ください。\n\n## metamodule とは何ですか？なぜ必要なのですか？\n\nMetamodule は、通常のモジュールをマウントするためのインフラストラクチャを提供する特殊なモジュールです。完全な説明については [Metamodule ガイド](metamodule.md) をご覧ください。\n"
  },
  {
    "path": "website/docs/ja_JP/guide/hidden-features.md",
    "content": "# 隠し機能\n\n## .ksurc\n\nデフォルトでは `/system/bin/sh` は `/system/etc/mkshrc` を読み込みます。\n\n`/data/adb/ksu/.ksurc` ファイルを作成することで、カスタマイズした rc ファイルを su に読み込ませられます。"
  },
  {
    "path": "website/docs/ja_JP/guide/how-to-build.md",
    "content": "# KernelSU のビルド方法は？\n\n::: warning\nこのドキュメントはアーカイブ参照のみを目的としており、更新されなくなりました。\nKernelSU v3.0以降、より高速な反復とビルド速度のため、GKIイメージモードの公式サポートを終了しました。`Ylarod/ddk` を使用してLKMをビルドすることをお勧めします。\n:::\n\nまず、Android の公式ドキュメントを読むべきです：\n\n1. [カーネルをビルドする](https://source.android.com/docs/setup/build/building-kernels)\n2. [GKI リリースビルド](https://source.android.com/docs/core/architecture/kernel/gki-release-builds)\n\n::: 警告\nこのページは GKI デバイス用です。もし古いカーネルを使用している場合は、[古いカーネルへの KernelSU の統合方法](how-to-integrate-for-non-gki)を参照してください。\n:::\n\n## カーネルビルド\n\n### カーネルソースコードの同期\n\n```sh\nrepo init -u https://android.googlesource.com/kernel/manifest\nmv <kernel_manifest.xml> .repo/manifests\nrepo init -m manifest.xml\nrepo sync\n```\n\n`<kernel_manifest.xml>` は、ビルドを一意に決定するマニフェストファイルです。マニフェストを使用して再現可能なビルドを行えます。マニフェストファイルは [Google GKI リリースビルド](https://source.android.com/docs/core/architecture/kernel/gki-release-builds) からダウンロードしてください。\n\n### ビルド\n\nまずは [公式ドキュメント](https://source.android.com/docs/setup/build/building-kernels)を確認してください。\n\nたとえば、aarch64 カーネルイメージをビルドする必要があります：\n\n```sh\nLTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh\n```\n`LTO=thin` フラグを追加するのを忘れないでください。それをしないと、コンピュータのメモリが 24Gb 未満の場合にビルドに失敗する可能性があります。\n\nAndroid 13 からは、カーネルは `bazel` によってビルドされます：\n\n```sh\ntools/bazel build --config=fast //common:kernel_aarch64_dist\n```\n## KernelSU を使ったカーネルビルド\n\nもしカーネルを正常にビルドできた場合、KernelSU をビルドするのは簡単です。カーネルソースのルートディレクトリで任意のものを選択して実行します：\n\n::: code-group\n```sh[最新タグ(安定版)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n```sh[ main ブランチ (開発用)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\n```\n```sh[タグを選択 (例：v0.5.2)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\n```\n\n:::\n\nその後でカーネルを再ビルドすると、KernelSU が組み込まれたカーネルイメージが得られます！\n"
  },
  {
    "path": "website/docs/ja_JP/guide/how-to-integrate-for-non-gki.md",
    "content": "# 非 GKI カーネルで KernelSU を統合する方法は？\n\n::: warning\nこのドキュメントはアーカイブ参照のみを目的としており、更新されなくなりました。\nKernelSU v1.0以降、非GKIデバイスの公式サポートを終了しました。\n:::\n\nKernelSU は非 GKI カーネルに統合することが可能であり、4.14 以下のバージョンにバックポートされました。\n\n非 GKI カーネルの断片化のため、統一されたビルド方法がありませんので、非 GKI ブートイメージを提供することができません。しかし、KernelSU を統合して自分自身でカーネルをビルドすることができます。\n\nまず、カーネルソースコードからブート可能なカーネルをビルドできる能力が必要です。もしカーネルがオープンソースでない場合、あなたのデバイスで KernelSU を実行することは困難です。\n\nブート可能なカーネルをビルドできるなら、カーネルソースコードに KernelSU を統合する方法は二つあります：\n\n1. `kprobe` で自動的に\n2. 手動で\n\n## kprobe で統合する\n\nKernelSU は kprobe を使ってカーネルフックを行います。もし *kprobe* があなたのカーネルでうまく動作する場合、この方法を使うことを推奨します。\n\nまず、KernelSU をカーネルソースツリーに追加してください：\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\n次に、*kprobe* がカーネル設定で有効になっているか確認してください。もし有効でなければ、これらの設定を追加してください：\n\n```\nCONFIG_KPROBES=y\nCONFIG_HAVE_KPROBES=y\nCONFIG_KPROBE_EVENTS=y\n```\nそしてカーネルを再度ビルドしてください。KernelSU はうまく動作するはずです。\n\nKPROBES がまだ有効化されていない場合は、CONFIG_MODULES を有効化して試みることができます。（それでも効果がない場合は、make menuconfig を使って KPROBES の他の依存関係を検索してください）\n\nしかし、KernelSU を統合した際にブートループに遭遇した場合、それは *kprobe* があなたのカーネルで破損している可能性があります。kprobe のバグを修正するか、二番目の方法を使用するべきです。\n\n:::tip kprobe が破損しているかどうかを確認する方法は？\n\n`KernelSU/kernel/ksu.c` にある `ksu_sucompat_init()` と `ksu_ksud_init()` をコメントアウトし、デバイスが正常にブートするか試してください。もし正常にブートするならば、kprobe が破損している可能性があります。\n\n## カーネルソースを手動で変更する\n\nもし kprobe があなたのカーネルで機能しない場合（上流のバグや 4.8 以下のカーネルバグが原因かもしれません）、以下の方法を試すことができます。\n\nまず、KernelSU をカーネルソースツリーに追加してください：\n\n::: code-group\n## カーネルソースを手動で変更する\n\nもし kprobe があなたのカーネルで機能しない場合（上流のバグや 4.8 以下のカーネルバグが原因かもしれません）、以下の方法を試すことができます。\n\nまず、KernelSU をカーネルソースツリーに追加してください：\n\n::: code-group\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\n[ main branch(dev)]\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\n```\n\n[Select tag(Such as v0.5.2)]\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\n```\n\n\n:::\n\nいくつかのデバイスでは、あなたの defconfig が `arch/arm64/configs` にあったり、または他のケースでは `arch/arm64/configs/vendor/your_defconfig` にあることを念頭に置いてください。例えばあなたの defconfig で、`CONFIG_KSU` を y で有効に、または n で無効に設定します。あなたのパスは次のようになるでしょう：\n`arch/arm64/configs/...`\n```\n# KernelSU\nCONFIG_KSU=y\n```\n次に、KernelSU の呼び出しをカーネルソースに追加します。こちらは参照のためのパッチです：\n\n::: code-group\n\n```diff[exec.c]\ndiff --git a/fs/exec.c b/fs/exec.c\nindex ac59664eaecf..bdd585e1d2cc 100644\n--- a/fs/exec.c\n+++ b/fs/exec.c\n@@ -1890,11 +1890,14 @@ static int __do_execve_file(int fd, struct filename *filename,\n \treturn retval;\n }\n\n+#ifdef CONFIG_KSU\n+extern bool ksu_execveat_hook __read_mostly;\n+extern int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv,\n+\t\t\tvoid *envp, int *flags);\n+extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,\n+\t\t\t\t void *argv, void *envp, int *flags);\n+#endif\n static int do_execveat_common(int fd, struct filename *filename,\n \t\t\t      struct user_arg_ptr argv,\n \t\t\t      struct user_arg_ptr envp,\n \t\t\t      int flags)\n {\n+   #ifdef CONFIG_KSU\n+\tif (unlikely(ksu_execveat_hook))\n+\t\tksu_handle_execveat(&fd, &filename, &argv, &envp, &flags);\n+\telse\n+\t\tksu_handle_execveat_sucompat(&fd, &filename, &argv, &envp, &flags);\n+   #endif\n \treturn __do_execve_file(fd, filename, argv, envp, flags, NULL);\n }\n```\n```diff[open.c]\ndiff --git a/fs/open.c b/fs/open.c\nindex 05036d819197..965b84d486b8 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -348,6 +348,8 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn ksys_fallocate(fd, mode, offset, len);\n }\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t int *flags);\n+#endif\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -355,6 +357,7 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n  */\n long do_faccessat(int dfd, const char __user *filename, int mode)\n {\n \tconst struct cred *old_cred;\n \tstruct cred *override_cred;\n \tstruct path path;\n \tstruct inode *inode;\n \tstruct vfsmount *mnt;\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n+   #ifdef CONFIG_KSU\n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n+   #endif\n \n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n```diff[read_write.c]\ndiff --git a/fs/read_write.c b/fs/read_write.c\nindex 650fc7e0f3a6..55be193913b6 100644\n--- a/fs/read_write.c\n+++ b/fs/read_write.c\n@@ -434,10 +434,14 @@ ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)\n }\n EXPORT_SYMBOL(kernel_read);\n\n+#ifdef CONFIG_KSU\n+extern bool ksu_vfs_read_hook __read_mostly;\n+extern int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,\n+\t\t\tsize_t *count_ptr, loff_t **pos);\n+#endif\n ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)\n {\n \tssize_t ret;\n+   #ifdef CONFIG_KSU \n+\tif (unlikely(ksu_vfs_read_hook))\n+\t\tksu_handle_vfs_read(&file, &buf, &count, &pos);\n+   #endif\n+\n \tif (!(file->f_mode & FMODE_READ))\n \t\treturn -EBADF;\n \tif (!(file->f_mode & FMODE_CAN_READ))\n```\n```diff[stat.c]\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 376543199b5a..82adcef03ecc 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -148,6 +148,8 @@ int vfs_statx_fd(unsigned int fd, struct kstat *stat,\n }\n EXPORT_SYMBOL(vfs_statx_fd);\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+#endif\n+\n /**\n  * vfs_statx - Get basic and extra attributes by filename\n  * @dfd: A file descriptor representing the base dir for a relative filename\n@@ -170,6 +172,7 @@ int vfs_statx(int dfd, const char __user *filename, int flags,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_AUTOMOUNT;\n\n+   #ifdef CONFIG_KSU\n+\tksu_handle_stat(&dfd, &filename, &flags);\n+   #endif\n \tif ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t       AT_EMPTY_PATH | KSTAT_QUERY_FLAGS)) != 0)\n \t\treturn -EINVAL;\n```\nカーネル ソースには 4 つの関数があるはずです。\n\n1. do_faccessat、通常は `fs/open.c` にあります\n2. do_execveat_common (通常は `fs/exec.c` にあります)\n3. vfs_read (通常は `fs/read_write.c` にあります)\n4. vfs_statx (通常は「fs/stat.c」にあります)\n\nカーネルに `vfs_statx` がない場合は、代わりに `vfs_fstatat` を使用してください:\n\n```diff\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 068fdbcc9e26..5348b7bb9db2 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -87,6 +87,8 @@ int vfs_fstat(unsigned int fd, struct kstat *stat)\n }\n EXPORT_SYMBOL(vfs_fstat);\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+#endif\n int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \t\tint flag)\n {\n@@ -94,6 +96,8 @@ int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = 0;\n+   #ifdef CONFIG_KSU \n+\tksu_handle_stat(&dfd, &filename, &flag);\n+   #endif\n+\n \tif ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t      AT_EMPTY_PATH)) != 0)\n \t\tgoto out;\n```\n\n4.17 より前のカーネルの場合、`do faccessat` が見つからない場合は、`faccessat` システムコールの定義に移動して、そこで呼び出しを実行します。\n\n```diff\ndiff --git a/fs/open.c b/fs/open.c\nindex 2ff887661237..e758d7db7663 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -355,6 +355,9 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn error;\n }\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t        int *flags);\n+#endif\n+\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -370,6 +373,8 @@ SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n+   #ifdef CONFIG_KSU\n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n+   #endif\n+\n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n\nKernelSU の組み込み SafeMode を有効にするには、`drivers/input/input.c` の `input_handle_event` も変更する必要があります。\n\n:::ヒント\nこの機能を有効にすることを強くお勧めします。ブートループを防ぐのに非常に役立ちます!\n:::\n\n```diff\ndiff --git a/drivers/input/input.c b/drivers/input/input.c\nindex 45306f9ef247..815091ebfca4 100755\n--- a/drivers/input/input.c\n+++ b/drivers/input/input.c\n@@ -367,10 +367,13 @@ static int input_get_disposition(struct input_dev *dev,\n \treturn disposition;\n }\n\n+#ifdef CONFIG_KSU\n+extern bool ksu_input_hook __read_mostly;\n+extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value);\n+#endif\n+\n static void input_handle_event(struct input_dev *dev,\n \t\t\t       unsigned int type, unsigned int code, int value)\n {\n\tint disposition = input_get_disposition(dev, type, code, &value);\n+   #ifdef CONFIG_KSU\n+\tif (unlikely(ksu_input_hook))\n+\t\tksu_handle_input_handle_event(&type, &code, &value);\n+   #endif\n \n \tif (disposition != INPUT_IGNORE_EVENT && type != EV_SYN)\n \t\tadd_input_randomness(type, code, value);\n```\n\n最後に、カーネルを再度ビルドすると、KernelSU が正常に動作するはずです。\n\n### How to backport path_umount\n\nYou can make the \"Umount modules\" feature work on pre-GKI kernels by manually backporting `path_umount` from 5.9. You can use this patch as reference:\n\n```diff\n--- a/fs/namespace.c\n+++ b/fs/namespace.c\n@@ -1739,6 +1739,39 @@ static inline bool may_mandlock(void)\n }\n #endif\n\n+static int can_umount(const struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\n+\tif (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW))\n+\t\treturn -EINVAL;\n+\tif (!may_mount())\n+\t\treturn -EPERM;\n+\tif (path->dentry != path->mnt->mnt_root)\n+\t\treturn -EINVAL;\n+\tif (!check_mnt(mnt))\n+\t\treturn -EINVAL;\n+\tif (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */\n+\t\treturn -EINVAL;\n+\tif (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN))\n+\t\treturn -EPERM;\n+\treturn 0;\n+}\n+\n+int path_umount(struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\tint ret;\n+\n+\tret = can_umount(path, flags);\n+\tif (!ret)\n+\t\tret = do_umount(mnt, flags);\n+\n+\t/* we mustn't call path_put() as that would clear mnt_expiry_mark */\n+\tdput(path->dentry);\n+\tmntput_no_expire(mnt);\n+\treturn ret;\n+}\n /*\n  * Now umount can handle mount points as well as block devices.\n  * This is important for filesystems which use unnamed block devices.\n```\n\nFinally, build your kernel again, and KernelSU should work correctly.\n\n:::info 誤ってセーフ モードに入ってしまった場合は、\n手動統合を使用し、`CONFIG_KPROBES` を無効にしない場合、ユーザーは起動後に音量を下げるボタンを押してセーフ モードをトリガーする可能性があります。 したがって、手動統合を使用する場合は、`CONFIG_KPROBES` を無効にする必要があります。\n:::\n"
  },
  {
    "path": "website/docs/ja_JP/guide/installation.md",
    "content": "# インストール\n\n## デバイスが対応しているか確認する\n\n[GitHub Releases](https://github.com/tiann/KernelSU/releases) から KernelSU Manager アプリをダウンロードし、お使いのデバイスにインストールしてください。\n\n- アプリが「非対応」と表示した場合は、**自分でカーネルをコンパイルする必要がある**という意味です。KernelSU は書き込むためのブートイメージを提供しません。\n- アプリが「未インストール」と表示した場合、お使いのデバイスは KernelSU に対応しています。\n\n::: info ヒント\n非対応と表示されているデバイスについては、[非公式の対応デバイス](unofficially-support-devices.md)であればご自身でカーネルをビルドできます。\n:::\n\n## 純正の boot.img をバックアップ\n\n書き込む前に、まず純正の boot.img をバックアップする必要があります。ブートループが発生した場合は、fastboot を使用して純正のブートイメージを書き込むことでいつでもシステムを復旧できます。\n\n::: warning 警告\n書き込みによりデータ損失を引き起こす可能性があります。次のステップに進む前に、このステップを必ず行うようにしてください！また、可能であればすべてのデータをバックアップしてください。\n:::\n\n## 必要な知識\n\n### ADB と fastboot\n\nこのチュートリアルでは、デフォルトで ADB と fastboot のツールを使用します。ご存じない方は、まず検索エンジンを使って勉強されることをおすすめします。\n\n### KMI\n\n同じ Kernel Module Interface (KMI) のカーネルバージョンは**互換性があります**。これが GKI の「汎用」という意味です。逆に言えば KMI が異なればカーネルには互換性がなく、お使いのデバイスと異なる KMI のカーネルイメージを書き込むと、ブートループが発生する場合があります。\n\n具体的には GKI デバイスの場合、カーネルバージョンの形式は以下のようになります：\n\n```txt\nKernelRelease :=\nVersion.PatchLevel.SubLevel-AndroidRelease-KmiGeneration-suffix\nw      .x         .y       -zzz           -k            -something\n```\n\n`w.x-zzz-k` は KMI のバージョンです。例えば、デバイスのカーネルバージョンが `5.10.101-android12-9-g30979850fc20` である場合、その KMIは `5.10-android12-9` であり、理論的には他の KMI カーネルでも正常に起動できます。\n\n::: tip ヒント\nカーネルバージョンの SubLevel は、KMI の一部ではないことに注意してください。`5.10.101-android12-9-g30979850fc20` は `5.10.137-android12-9-g30979850fc20` と同じ KMI を持っているということになります。\n:::\n\n### セキュリティパッチレベル {#security-patch-level}\n\n新しめの Android 端末には、セキュリティパッチレベルが古い boot イメージのフラッシュを許可しないロールバック防止機構が導入されていることがあります。例えば、もしあなたの端末のカーネルが `5.10.101-android12-9-g30979850fc20` であれば、セキュリティパッチレベルは `2023-11` です。たとえ KMI に対応するカーネルをフラッシュしても、セキュリティパッチレベルが (`2023-06` のように) `2023-11` より低い場合、ブートループを起こす可能性があります。\n\n### Kernel バージョンと Android バージョンの違い\n\n注意： **カーネルバージョンと Android バージョンは必ずしも同じではありません**。\n\nカーネルのバージョンは「android12-5.10.101」なのに、Android システムのバージョンは「Android 13」などとなっていても、驚かないでください。Android システムのバージョン番号は、必ずしも Linux カーネルのバージョン番号と同じではありません。Linux カーネルのバージョン番号は、通常、**デバイスの出荷時**にプリインストールされている Android システムのバージョンに一致しています。Android システムが後でアップグレードされた場合、一般的にはカーネルのバージョンは変更されません。フラッシュをする際は、**必ずカーネルバージョンを参照してください**!!!\n\n## はじめに\n\nバージョン [0.9.0](https://github.com/tiann/KernelSU/releases/tag/v0.9.0) 以降、KernelSU は GKI 端末において 2 つの動作モードに対応しています。\n\n1. `GKI`: 端末本来のカーネルを KernelSU によって提供される **汎用カーネルイメージ** (GKI) で置き換えます。\n2. `LKM`: 端末本来のカーネルを置き換えずに、カーネルに **ローダブルカーネルモジュール** (LKM) を読み込みます。\n\nこれらの 2 つのモードはそれぞれ異なる状況に適しており、必要に応じてどちらかを選ぶことができます。\n\n### GKI モード {#gki-mode}\n\nGKI モードでは、端末本来のカーネルは KernelSU によって提供される汎用カーネルイメージによって置き換えられます。GKI モードの利点は:\n\n1. 強い汎用性があり、大部分の端末に適合。例えば、Samsung 端末は KNOX が有効であり、LKM モードは動作できません。また、GKI モードしか使えないニッチな改造を施された端末もあります。\n2. 公式ファームウェアに依存せずに使うことができます。KMI が一致している限り、公式ファームウェアの更新を待つことなく使うことができます。\n\n### LKM モード {#lkm-mode}\n\nLKM モードでは、端末本来のカーネルを置き換えずに、カーネルにローダブルカーネルモジュールを読み込みます。LKM モードの利点は:\n\n1. 端末本来のカーネルを置き換えません。端末本来のカーネルに特別な要件がある場合や、KernelSU をサードパーティのカーネルと両用したい場合、LKM モードを利用できます。\n2. アップグレードと OTA がより便利です。KernelSU をアップデートする時は、手動でフラッシュすることなくマネージャーから直接インストールできます。システム OTA の後は、手動でフラッシュすることなく非アクティブなスロットに直接インストールすることができます。\n3. いくつかの特殊な状況に適しています。例えば、LKM は一時的な Root 権限を用いて読み込むことができます。boot パーティションを置き換えないので、AVB を動作させず、端末を文鎮化しません。\n4. LKM は一時的にアンインストールすることができます。一時的に root アクセスを無効化したい場合、LKM をアンインストールすることができます。この過程でパーティションをフラッシュすることも、また再起動することさえも必要ありません。root アクセスを再度有効化したい場合は、再起動するだけです。\n\n:::tip 2 つのモードの共存\nマネージャーを開くと、ホームで現在の端末の動作モードを確認できます。GKI モードの優先度は LKM モードより高いことに留意してください。例えば、本来のカーネルを GKI カーネルで置き換えたうえで LKM を使って GKI カーネルにパッチしている場合、LKM は無視され、端末は常時 GKI モードで動作します。\n:::\n\n### どちらを選ぶべきですか? {#which-one}\n\n端末が携帯電話の場合、LKM モードを優先することを推奨します。端末がエミュレーター、WSA、または Waydroid の場合、GKI モードを優先することを推奨します。\n\n## LKM モードのインストール\n\n### 公式ファームウェアの入手\n\nLKM モードを使うためには、公式ファームウェアを入手し、それを基にパッチを当てる必要があります。サードパーティのカーネルを使う場合、その `boot.img` を公式ファームウェアとして利用できます。\n\n公式ファームウェアを入手する方法はたくさんあります。端末が `fastboot boot` に対応している場合、`fastboot boot` を使って一時的に KernelSU が提供する GKI カーネルを起動する、 **最も推奨される、最も簡単な** 方法を推奨します。\n\n端末が `fastboot boot` に対応していない場合、公式ファームウェアのパッケージを手動でダウンロードし、そこから `boot.img` を展開する必要があります。\n\nGKI モードとは異なり、LKM モードは `ramdisk` を展開します。そのため、Android 13 の端末では `boot` パーティションの代わりに `init_boot` パーティションにパッチを当てる必要があります。一方で、GKI モードは常に `boot` パーティションを操作します。\n\n### マネージャーを使う\n\nマネージャーを開き、右上のインストールアイコンをタップすると、いくつかのオプションが現れます:\n\n1. ファイルを選択しパッチを当てる。端末が Root 権限を持っていない場合、このオプションを選択し、公式ファームウェアを選択すると、マネージャーが自動的にパッチを当てます。恒久的に Root 権限を獲得するには、パッチが当てられたファイルをフラッシュするだけです。\n2. 直接インストール。端末が既に Root 化されている場合、このオプションを選択すると、マネージャーが自動的に端末の情報を取得し、自動的に公式ファームウェアにパッチを当て、それをフラッシュします。`fastboot boot` で KernelSU の GKI を起動し一時的に Root 権限を獲得したうえでこのオプションを利用することもできます。これは KernelSU をアップグレードする主な方法でもあります。\n3. 非アクティブなスロットにインストール。端末が A/B パーティションに対応している場合、このオプションを選択すると、マネージャーが自動的に公式ファームウェアにパッチを当て、もう一方のパーティションにインストールします。この方法は OTA 後の端末に適しており、OTA 後にもう一方のパーティションに直接 KernelSU をインストールし、端末を再起動することができます。\n\n### コマンドラインを使う\n\nマネージャーを使いたくない場合、コマンドラインを使って LKM をインストールできます。KernelSU の提供する `ksud` ツールを使うと、迅速に公式ファームウェアにパッチを当てフラッシュすることができます。\n\nこのツールは macOS、Linux および Windows に対応しています。[GitHub Release](https://github.com/tiann/KernelSU/releases) から対応するバージョンをダウンロードしてください。\n\n使用方法: `ksud bootpatch` コマンドの各オプションのヘルプをコマンドラインから確認できます。\n```sh\noriole:/ # ksud boot-patch -h\nPatch boot or init_boot images to apply KernelSU\n\nUsage: ksud boot-patch [OPTIONS]\n\nOptions:\n  -b, --boot <BOOT>              boot image path, if not specified, will try to find the boot image automatically\n  -k, --kernel <KERNEL>          kernel image path to replace\n  -m, --module <MODULE>          LKM module path to replace, if not specified, will use the builtin one\n  -i, --init <INIT>              init to be replaced\n  -u, --ota                      will use another slot when boot image is not specified\n  -f, --flash                    Flash it to boot partition after patch\n  -o, --out <OUT>                output path, if not specified, will use current directory\n      --magiskboot <MAGISKBOOT>  magiskboot path, if not specified, will use builtin one\n      --kmi <KMI>                KMI version, if specified, will use the specified KMI\n  -h, --help                     Print help\n```\n\n説明が必要ないくつかのオプション:\n\n1. `--magiskboot` オプションは magiskboot のパスを指定します。指定されていない場合、ksud は環境変数を参照します。magiskboot の入手方法がわからない場合、[こちら](#patch-boot-image) を参照してください。\n2. `--kmi` オプションは `KMI` バージョンを指定します。端末のカーネル名が KMI の仕様に準拠していない場合は、このオプションで指定することができます。\n\n最も一般的な使い方: \n\n```sh\nksud boot-patch -b <boot.img> --kmi android13-5.10\n```\n\n## GKI モードのインストール\n\nGKI モードのインストール方法はいくつかあり、それぞれ適したシーンが異なりますので、必要に応じて選択してください。\n\n1. KernelSU が提供する boot.img を使用し、fastboot でインストールする\n2. KernelFlasher などのカーネル管理アプリでインストールする\n3. boot.img を手動でパッチしてインストールする\n4. カスタムリカバリー（TWRPなど）でインストールする\n\n## KernelSU が提供する boot.img を使用してインストール\n\nこの方法は TWRP や root 権限を必要としないので、KernelSU を初めてインストールする場合に適しています。\n\n### 正しい boot.img を見つける\n\nKernelSU では、GKI デバイス用の汎用 boot.img を提供しています。デバイスの boot パーティションに boot.img をフラッシュする必要があります。\n\nboot.img は、[GitHub Release](https://github.com/tiann/KernelSU/releases) からダウンロードできます。例えば、あなたのデバイスがカーネル `android12-5.10.101` の場合、`android-5.10.101_yyyy-MM.boot-<format>.img`をダウンロードする必要があります。（KMI を同じにしてください！）。\n\n`<format>`は純正 boot.img のカーネル圧縮形式を指します。純正の boot.img のカーネル圧縮形式を確認してください。間違った圧縮形式を使うと、ブートループするかもしれません。\n\n::: info 情報\n1. magiskboot を使えば、元のブートの圧縮形式を知ることができます。もちろん、あなたのデバイスと同じモデルを持つ、より経験豊富な他の人にも聞くこともできます。また、カーネルの圧縮形式は通常変更されないので、ある圧縮形式でうまく起動した場合、後でその形式を試すことも可能です。\n2. Xiaomi デバイスでは通常 `gz` か**無圧縮**が使われます。\n3. Pixel デバイスでは以下の手順に従ってください。\n:::\n\n### boot.img をデバイスに書き込む\n\n`adb` でデバイスを接続し、`adb reboot bootloader` で fastboot モードにし、このコマンドで KernelSU を書き込んでください：\n\n```sh\nfastboot flash boot boot.img\n```\n\n::: info 情報\nデバイスが `fastboot boot` をサポートしている場合、まず `fastboot boot.img` を使えば書き込みせずにシステムを起動できます。予期せぬことが起こった場合は、もう一度再起動して起動してください。\n:::\n\n### 再起動\n\n書き込みが完了したら、デバイスを再起動します：\n\n```sh\nfastboot reboot\n```\n\n## カーネル管理アプリでインストール\n\n前提条件：お使いのデバイスが root 化されている必要があります。例えば、Magisk をインストールして root を取得した場合、または古いバージョンの KernelSU をインストールしており、別のバージョンの KernelSU にアップグレードする必要がある場合などです。お使いのデバイスが root 化されていない場合、他の方法をお試しください。\n\n手順:\n\n1. AnyKernel3 ZIP をダウンロードします。ダウンロード方法は、「カスタムリカバリーでインストール」を参照してください。\n2. カーネル管理アプリを開き、AnyKernel3 の ZIP をインストールします。\n\nカーネル管理アプリは以下のようなものが人気です：\n\n1. [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases)\n2. [Franco Kernel Manager](https://play.google.com/store/apps/details?id=com.franco.kernel)\n3. [Ex Kernel Manager](https://play.google.com/store/apps/details?id=flar2.exkernelmanager)\n\nこの方法は KernelSU をアップグレードするときに便利で、パソコンがなくてもできます。（まずはバックアップしてください！）\n\n## boot.img を手動でパッチ {#patch-boot-image}\n\nデバイスによっては、boot.img のフォーマットが `lz4` や、 `gz` 、無圧縮などの一般的な形式ではないことがあります。最も典型的なのは Pixel で、boot.img フォーマットは `lz4_legacy` 圧縮で、RAM ディスクは `gz` か `lz4_legacy` 圧縮です。この時、KernelSU が提供した boot.img を直接書き込むとデバイスが起動できなくなる場合があります。その場合は手動で boot.img に対してパッチしてください。\n\n常に `magiskboot` を利用してイメージにパッチを当てることが推奨されます。2 つの方法があります:\n\n1. [magiskboot](https://github.com/topjohnwu/Magisk/releases)\n2. [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci)\n\n公式の `magisikboot` ビルドは Android 端末でしか動作しないため、PC で作業したい場合、2 つめの方法を試してください。\n\n:::tip\nAndroid-Image-Kitchen は現在非推奨です。セキュリティパッチレベルなどの boot メタデータを正しく扱わないため、一部のデバイスでは動作しない可能性があります。\n:::\n\n### 準備\n\n1. お使いのデバイスの純正 boot.img を入手します。デバイスメーカーから入手できます。[payload-dumper-go](https://github.com/ssut/payload-dumper-go)が必要かもしれません。\n2. お使いのデバイスの KMI バージョンに合った、KernelSU が提供する AnyKernel3 の ZIP ファイルをダウンロードします（*カスタムリカバリーでインストール*を参照してください）。\n3. AnyKernel3 パッケージを展開し、KernelSU のカーネルファイルである `Image` ファイルを取得します。\n\n### Android 端末で magiskboot を使う {#using-magiskboot-on-Android-devices}\n\n1. 最新の Magisk を[リリースページ](https://github.com/topjohnwu/Magisk/releases)からダウンロードしてください。\n2. `Magisk-*(version).apk` を `Magisk-*.zip` に名前を変更して展開してください。\n3. `Magisk-*/lib/arm64-v8a/libmagiskboot.so`を adb でデバイスに転送します：`adb push Magisk-*/lib/arm64-v8a/libmagiskboot.so /data/local/tmp/magiskboot`\n4. 純正 boot.img と AnyKernel3 の中の Image をデバイスに転送します。\n5. adb shell に入り、`cd /data/local/tmp/` し、`chmod +x magiskboot` を実行します。\n6. adb shell に入り、`cd /data/local/tmp/` し、`./magiskboot unpack boot.img` を実行して `boot.img` を抽出します。`kernel` ファイルが純正カーネルです。\n7. `kernel` を `Image` で置き換えます: `mv -f Image kernel`\n8. `./magiskboot repack boot.img` を実行してブートイメージをリパックします。出来上がった `new-boot.img` を fastboot でデバイスに書き込んでください。\n\n### Windows/macOS/Linux PC で magiskboot を使う {#using-magiskboot-on-PC}\n\n1. OS に対応する `magiskboot` バイナリを [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci) からダウンロードします。\n2. 純正の `boot.img` と `Image` を PC 上に準備します。\n3. `chmod +x magiskboot` を実行します。\n4. 対応するディレクトリに入り、`./magiskboot unpack boot.img` を実行して `boot.img` を展開します。展開された `kernel` ファイルが純正のカーネルです。\n5. 下記のコマンドを実行し、`kernel` を `Image` で置き換えてください: `mv -f Image kernel`\n6. `./magiskboot repack boot.img` を実行し、boot イメージを再パックします。作成された `new-boot.img` ファイルを、fastboot を利用して端末にフラッシュします。\n\n::: info\n公式の `magiskboot` は、`Linux` 環境においては正常に動作します。Linux を使っている場合、公式のビルドを利用できます。\n:::\n\n## カスタムリカバリーでインストール {#install-with-custom-recovery}\n\n前提条件：デバイスに TWRP などのカスタムリカバリーがあること。ない場合、または公式リカバリーしかない場合は他の方法を使用してください。\n\n手順:\n\n1. KernelSUの[リリースページ](https://github.com/tiann/KernelSU/releases)から、お使いのデバイスのバージョンにあった AnyKernel3 で始まる ZIP パッケージをダウンロードします。例えば、デバイスのカーネルのバージョンが`android12-5.10. 66`の場合、AnyKernel3-android12-5.10.66_yyyy-MM.zip`（yyyy`は年、`MM`は月）のファイルをダウンロードします。\n2. デバイスを TWRP へ再起動します。\n3. adb を使用して AnyKernel3-*.zip をデバイスの /sdcard に入れ、TWRP GUI でインストールを選択します。または直接`adb sideload AnyKernel-*.zip` でインストールできます。\n\nこの方法は TWRP を使用できるならどのようなインストール（初期インストールやその後のアップグレード）にも適しています。\n\n## その他の方法\n\n実はこれらのインストール方法はすべて、**元のカーネルを KernelSU が提供するカーネルに置き換える**という主旨でしかなく、これが実現できれば他の方法でもインストール可能です：\n\n## インストール後：モジュールサポート {#post-installation}\n\n::: warning システムファイル変更用の METAMODULE\n`/system` ファイルを変更するモジュールを使用したい場合は、KernelSU インストール後に **metamodule** をインストールする必要があります。スクリプト、sepolicy、または system.prop のみを使用するモジュールは metamodule なしで動作します。\n:::\n\n**`/system` 変更サポートが必要な場合**、[Metamodule ガイド](metamodule.md)を参照して：\n- metamodule とは何か、なぜ必要なのかを理解する\n- 公式の `meta-overlayfs` metamodule をインストールする\n- 他の metamodule オプションについて学ぶ\n\n1. まず Magisk をインストールし、Magisk を通じて root 権限を取得し、カーネル管理アプリで KernelSU の AnyKernel ZIPをインストールする\n2. PC 上で何らかの書き込みツールを使用し、KernelSU が提供するカーネルを書き込む\n\nしかし、これらの方法でうまく行かない場合、`magiskboot` を使う方法を試してみてください。\n"
  },
  {
    "path": "website/docs/ja_JP/guide/metamodule.md",
    "content": "# メタモジュール\n\nメタモジュールは、KernelSU の革命的な機能であり、モジュールシステムの重要な機能をコアデーモンからプラグイン可能なモジュールに移行します。このアーキテクチャの転換により、KernelSU の安定性とセキュリティを維持しながら、モジュールエコシステムのより大きなイノベーションの可能性を解き放ちます。\n\n## メタモジュールとは?\n\nメタモジュールは、モジュールシステムのコアインフラストラクチャ機能を提供する特別なタイプの KernelSU モジュールです。システムファイルを変更する通常のモジュールとは異なり、メタモジュールは通常のモジュールの*インストールとマウントの方法*を制御します。\n\nメタモジュールは、KernelSU のモジュール管理インフラストラクチャの完全なカスタマイズを可能にするプラグインベースの拡張メカニズムです。マウントとインストールのロジックをメタモジュールに委任することで、KernelSU は脆弱な検出ポイントになることを避けながら、多様な実装戦略を可能にします。\n\n**主な特徴:**\n\n- **インフラストラクチャの役割**: メタモジュールは通常のモジュールが依存するサービスを提供します\n- **単一インスタンス**: 一度に1つのメタモジュールのみインストールできます\n- **優先実行**: メタモジュールスクリプトは通常のモジュールスクリプトの前に実行されます\n- **特別なフック**: インストール、マウント、クリーンアップのための3つのフックスクリプトを提供します\n\n## なぜメタモジュールが必要なのか?\n\n従来のルートソリューションは、マウントロジックをコアに組み込んでおり、検出されやすく、進化させることが困難です。KernelSU のメタモジュールアーキテクチャは、関心の分離によってこれらの問題を解決します。\n\n**戦略的優位性:**\n\n- **検出面の削減**: KernelSU 自体はマウントを実行しないため、検出ベクトルが減少します\n- **安定性**: コアデーモンは安定したままで、マウント実装は進化し続けることができます\n- **イノベーション**: コミュニティは KernelSU をフォークすることなく、代替マウント戦略を開発できます\n- **選択**: ユーザーは自分のニーズに最適な実装を選択できます\n\n**マウントの柔軟性:**\n\n- **マウントなし**: マウントレスモジュールのみを使用するユーザーは、マウントのオーバーヘッドを完全に回避できます\n- **OverlayFS マウント**: 読み書きレイヤーサポートを備えた従来のアプローチ(`meta-overlayfs` 経由)\n- **マジックマウント**: より良いアプリ互換性のための Magisk 互換マウント\n- **カスタム実装**: FUSE ベースのオーバーレイ、カスタム VFS マウント、または全く新しいアプローチ\n\n**マウントを超えて:**\n\n- **拡張性**: コア KernelSU を変更することなく、カーネルモジュールサポートなどの機能を追加できます\n- **モジュール性**: KernelSU リリースとは独立して実装を更新できます\n- **カスタマイズ**: 特定のデバイスやユースケースに特化したソリューションを作成できます\n\n::: warning 重要\nメタモジュールがインストールされていない場合、モジュールは**マウントされません**。新規の KernelSU インストールでは、モジュールを機能させるためにメタモジュール(`meta-overlayfs` など)をインストールする必要があります。\n:::\n\n## ユーザー向け\n\n### メタモジュールのインストール\n\n通常のモジュールと同じ方法でメタモジュールをインストールします:\n\n1. メタモジュール ZIP ファイル(例: `meta-overlayfs.zip`)をダウンロードします\n2. KernelSU Manager アプリを開きます\n3. フローティングアクションボタン(➕)をタップします\n4. メタモジュール ZIP ファイルを選択します\n5. デバイスを再起動します\n\n`meta-overlayfs` メタモジュールは、ext4 イメージサポートを備えた従来の overlayfs ベースのモジュールマウントを提供する公式リファレンス実装です。\n\n### アクティブなメタモジュールの確認\n\nKernelSU Manager アプリのモジュールページで、現在アクティブなメタモジュールを確認できます。アクティブなメタモジュールは、特別な指定とともにモジュールリストに表示されます。\n\n### メタモジュールのアンインストール\n\n::: danger 警告\nメタモジュールをアンインストールすると、**すべて**のモジュールに影響します。削除後、別のメタモジュールをインストールするまで、モジュールはマウントされなくなります。\n:::\n\nアンインストール手順:\n\n1. KernelSU Manager を開きます\n2. モジュールリストでメタモジュールを見つけます\n3. アンインストールをタップします(特別な警告が表示されます)\n4. アクションを確認します\n5. デバイスを再起動します\n\nアンインストール後、モジュールが引き続き動作するようにするには、別のメタモジュールをインストールする必要があります。\n\n### 単一メタモジュールの制約\n\n一度に1つのメタモジュールのみインストールできます。2つ目のメタモジュールをインストールしようとすると、KernelSU は競合を避けるためにインストールを防止します。\n\nメタモジュールを切り替える手順:\n\n1. すべての通常のモジュールをアンインストールします\n2. 現在のメタモジュールをアンインストールします\n3. 再起動します\n4. 新しいメタモジュールをインストールします\n5. 通常のモジュールを再インストールします\n6. 再度再起動します\n\n## モジュール開発者向け\n\n通常の KernelSU モジュールを開発している場合、メタモジュールについてあまり心配する必要はありません。ユーザーが互換性のあるメタモジュール(`meta-overlayfs` など)をインストールしていれば、モジュールは動作します。\n\n**知っておくべきこと:**\n\n- **マウントにはメタモジュールが必要**: モジュール内の `system` ディレクトリは、ユーザーがマウント機能を提供するメタモジュールをインストールしている場合にのみマウントされます\n- **コード変更不要**: 既存のモジュールは変更なしで引き続き動作します\n\n::: tip\nMagisk モジュール開発に精通している場合、メタモジュールをインストールすると、Magisk 互換のマウントを提供するため、モジュールは KernelSU でも同じように動作します。\n:::\n\n## メタモジュール開発者向け\n\nメタモジュールを作成すると、KernelSU がモジュールのインストール、マウント、アンインストールを処理する方法をカスタマイズできます。\n\n### 基本要件\n\nメタモジュールは、`module.prop` の特別なプロパティによって識別されます:\n\n```txt\nid=my_metamodule\nname=My Custom Metamodule\nversion=1.0\nversionCode=1\nauthor=Your Name\ndescription=Custom module mounting implementation\nmetamodule=1\n```\n\n`metamodule=1`(または `metamodule=true`)プロパティは、これをメタモジュールとしてマークします。このプロパティがない場合、モジュールは通常のモジュールとして扱われます。\n\n### ファイル構造\n\nメタモジュール構造:\n\n```txt\nmy_metamodule/\n├── module.prop              (metamodule=1 を含む必要があります)\n│\n│      *** メタモジュール固有のフック ***\n├── metamount.sh             (オプション: カスタムマウントハンドラー)\n├── metainstall.sh           (オプション: 通常モジュールのインストールフック)\n├── metauninstall.sh         (オプション: 通常モジュールのクリーンアップフック)\n│\n│      *** 標準モジュールファイル(すべてオプション) ***\n├── customize.sh             (インストールカスタマイズ)\n├── post-fs-data.sh          (post-fs-data ステージスクリプト)\n├── service.sh               (late_start service スクリプト)\n├── boot-completed.sh        (起動完了スクリプト)\n├── uninstall.sh             (メタモジュール自体のアンインストールスクリプト)\n├── system/                  (必要に応じてシステムレス変更)\n└── [その他のファイル]\n```\n\nメタモジュールは、特別なメタモジュールフックに加えて、すべての標準モジュール機能(ライフサイクルスクリプトなど)を使用できます。\n\n### フックスクリプト\n\nメタモジュールは最大3つの特別なフックスクリプトを提供できます:\n\n#### 1. metamount.sh - マウントハンドラー\n\n**目的**: 起動中にモジュールがマウントされる方法を制御します。\n\n**実行タイミング**: `post-fs-data` ステージ中、モジュールスクリプトが実行される前。\n\n**環境変数:**\n\n- `MODDIR`: メタモジュールのディレクトリパス(例: `/data/adb/modules/my_metamodule`)\n- すべての標準 KernelSU 環境変数\n\n**責任:**\n\n- すべての有効なモジュールをシステムレスにマウントする\n- `skip_mount` フラグをチェックする\n- モジュール固有のマウント要件を処理する\n\n::: danger 重要な要件\nマウント操作を実行する際、ソース/デバイス名を `\"KSU\"` に**設定する必要があります**。これにより、マウントが KernelSU に属していることが識別されます。\n\n**例(正しい):**\n\n```sh\nmount -t overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work KSU /target\n```\n\n**最新のマウント API の場合**、ソース文字列を設定します:\n\n```rust\nfsconfig_set_string(fs, \"source\", \"KSU\")?;\n```\n\nこれは、KernelSU がマウントを適切に識別して管理するために不可欠です。\n:::\n\n**スクリプト例:**\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\n\n# 例: シンプルなバインドマウント実装\nfor module in /data/adb/modules/*; do\n    if [ -f \"$module/disable\" ] || [ -f \"$module/skip_mount\" ]; then\n        continue\n    fi\n\n    if [ -d \"$module/system\" ]; then\n        # source=KSU でマウント(必須!)\n        mount -o bind,dev=KSU \"$module/system\" /system\n    fi\ndone\n```\n\n#### 2. metainstall.sh - インストールフック\n\n**目的**: 通常のモジュールのインストール方法をカスタマイズします。\n\n**実行タイミング**: モジュールのインストール中、ファイルが抽出された後、インストールが完了する前。このスクリプトは、`customize.sh` の動作と同様に、組み込みインストーラーによって**ソースされます**(実行されるのではありません)。\n\n**環境変数と関数:**\n\nこのスクリプトは、組み込みの `install.sh` からすべての変数と関数を継承します:\n\n- **変数**: `MODPATH`、`TMPDIR`、`ZIPFILE`、`ARCH`、`API`、`IS64BIT`、`KSU`、`KSU_VER`、`KSU_VER_CODE`、`BOOTMODE` など\n- **関数**:\n  - `ui_print <msg>` - コンソールにメッセージを出力\n  - `abort <msg>` - エラーを出力してインストールを終了\n  - `set_perm <target> <owner> <group> <permission> [context]` - ファイルパーミッションを設定\n  - `set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]` - 再帰的にパーミッションを設定\n  - `install_module` - 組み込みモジュールインストールプロセスを呼び出す\n\n**ユースケース:**\n\n- 組み込みインストールの前後にモジュールファイルを処理する(準備ができたら `install_module` を呼び出す)\n- モジュールファイルを移動する\n- モジュールの互換性を検証する\n- 特別なディレクトリ構造を設定する\n- モジュール固有のリソースを初期化する\n\n**注意**: このスクリプトは、メタモジュール自体をインストールする際には**呼び出されません**。\n\n#### 3. metauninstall.sh - クリーンアップフック\n\n**目的**: 通常のモジュールがアンインストールされるときにリソースをクリーンアップします。\n\n**実行タイミング**: モジュールのアンインストール中、モジュールディレクトリが削除される前。\n\n**環境変数:**\n\n- `MODULE_ID`: アンインストールされるモジュールの ID\n\n**ユースケース:**\n\n- ファイルを処理する\n- シンボリックリンクをクリーンアップする\n- 割り当てられたリソースを解放する\n- 内部追跡を更新する\n\n**スクリプト例:**\n\n```sh\n#!/system/bin/sh\n# 通常のモジュールをアンインストールするときに呼び出される\nMODULE_ID=\"$1\"\nIMG_MNT=\"/data/adb/metamodule/mnt\"\n\n# イメージからモジュールファイルを削除\nif [ -d \"$IMG_MNT/$MODULE_ID\" ]; then\n    rm -rf \"$IMG_MNT/$MODULE_ID\"\nfi\n```\n\n### 実行順序\n\n起動実行順序を理解することは、メタモジュール開発にとって重要です:\n\n```txt\npost-fs-data ステージ:\n  1. 共通の post-fs-data.d スクリプトを実行\n  2. モジュールをプルーン、restorecon、sepolicy.rule をロード\n  3. メタモジュールの post-fs-data.sh を実行(存在する場合)\n  4. 通常のモジュールの post-fs-data.sh を実行\n  5. system.prop をロード\n  6. メタモジュールの metamount.sh を実行\n     └─> すべてのモジュールをシステムレスにマウント\n  7. post-mount.d ステージが実行される\n     - 共通の post-mount.d スクリプト\n     - メタモジュールの post-mount.sh(存在する場合)\n     - 通常のモジュールの post-mount.sh\n\nservice ステージ:\n  1. 共通の service.d スクリプトを実行\n  2. メタモジュールの service.sh を実行(存在する場合)\n  3. 通常のモジュールの service.sh を実行\n\nboot-completed ステージ:\n  1. 共通の boot-completed.d スクリプトを実行\n  2. メタモジュールの boot-completed.sh を実行(存在する場合)\n  3. 通常のモジュールの boot-completed.sh を実行\n```\n\n**重要なポイント:**\n\n- `metamount.sh` は、すべての post-fs-data スクリプト(メタモジュールと通常のモジュールの両方)**の後**に実行されます\n- メタモジュールのライフサイクルスクリプト(`post-fs-data.sh`、`service.sh`、`boot-completed.sh`)は、常に通常のモジュールスクリプトの前に実行されます\n- `.d` ディレクトリの共通スクリプトは、メタモジュールスクリプトの前に実行されます\n- `post-mount` ステージは、マウントが完了した後に実行されます\n\n### シンボリックリンクメカニズム\n\nメタモジュールがインストールされると、KernelSU はシンボリックリンクを作成します:\n\n```sh\n/data/adb/metamodule -> /data/adb/modules/<metamodule_id>\n```\n\nこれにより、ID に関係なく、アクティブなメタモジュールにアクセスするための安定したパスが提供されます。\n\n**利点:**\n\n- 一貫したアクセスパス\n- アクティブなメタモジュールの簡単な検出\n- 設定の簡素化\n\n### 実例: meta-overlayfs\n\n`meta-overlayfs` メタモジュールは公式のリファレンス実装です。メタモジュール開発のベストプラクティスを示しています。\n\n#### アーキテクチャ\n\n`meta-overlayfs` は**デュアルディレクトリアーキテクチャ**を使用します:\n\n1. **メタデータディレクトリ**: `/data/adb/modules/`\n   - `module.prop`、`disable`、`skip_mount` マーカーを含む\n   - 起動中に高速スキャン\n   - 小さなストレージフットプリント\n\n2. **コンテンツディレクトリ**: `/data/adb/metamodule/mnt/`\n   - 実際のモジュールファイル(system、vendor、product など)を含む\n   - ext4 イメージ(`modules.img`)に保存\n   - ext4 機能でスペースを最適化\n\n#### metamount.sh の実装\n\n`meta-overlayfs` がマウントハンドラーを実装する方法は次のとおりです:\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\nIMG_FILE=\"$MODDIR/modules.img\"\nMNT_DIR=\"$MODDIR/mnt\"\n\n# まだマウントされていない場合は ext4 イメージをマウント\nif ! mountpoint -q \"$MNT_DIR\"; then\n    mkdir -p \"$MNT_DIR\"\n    mount -t ext4 -o loop,rw,noatime \"$IMG_FILE\" \"$MNT_DIR\"\nfi\n\n# デュアルディレクトリサポートのための環境変数を設定\nexport MODULE_METADATA_DIR=\"/data/adb/modules\"\nexport MODULE_CONTENT_DIR=\"$MNT_DIR\"\n\n# マウントバイナリを実行\n# (実際のマウントロジックは Rust バイナリにあります)\n\"$MODDIR/meta-overlayfs\"\n```\n\n#### 主な機能\n\n**Overlayfs マウント:**\n\n- 真のシステムレス変更のためのカーネル overlayfs を使用\n- 複数のパーティション(system、vendor、product、system_ext、odm、oem)をサポート\n- `/data/adb/modules/.rw/` を介した読み書きレイヤーサポート\n\n**ソース識別:**\n\n```rust\n// meta-overlayfs/src/mount.rs から\nfsconfig_set_string(fs, \"source\", \"KSU\")?;  // 必須!\n```\n\nこれにより、すべてのオーバーレイマウントに対して `dev=KSU` が設定され、適切な識別が可能になります。\n\n### ベストプラクティス\n\nメタモジュールを開発する際:\n\n1. **マウント操作には常にソースを \"KSU\" に設定する** - カーネルアンマウントと zygisksu アンマウントが正しくアンマウントするためにこれが必要です\n2. **エラーを適切に処理する** - 起動プロセスは時間に敏感です\n3. **標準フラグを尊重する** - `skip_mount` と `disable` をサポートします\n4. **操作をログに記録する** - デバッグには `echo` またはロギングを使用します\n5. **徹底的にテストする** - マウントエラーは起動ループを引き起こす可能性があります\n6. **動作を文書化する** - メタモジュールが何をするかを明確に説明します\n7. **移行パスを提供する** - ユーザーが他のソリューションから切り替えるのを支援します\n\n### メタモジュールのテスト\n\nリリース前に:\n\n1. クリーンな KernelSU セットアップで**インストールをテスト**する\n2. さまざまなモジュールタイプで**マウントを検証**する\n3. 一般的なモジュールとの**互換性をチェック**する\n4. **アンインストールとクリーンアップをテスト**する\n5. **起動パフォーマンスを検証**する(metamount.sh はブロッキングです!)\n6. 起動ループを避けるために**適切なエラー処理を確保**する\n\n## よくある質問\n\n### メタモジュールは必要ですか?\n\n**ユーザー向け**: マウントが必要なモジュールを使用したい場合のみ。スクリプトを実行するだけでシステムファイルを変更しないモジュールのみを使用する場合は、メタモジュールは必要ありません。\n\n**モジュール開発者向け**: いいえ、通常どおりモジュールを開発します。モジュールがマウントを必要とする場合にのみ、ユーザーはメタモジュールが必要です。\n\n**上級ユーザー向け**: マウント動作をカスタマイズしたい場合、または代替マウント実装を作成したい場合のみ。\n\n### 複数のメタモジュールを持つことはできますか?\n\nいいえ。一度に1つのメタモジュールのみインストールできます。これにより、競合が防止され、予測可能な動作が保証されます。\n\n### 唯一のメタモジュールをアンインストールするとどうなりますか?\n\nモジュールはマウントされなくなります。デバイスは正常に起動しますが、別のメタモジュールをインストールするまで、モジュールの変更は適用されません。\n\n### meta-overlayfs は必須ですか?\n\nいいえ。ほとんどのモジュールと互換性のある標準の overlayfs マウントを提供します。異なる動作が必要な場合は、独自のメタモジュールを作成できます。\n\n## 関連項目\n\n- [モジュールガイド](module.md) - 一般的なモジュール開発\n- [Magisk との違い](difference-with-magisk.md) - KernelSU と Magisk の比較\n- [ビルド方法](how-to-build.md) - ソースから KernelSU をビルド\n"
  },
  {
    "path": "website/docs/ja_JP/guide/module-config.md",
    "content": "# モジュール設定\n\nKernelSU は、モジュールが永続的または一時的なキー値設定を保存できる組み込みの設定システムを提供します。設定は `/data/adb/ksu/module_configs/<module_id>/` にバイナリ形式で保存され、以下の特性があります:\n\n## 設定タイプ\n\n- **永続設定** (`persist.config`):再起動後も保持され、明示的に削除またはモジュールをアンインストールするまで残ります\n- **一時設定** (`tmp.config`):起動時の post-fs-data ステージで自動的にクリアされます\n\n設定を読み取るとき、同じキーに対して一時値が永続値より優先されます。\n\n## モジュールスクリプトでの設定の使用\n\nすべてのモジュールスクリプト(`post-fs-data.sh`、`service.sh`、`boot-completed.sh` など)は、`KSU_MODULE` 環境変数がモジュール ID に設定された状態で実行されます。`ksud module config` コマンドを使用してモジュールの設定を管理できます:\n\n```bash\n# 設定値を取得\nvalue=$(ksud module config get my_setting)\n\n# 永続設定値を設定\nksud module config set my_setting \"some value\"\n\n# 一時設定値を設定(再起動後にクリア)\nksud module config set --temp runtime_state \"active\"\n\n# stdinから値を設定(複数行や複雑なデータに便利)\nksud module config set my_key <<EOF\n複数行\nテキスト値\nEOF\n\n# またはコマンドからパイプ\necho \"value\" | ksud module config set my_key\n\n# 明示的なstdinフラグ\ncat file.json | ksud module config set json_data --stdin\n\n# すべての設定エントリを一覧表示(永続と一時をマージ)\nksud module config list\n\n# 設定エントリを削除\nksud module config delete my_setting\n\n# 一時設定エントリを削除\nksud module config delete --temp runtime_state\n\n# すべての永続設定をクリア\nksud module config clear\n\n# すべての一時設定をクリア\nksud module config clear --temp\n```\n\n## 検証制限\n\n設定システムは以下の制限を強制します:\n\n- **最大キー長**:256 バイト\n- **最大値長**:1MB (1048576 バイト)\n- **最大設定エントリ数**:モジュールあたり 32 個\n- **キー形式**:`^[a-zA-Z][a-zA-Z0-9._-]+$` にマッチする必要があります(モジュールIDと同じ)\n  - 文字(a-zA-Z)で開始する必要があります\n  - 文字、数字、ドット(`.`)、アンダースコア(`_`)、ハイフン(`-`)を含めることができます\n  - 最小長:2文字\n- **値形式**:制限なし - 改行、制御文字などを含むあらゆるUTF-8文字を含めることができます\n  - 長さプレフィックス付きのバイナリ形式で保存され、すべてのデータの安全な処理を保証\n\n## ライフサイクル\n\n- **起動時**:すべての一時設定が post-fs-data ステージでクリアされます\n- **モジュールアンインストール時**:すべての設定(永続と一時)が自動的に削除されます\n- 設定はバイナリ形式で保存され、マジックナンバー `0x4b53554d`(\"KSUM\")とバージョン検証を使用します\n\n## ユースケース\n\n設定システムは以下に最適です:\n\n- **ユーザー設定**:WebUI または action スクリプトを通じてユーザーが設定したモジュール設定を保存\n- **機能フラグ**:再インストールせずにモジュール機能を有効/無効にする\n- **ランタイム状態**:再起動時にリセットすべき一時的な状態を追跡(一時設定を使用)\n- **インストール設定**:モジュールインストール時に行った選択を記憶\n- **複雑なデータ**:JSON、複数行テキスト、Base64エンコードデータ、または任意の構造化コンテンツを保存(最大1MB)\n\n::: tip ベストプラクティス\n- 再起動後も保持すべきユーザー設定には永続設定を使用\n- 起動時にリセットすべきランタイム状態や機能フラグには一時設定を使用\n- スクリプトで設定値を使用する前に検証する\n- 設定の問題をデバッグするには `ksud module config list` コマンドを使用\n:::\n\n## 高級機能\n\nモジュール設定システムは、高度なユースケースのための特別な設定キーを提供します:\n\n### モジュール説明のオーバーライド {#overriding-module-description}\n\n`override.description` 設定キーを設定することで、`module.prop` の `description` フィールドを動的にオーバーライドできます:\n\n```bash\n# モジュール説明をオーバーライド\nksud module config set override.description \"マネージャーに表示されるカスタム説明\"\n```\n\nモジュールリストを取得する際、`override.description` 設定が存在する場合、`module.prop` の元の説明が置き換えられます。これは以下の場合に便利です:\n- モジュール説明に動的なステータス情報を表示\n- ユーザーにランタイム設定の詳細を表示\n- 再インストールせずにモジュールの状態に基づいて説明を更新\n\n### 管理対象機能の宣言\n\nモジュールは `manage.<feature>` 設定パターンを使用して、管理する KernelSU 機能を宣言できます。サポートされている機能は、KernelSU 内部の `FeatureId` 列挙型に対応しています:\n\n**サポートされている機能:**\n- `su_compat` - SU 互換モード\n- `kernel_umount` - カーネル自動アンマウント\n\n```bash\n# このモジュールが SU 互換性を管理し、有効にすることを宣言\nksud module config set manage.su_compat true\n\n# このモジュールがカーネルアンマウントを管理し、無効にすることを宣言\nksud module config set manage.kernel_umount false\n\n# 機能管理を削除(モジュールはこの機能を制御しなくなります)\nksud module config delete manage.su_compat\n```\n\n**動作原理:**\n- `manage.<feature>` キーの存在は、モジュールがその機能を管理していることを示します\n- 値は希望する状態を示します:`true`/`1` は有効、`false`/`0`(またはその他の値)は無効\n- 機能の管理を停止するには、設定キーを完全に削除します\n\n管理対象機能は、モジュールリスト API を通じて `managedFeatures` フィールド(カンマ区切りの文字列)として公開されます。これにより以下が可能になります:\n- KernelSU マネージャーがどのモジュールがどの KernelSU 機能を管理しているかを検出\n- 複数のモジュールが同じ機能を管理しようとした際の競合を防止\n- モジュールとコア KernelSU 機能間のより良い調整\n\n::: warning サポートされている機能のみ\n上記にリストされた事前定義された機能名(`su_compat`、`kernel_umount`)のみを使用してください。これらは実際の KernelSU 内部機能に対応しています。他の機能名を使用してもエラーにはなりませんが、機能的な目的はありません。\n:::\n"
  },
  {
    "path": "website/docs/ja_JP/guide/module-webui.md",
    "content": "# Module WebUI\n\nKernelSU のモジュールは、ブートスクリプトの実行やシステムファイルの修正に加えて、UI インターフェースの表示やユーザーとの対話もサポートしています。\n\nモジュールは、任意の Web 技術を通じて HTML + CSS + JavaScript のページを作成することができます。KernelSU のマネージャーは WebView を通じてこれらのページを表示します。また、シェルコマンドの実行など、システムと対話するためのいくつかのAPIを提供しています。\n\n## `webroot` ディレクトリ\n\nWeb リソースファイルは、モジュールのルートディレクトリの `webroot` サブディレクトリに置かれるべきであり、`index.html` という名前のファイルが必ず存在しなければなりません。これがモジュールページのエントリです。Web インターフェイスを含む最もシンプルなモジュール構造は以下の通りです：\n\n```txt\n❯ tree .\n.\n|-- module.prop\n`-- webroot\n    `-- index.html\n```\n\n:::警告\nモジュールをインストールするとき、KernelSU はこのディレクトリのパーミッションと SELinux コンテキストを自動的に設定します。何をしているかわからないのであれば、自分でこのディレクトリのパーミッションを設定しないでください！\n:::\n\nページに css や JavaScript が含まれている場合は、このディレクトリに配置する必要があります。\n\n## JavaScript API\n\n単なる表示ページであれば、通常の Web ページとの違いはありません。より重要なのは、KernelSU がモジュールの固有機能を実装させるための一連のシステム API を提供することです。\n\nKernelSU は JavaScript ライブラリを提供し、[npm で公開しています](https://www.npmjs.com/package/kernelsu)。これを Web ページの JavaScript コードで使用することができます。\n\nたとえば、特定の設定を取得したり、プロパティを変更するために、シェルコマンドを実行することができます：\n\n```JavaScript\nimport { exec } from 'kernelsu';\n\nconst { errno, stdout } = exec(\"getprop ro.product.model\");\n```\n\n別の例として、Webページをフルスクリーンで表示したり、トーストを表示することができます。\n\n[API ドキュメント](https://www.npmjs.com/package/kernelsu)\n\n既存のAPIがご自身のニーズを満たしていない、または使い勝手が不便である場合、[こちら](https://github.com/tiann/KernelSU/issues)でご提案いただければ幸いです！\n\n## いくつかのヒント\n\n1. `localStorage` を通常通りに使用してデータを保存することができますが、Manager アプリをアンインストールした後には失われます。永続的に保存する必要がある場合は、自分でいくつかのディレクトリにデータを書き込むことができます。\n2. シンプルなページには、[parceljs](https://parceljs.org/)を使用することをお勧めします。設定が不要で非常に便利です。しかし、フロントエンドの達人である場合や、自分の好みがある場合は、気に入ったものを選んでください！\n"
  },
  {
    "path": "website/docs/ja_JP/guide/module.md",
    "content": "# モジュールのガイド\n\nKernelSU はシステムパーティションの整合性を維持しながら、システムディレクトリを変更する効果を実現するモジュール機構を提供します。この機構は一般に「システムレス」と呼ばれています。\n\nKernelSU のモジュール機構は、Magisk とほぼ同じです。Magisk のモジュール開発に慣れている方であれば、KernelSU のモジュール開発も簡単でしょう。その場合は以下のモジュールの紹介は読み飛ばして、[Magisk との違い](difference-with-magisk.md)の内容だけ読めばOKです。\n\n::: warning METAMODULE はシステムファイル変更時のみ必要\nKernelSU は [metamodule](metamodule.md) アーキテクチャを使用して `system` ディレクトリをマウントします。**モジュールが `/system` ファイルを変更する必要がある場合のみ**（`system` ディレクトリ経由で）、metamodule ([meta-overlayfs](https://github.com/tiann/KernelSU/releases)など) をインストールする必要があります。スクリプト、sepolicy ルール、system.propなどの他のモジュール機能は metamodule なしで動作します。\n:::\n\n## WebUI\n\nKernelSU modules support displaying interfaces and interacting with users. See the [WebUI documentation](module-webui.md) for more information.\n\n## モジュール設定\n\nKernelSU は、モジュールが永続的または一時的なキー値設定を保存できる組み込みの設定システムを提供します。詳細については、[モジュール設定ドキュメント](module-config.md)を参照してください。\n\n## Busybox\n\nKernelSU には、機能的に完全な Busybox バイナリ (SELinux の完全サポートを含む) が同梱されています。実行ファイルは `/data/adb/ksu/bin/busybox` に配置されています。KernelSU の Busybox はランタイムに切り替え可能な「ASH スタンドアローンシェルモード」をサポートしています。このスタンドアロンモードとは、Busybox の `ash` シェルで実行する場合 `PATH` として設定されているものに関係なく、すべてのコマンドが Busybox 内のアプレットを直接使用するというものです。たとえば、`ls`、`rm`、`chmod` などのコマンドは、`PATH` にあるもの（Android の場合、デフォルトではそれぞれ `/system/bin/ls`, `/system/bin/rm`, `/system/bin/chmod`）ではなく、直接 Busybox 内部のアプレットを呼び出すことになります。これにより、スクリプトは常に予測可能な環境で実行され、どの Android バージョンで実行されていても常にコマンドを利用できます。Busybox を使用しないコマンドを強制的に実行するには、フルパスで実行ファイルを呼び出す必要があります。\n\nKernelSU のコンテキストで実行されるすべてのシェルスクリプトは、Busybox の `ash` シェルでスタンドアロンモードが有効な状態で実行されます。サードパーティの開発者に関係するものとしては、すべてのブートスクリプトとモジュールのインストールスクリプトが含まれます。\n\nこの「スタンドアロンモード」機能を KernelSU 以外で使用したい場合、2つの方法で有効にできます：\n\n1. 環境変数 `ASH_STANDALONE` を `1` にする<br>例: `ASH_STANDALONE=1 /data/adb/ksu/bin/busybox sh <script>`\n2. コマンドラインのオプションで変更する:<br>`/data/adb/ksu/bin/busybox sh -o standalone <script>`\n\n環境変数が子プロセスに継承されるため、その後に実行されるすべての `sh` シェルもスタンドアロンモードで実行されるようにするにはオプション 1 が望ましい方法です（KernelSU と KernelSU Managerが内部的に使用しているのもこちらです）。\n\n::: tip Magisk との違い\n\nKernelSU の Busybox は、Magisk プロジェクトから直接コンパイルされたバイナリファイルを使用するようになりました。Magisk と KernelSU の Busybox スクリプトはまったく同じものなので、互換性の問題を心配する必要はありません！\n:::\n\n## KernelSU モジュール\n\nKernelSU モジュールは、`/data/adb/modules` に配置された以下の構造を持つフォルダーです：\n\n```txt\n/data/adb/modules\n├── .\n├── .\n|\n├── $MODID                  <--- フォルダの名前はモジュールの ID で付けます\n│   │\n│   │      *** モジュールの ID ***\n│   │\n│   ├── module.prop         <--- このファイルにモジュールのメタデータを保存します\n│   │\n│   │      *** メインコンテンツ ***\n│   │\n│   ├── system              <--- skip_mount が存在しない場合、このフォルダがマウントされます\n│   │   ├── ...\n│   │   ├── ...\n│   │   └── ...\n│   │\n│   │      *** ステータスフラグ ***\n│   │\n│   ├── skip_mount          <--- 存在する場合、KernelSU はシステムフォルダをマウントしません\n│   ├── disable             <--- 存在する場合、モジュールは無効化されます\n│   ├── remove              <--- 存在する場合、次の再起動時にモジュールが削除されます\n│   │\n│   │      *** 任意のファイル ***\n│   │\n│   ├── post-fs-data.sh     <--- このスクリプトは post-fs-data で実行されます\n│   ├── service.sh          <--- このスクリプトは late_start サービスで実行されます\n|   ├── uninstall.sh        <--- このスクリプトは KernelSU がモジュールを削除するときに実行されます\n│   ├── system.prop         <--- このファイルのプロパティは resetprop によってシステムプロパティとして読み込まれます\n│   ├── sepolicy.rule       <--- カスタム SEPolicy ルールを追加します\n│   │\n│   │      *** 自動生成されるため、手動で作成または変更しないでください ***\n│   │\n│   ├── vendor              <--- $MODID/system/vendor へのシンボリックリンク\n│   ├── product             <--- $MODID/system/product へのシンボリックリンク\n│   ├── system_ext          <--- $MODID/system/system_ext へのシンボリックリンク\n│   │\n│   │      *** その他のファイル/フォルダの追加も可能です ***\n│   │\n│   ├── ...\n│   └── ...\n|\n├── another_module\n│   ├── .\n│   └── .\n├── .\n├── .\n```\n\n::: tip Magisk との違い\nKernelSU は Zygisk をビルトインでサポートしていないため、モジュール内に Zygisk に関連するコンテンツは存在しません。 しかし、[ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) をインストールすれば Zygisk モジュールを使えます。その場合の Zygisk モジュールのコンテンツは Magisk と同じです。\n:::\n\n### module.prop\n\nmodule.prop はモジュールの設定ファイルです。KernelSU ではこのファイルを含まないモジュールは、モジュールとして認識されません。このファイルの形式は以下の通りです：\n\n```txt\nid=<string>\nname=<string>\nversion=<string>\nversionCode=<int>\nauthor=<string>\ndescription=<string>\nupdateJson=<url> (optional)\nactionIcon=<path> (optional)\nwebuiIcon=<path> (optional)\n```\n\n- `id` はこの正規表現に一致していなければいけません: `^[a-zA-Z][a-zA-Z0-9._-]+$`<br>\n  例: ✓ `a_module`, ✓ `a.module`, ✓ `module-101`, ✗ `a module`, ✗ `1_module`, ✗ `-a-module`<br>\n  これはモジュールの**ユニークな ID** です。公開後は変更しないでください。\n- `versionCode` は **integer** です。バージョンの比較に使います。\n- 他のものには**単一行** の文字であれば何でも使えます。\n- 改行文字は `UNIX (LF)` を使ってください。`Windows (CR+LF)` や `Macintosh (CR)` は使ってはいけません。\n- `actionIcon` と `webuiIcon` は、マネージャーアプリ内に表示される**モジュールアクションのショートカット**および**WebUI ショートカット**の既定アイコンとして使用される、任意指定の画像パスです。これらのパスは、モジュール ディレクトリを基準として指定する必要があります。例えば、`actionIcon=icon/icon.png` は `<MODDIR>/icon/icon.png` を指します。\n\n::: tip 動的説明\n`description` フィールドは、モジュール設定システムを使用して実行時に動的にオーバーライドできます。詳細は[モジュール説明のオーバーライド](module-config.md#overriding-module-description)を参照してください。\n:::\n\n### シェルスクリプト\n\n`post-fs-data.sh` と `service.sh` の違いについては、[ブートスクリプト](#boot-scripts)のセクションを読んでください。ほとんどのモジュール開発者にとって、ブートスクリプトを実行するだけなら `service.sh` で十分なはずです。\n\nモジュールのすべてのスクリプトでは、`MODDIR=${0%/*}`を使えばモジュールのベースディレクトリのパスを取得できます。スクリプト内でモジュールのパスをハードコードしないでください。\n\n::: tip Magisk との違い\n環境変数 `KSU` を使用すると、スクリプトが KernelSU と Magisk どちらで実行されているかを判断できます。KernelSU で実行されている場合、この値は `true` に設定されます。\n:::\n\n### `system` ディレクトリ\n\nこのディレクトリの内容は、システムの起動後に OverlayFS を使用してシステムの /system パーティションの上にオーバーレイされます：\n\n1. システム内の対応するディレクトリにあるファイルと同名のファイルは、このディレクトリにあるファイルで上書きされます。\n2. システム内の対応するディレクトリにあるフォルダと同じ名前のフォルダは、このディレクトリにあるフォルダと統合されます。\n\n元のシステムディレクトリにあるファイルやフォルダを削除したい場合は、`mknod filename c 0 0` を使ってモジュールディレクトリにそのファイル/フォルダと同じ名前のファイルを作成する必要があります。こうすることで、OverlayFS システムはこのファイルを削除したかのように自動的に「ホワイトアウト」します（/system パーティションは実際には変更されません）。\n\nまた、`customize.sh` 内で `REMOVE` という変数に削除操作を実行するディレクトリのリストを宣言すると、KernelSU は自動的にそのモジュールの対応するディレクトリで `mknod <TARGET> c 0 0` を実行します。例えば\n\n```sh\nREMOVE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n```\n\n上記の場合は、`mknod $MODPATH/system/app/YouTuBe c 0 0`と`mknod $MODPATH/system/app/Bloatware c 0 0`を実行し、`/system/app/YouTube`と`/system/app/Bloatware`はモジュール有効化後に削除されます。\n\nシステム内のディレクトリを置き換えたい場合は、モジュールディレクトリに同じパスのディレクトリを作成し、このディレクトリに `setfattr -n trusted.overlay.opaque -v y <TARGET>` という属性を設定する必要があります。こうすることで、OverlayFS システムは（/system パーティションを変更することなく）システム内の対応するディレクトリを自動的に置き換えることができます。\n\n`customize.sh` ファイル内に `REPLACE` という変数を宣言し、その中に置換するディレクトリのリストを入れておけば、KernelSU は自動的にモジュールディレクトリに対応した処理を行います。例えば：\n\nREPLACE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n\nこのリストは、自動的に `$MODPATH/system/app/YouTube` と `$MODPATH/system/app/Bloatware` というディレクトリを作成し、 `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/YouTube` と `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/Bloatware` を実行します。モジュールが有効になると、`/system/app/YouTube` と `/system/app/Bloatware` は空のディレクトリに置き換えられます。\n\n::: tip Magisk との違い\n\nKernelSU のシステムレスメカニズムはカーネルの OverlayFS によって実装され、Magisk は現在マジックマウント（bind mount）を使用しています。この2つの実装方法には大きな違いがありますが最終的な目的は同じで、/system パーティションを物理的に変更することなく、/system のファイルを変更できます。\n:::\n\nOverlayFS に興味があれば、Linux カーネルの [OverlayFS のドキュメンテーション](https://docs.kernel.org/filesystems/overlayfs.html) を読んでみてください。\n\n### system.prop\n\nこのファイルは `build.prop` と同じ形式をとっています。各行は `[key]=[value]` で構成されます。\n\n### sepolicy.rule\n\nもしあなたのモジュールが追加の SEPolicy パッチを必要とする場合は、それらのルールをこのファイルに追加してください。このファイルの各行は、ポリシーステートメントとして扱われます。\n\n## モジュールのインストーラー\n\nKernelSU モジュールインストーラーは、KernelSU Manager アプリでインストールできる、ZIP ファイルにパッケージされた KernelSU モジュールです。最もシンプルな KernelSU モジュールインストーラーは、KernelSU モジュールを ZIP ファイルとしてパックしただけのものです。\n\n```txt\nmodule.zip\n│\n├── customize.sh                       <--- (任意、詳細は後述)\n│                                           このスクリプトは update-binary から読み込まれます\n├── ...\n├── ...  /* 残りのモジュールのファイル */\n│\n```\n\n::: warning 警告\nKernelSU モジュールは、カスタムリカバリーからのインストールには非対応です！\n:::\n\n### カスタマイズ\n\nモジュールのインストールプロセスをカスタマイズする必要がある場合、`customize.sh` という名前のスクリプトを作成してください。このスクリプトは、すべてのファイルが抽出され、デフォルトのパーミッションと secontext が適用された後、モジュールインストーラースクリプトによって読み込み (実行ではなく) されます。これは、モジュールがデバイスの ABI に基づいて追加設定を必要とする場合や、モジュールファイルの一部に特別なパーミッション/コンテキストを設定する必要がある場合に、非常に便利です。\n\nインストールプロセスを完全に制御しカスタマイズしたい場合は、`customize.sh` で `SKIPUNZIP=1` と宣言すればデフォルトのインストールステップをすべてスキップできます。そうすることで、`customize.sh` が責任をもってすべてをインストールするようになります。\n\n`customize.sh`スクリプトは、KernelSU の Busybox `ash` シェルで、「スタンドアロンモード」を有効にして実行します。以下の変数と関数が利用可能です：\n\n#### 変数\n\n- `KSU` (bool): スクリプトが KernelSU 環境で実行されていることを示すための変数で、この変数の値は常に true になります。KernelSU と Magisk を区別するために使用できます。\n- `KSU_VER` (string): 現在インストールされている KernelSU のバージョン文字列 (例: `v0.4.0`)\n- `KSU_VER_CODE` (int): ユーザー空間に現在インストールされているKernelSUのバージョンコード (例: `10672`)\n- `KSU_KERNEL_VER_CODE` (int): 現在インストールされている KernelSU のカーネル空間でのバージョンコード（例：`10672`）\n- `BOOTMODE` (bool): KernelSU では常に `true` \n- `MODPATH` (path): モジュールファイルがインストールされるパス\n- `TMPDIR` (path): ファイルを一時的に保存しておく場所\n- `ZIPFILE` (path): あなたのモジュールのインストールZIP\n- `ARCH` (string): デバイスの CPU アーキテクチャ。値は `arm`、`arm64`、`x86`、`x64` のいずれか\n- `IS64BIT` (bool): `ARCH` が `arm64` または `x64` のときは `true` \n- `API` (int): 端末の API レベル・Android のバージョン（例：Android 6.0 なら`23`）\n\n::: warning 警告\nKernelSU では、MAGISK_VER_CODE は常に25200、MAGISK_VER は常にv25.2です。この2つの変数で KernelSU 上で動作しているかどうかを判断するのはやめてください。\n:::\n\n#### 機能\n\n```txt\nui_print <msg>\n    コンソールに <msg> を表示します\n    カスタムリカバリーのコンソールでは表示されないため、「echo」の使用は避けてください\n\nabort <msg>\n    エラーメッセージ<msg>をコンソールに出力し、インストールを終了させます\n    終了時のクリーンアップがスキップされてしまうため、「exit」の使用は避けてください\n\nset_perm <target> <owner> <group> <permission> [context]\n    [context] が設定されていない場合、デフォルトは \"u:object_r:system_file:s0\" です。\n    この機能は、次のコマンドの略記です：\n       chown owner.group target\n       chmod permission target\n       chcon context target\n\nset_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]\n    [context] が設定されていない場合、デフォルトは \"u:object_r:system_file:s0\" です。\n    <directory> 内のすべてのファイルに対しては以下が実行されます:\n       set_perm file owner group filepermission context\n    <directory> 内のすべてのディレクトリ（自身を含む）に対しては以下が実行されます:\n       set_perm dir owner group dirpermission context\n```\n\n## ブートスクリプト\n\nKernelSU では、スクリプトは実行モードによって post-fs-data モードと late_start サービスモードの2種類に分けられます：\n\n- post-fs-data モード\n  - 同期処理です。実行が終わるか、10秒が経過するまでブートプロセスが一時停止されます。\n  - スクリプトはモジュールがマウントされる前に実行されます。モジュール開発者はモジュールがマウントされる前に、動的にモジュールを調整できます。\n  - このステージは Zygote が始まる前に起こるので、Android のほぼすべての処理の前に割り込めます\n  - **警告:** `setprop` を使うとブートプロセスのデッドロックを引き起こします! `resetprop -n <prop_name> <prop_value>` を使ってください。\n  - **本当に必要な場合だけこのモードでコマンド実行してください**\n- late_start サービスモード\n  - 非同期処理です。スクリプトは、起動プロセスの残りの部分と並行して実行されます。\n  - **ほとんどのスクリプトにはこちらがおすすめです**\n\nKernelSU では、起動スクリプトは保存場所によって一般スクリプトとモジュールスクリプトの2種類に分けられます：\n\n- 一般スクリプト\n  - `/data/adb/post-fs-data.d` か `/data/adb/service.d` に配置されます\n  - スクリプトが実行可能な状態に設定されている場合にのみ実行されます (`chmod +x script.sh`)\n  - `post-fs-data.d` のスクリプトは post-fs-data モードで実行され、`service.d` のスクリプトは late_start サービスモードで実行されます\n  - モジュールはインストール時に一般スクリプトを追加するべきではありません\n- モジュールスクリプト\n  - モジュール独自のフォルダに配置されます\n  - モジュールが有効な場合のみ実行されます\n  - `post-fs-data.sh` は post-fs-data モードで実行され、`service.sh` は late_start サービスモードで実行されます\n\nすべてのブートスクリプトは、KernelSU の Busybox `ash` シェルで「スタンドアロンモード」を有効にした状態で実行されます。\n\n## Late-load モード {#late-load-mode}\n\n上記の標準起動フローに加えて、KernelSU は LKM（ローダブルカーネルモジュール）シナリオ向けの **late-load モード** をサポートしています。このモードでは、KernelSU カーネルモジュールは init プロセス中ではなく、**システムが完全に起動した後** にロードされます。\n\n### late-load はいつ発生するか？\n\n`ksud late-load` コマンドを実行することでトリガーされます。このコマンドは：\n\n1. 現在の KMI バージョンを検出し、組み込みアセットから対応する `kernelsu.ko` をロードします。\n2. 通常起動時に行われるモジュールの初期化（SELinux ルール、許可リスト、機能など）を実行します。\n\nシステムが既に完全に稼働しているため、起動時の特定のメカニズムは利用不可または不要です。\n\n### 標準起動との違い\n\n| 動作 | 標準起動 | Late-load モード |\n|------|:---:|:---:|\n| カーネルモジュールが init (PID 1) によりロード | はい | いいえ（起動後にロード） |\n| ksud の kprobe フック (execve/read/fstat/input) | はい | スキップ |\n| セーフモード検出（音量キー） | はい | 常に無効 |\n| 起動ログのキャプチャ (logcat/dmesg) | はい | スキップ |\n| Magisk 共存チェック | はい | スキップ |\n| `post-fs-data` イベントのカーネル通知 | はい | スキップ |\n| `boot-completed` イベントのカーネル通知 | はい | 初期化時に直接設定 |\n| `post-fs-data.sh` / `post-fs-data.d/` スクリプト | はい | `late-load` ステージで代替 |\n| `system.prop` の読み込み | はい | はい |\n| OverlayFS マウント（metamodule） | はい | はい |\n| `post-mount.sh` / `post-mount.d/` スクリプト | はい | はい |\n| `service.sh` / `service.d/` スクリプト | はい | はい |\n| `boot-completed.sh` / `boot-completed.d/` スクリプト | はい | はい |\n| 環境変数 `KSU_LATE_LOAD` | 未設定 | `1` に設定 |\n| カーネル info フラグ `0x4` | 未設定 | 設定済み |\n\n### スクリプト実行順序\n\nlate-load モードでのスクリプト実行順序は以下の通りです：\n\n```txt\nksud late-load:\n  1. kernelsu.ko をロード（まだロードされていない場合）\n  2. バイナリの展開、モジュール更新の処理、SELinux ルールの読み込み、機能の初期化\n  3. late-load.d/ の汎用スクリプトとモジュールの late-load スクリプトを実行（ブロッキング）\n  4. system.prop を読み込み (resetprop -n)\n  5. metamodule マウントスクリプトを実行（OverlayFS マウント）\n  6. post-mount.d/ の汎用スクリプトとモジュールの post-mount.sh を実行（ブロッキング）\n  7. service.d/ の汎用スクリプトとモジュールの service.sh を実行（ノンブロッキング）\n  8. boot-completed.d/ の汎用スクリプトとモジュールの boot-completed.sh を実行（ノンブロッキング）\n```\n\n### Late-load 専用スクリプト\n\nモジュールは `late-load.sh` スクリプトを提供でき、このスクリプトは **late-load モードでのみ** 実行され、`post-fs-data.sh` の代替として機能します。このスクリプトは OverlayFS マウント前に実行され、標準フローの `post-fs-data.sh` と同様のタイミングです。\n\nまた、汎用スクリプトを `/data/adb/late-load.d/` に配置して、このステージで実行することもできます。\n\n### スクリプト内で late-load モードを検出する\n\nモジュールは環境変数 `KSU_LATE_LOAD` をチェックすることで late-load モードかどうかを検出できます：\n\n```sh\nif [ \"$KSU_LATE_LOAD\" = \"1\" ]; then\n    # late-load モードで実行中\n    echo \"Late-load mode detected\"\nfi\n```\n\nこれにより、モジュールは自身の動作を調整できます。例えば、早期起動時にのみ必要な操作をスキップできます。\n"
  },
  {
    "path": "website/docs/ja_JP/guide/rescue-from-bootloop.md",
    "content": "# ブートループからの復旧\n\nデバイスに書き込む際、デバイスが「文鎮化」状態になる場面に遭遇することがあります。理論的には、fastboot で boot パーティションを書き込んだだけだったり、不適切なモジュールをインストールしてデバイスが起動しなくなったりした場合なら、適切な操作で復旧できます。このページでは、「文鎮化」状態になったデバイスを復旧させるための緊急手段を紹介します。\n\n## boot パーティションの書き込みによる文鎮化\n\nKernelSU では、以下のような状況で boot パーティションを書き込んだときに文鎮化する場合があります：\n\n1. 間違った形式の boot イメージを書き込んでしまった場合。例えばお使いのデバイスのパーティション形式が `gz` なのに `lz4` 形式のイメージを書き込んでしまうと、起動しなくなります。\n2. お使いのデバイスが起動するために AVB 検証を無効にする必要がある場合（通常、無効にするにはすべてのデータを消去する必要があります）。\n3. カーネルにバグがある、または書き込みに適していない場合。\n\nどのような状況であっても、**純正の boot イメージを書き込む**ことで復旧できます。したがって、インストールする前にまずは純正の boot パーティションをバックアップすることを強くおすすめします。バックアップしていない場合は、あなたと同じデバイスを持つ他のユーザー、または公式ファームウェアから純正の boot イメージを入手できます。\n\n## モジュールによる文鎮化\n\nモジュールのインストールはデバイスを文鎮化させる一般的な原因です。**モジュールを未知のソースからインストールしないでください**！モジュールは root 権限を持つため、あなたのデバイスに不可逆的なダメージを与える可能性があります！\n\n### 通常のモジュール\n\n安全であることが確認されているモジュールをインストールしてデバイスが起動しなくなった場合、KernelSU では心配することなく簡単に復旧できます。KernelSU には、以下のようなデバイスを救出するための仕組みが組み込まれています：\n\n1. AB アップデート\n2. 音量下ボタンでの復旧\n\n#### AB アップデート\n\nKernelSU のモジュール更新は、OTA アップデートで使用される Android システムの AB アップデート機構からヒントを得ています。新しいモジュールをインストールしたり、既存のモジュールを更新したりする場合、現在使用されているモジュールファイルを直接変更することはありません。代わりに、すべてのモジュールが別のアップデートイメージに組み込まれます。システムが再起動された後、このアップデートイメージの使用を開始しようとします。Android システムが正常に起動した場合、モジュールは本当に更新されます。\n\nそのため、デバイスを復旧する最もシンプルで一般的な方法は、**強制的に再起動すること**です。モジュールをインストールした後にシステムを起動できなくなった場合、電源ボタンを10秒以上長押しするとシステムが自動的に再起動します。再起動後はモジュールを更新する前の状態にロールバックされ、以前に更新したモジュールは自動的に無効化されます。\n\n#### 音量下ボタンでの復旧\n\nAB アップデートでも解決しない場合は、**セーフモード**を使用してみてください。セーフモードでは、すべてのモジュールが無効化されます。\n\nセーフモードに入るには、2つの方法があります：\n\n1. 一部のシステムの内蔵セーフモード：音量下ボタンの長押しでセーフモードに入れるシステムもあれば、リカバリーでセーフモードに入れるシステム（MIUI など）もあります。システムのセーフモードに入ると KernelSU もセーフモードに入り、自動的にモジュールを無効化します。\n2. KernelSU の内蔵セーフモード：最初の起動画面の後、**音量下キーを3回以上連続して押す**と入れます。なお、押す→離すを三回繰り返すのであって、長押しではありません。\n\nセーフモードに入ると、KernelSU Manager のモジュールページにあるすべてのモジュールが無効になります。「アンインストール」操作を行うことで、問題を起こしている可能性のあるモジュールをアンインストールできます。\n\n内蔵のセーフモードはカーネルに実装されているため、キーイベントを見逃す可能性はありません。ただし、GKI 以外のカーネルでは手動によるコードの統合が必要な場合があるため、公式ドキュメントを参考にしてください。\n\n### 悪意のあるモジュール\n\n上記の方法でデバイスを救出できない場合、インストールしたモジュールが悪意のある操作をしているか、他の手段でデバイスを損傷している可能性が高いです。この場合、2つの方法しかありません：\n\n1. データを消去して純正システムをインストールし直す\n2. アフターセールスサービスに問い合わせする\n"
  },
  {
    "path": "website/docs/ja_JP/guide/unofficially-support-devices.md",
    "content": "# 非公式の対応デバイス\n\n::: warning 警告\nこのドキュメントはアーカイブ参照のみを目的としており、更新されなくなりました。\nKernelSU v1.0以降、非GKIデバイスの公式サポートを終了しました。\n:::\n\n::: warning 警告\nこのページでは他の開発者が管理している、KernelSU をサポートする GKI 以外のデバイス用のカーネルを紹介しています。\n:::\n\n::: warning 警告\nこのページはあなたのデバイスに対応するソースコードを見つけるためのものであり、そのソースコードが _KernelSU 開発者_ によってレビューされたことを意味するものではありません。ご自身の責任においてご利用ください。\n:::\n\n<script setup>\nimport data from '../../repos.json'\n</script>\n\n<table>\n   <thead>\n      <tr>\n         <th>メンテナー</th>\n         <th>リポジトリ</th>\n         <th>対応デバイス</th>\n      </tr>\n   </thead>\n   <tbody>\n    <tr v-for=\"repo in data\" :key=\"repo.devices\">\n        <td><a :href=\"repo.maintainer_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.maintainer }}</a></td>\n        <td><a :href=\"repo.kernel_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.kernel_name }}</a></td>\n        <td>{{ repo.devices }}</td>\n    </tr>\n   </tbody>\n</table>"
  },
  {
    "path": "website/docs/ja_JP/guide/what-is-kernelsu.md",
    "content": "# KernelSU とは?\n\nKernelSU は Android GKI デバイスのための root ソリューションです。カーネルモードで動作し、カーネル空間で直接ユーザー空間アプリに root 権限を付与します。\n\n## 機能\n\nKernelSU の最大の特徴は、**カーネルベース**であることです。KernelSU はカーネルモードで動作するため、今までにないカーネルインターフェイスを提供できます。例えば、カーネルモードで任意のプロセスにハードウェアブレークポイントを追加できる、誰にも気づかれずに任意のプロセスの物理メモリにアクセスできる、カーネル空間で任意のシステムコールを傍受できる、などです。\n\nさらに、KernelSU は [metamodule システム](metamodule.md) を提供しています。これはモジュール管理のためのプラグ可能なアーキテクチャです。従来の root ソリューションがマウントロジックをコアに組み込むのとは異なり、KernelSU はこの作業を metamodule に委任します。これにより、metamodule ([meta-overlayfs](https://github.com/tiann/KernelSU/tree/main/userspace/meta-overlayfs)など) をインストールして、`/system`パーティションや他のパーティションへのsystemless変更を提供できます。\n\n## 使用方法\n\nこちらをご覧ください: [インストール方法](installation)\n\n## ビルド方法\n\n[ビルドするには](../../guide/how-to-build)\n\n## ディスカッション\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n"
  },
  {
    "path": "website/docs/ja_JP/index.md",
    "content": "---\nlayout: home\ntitle: Android 向けのカーネルベース root ソリューション\n\nhero:\n  name: KernelSU\n  text: Android 向けのカーネルベース root ソリューション\n  tagline: \"\"\n  image:\n    src: /logo.png\n    alt: KernelSU\n  actions:\n    - theme: brand\n      text: はじめる\n      link: /ja_JP/guide/what-is-kernelsu\n    - theme: alt\n      text: GitHub で表示\n      link: https://github.com/tiann/KernelSU\n\nfeatures:\n  - title: カーネルベース\n    details: KernelSU は Linux カーネルモードで動作し、ユーザー空間よりも高度な制御が可能です。\n  - title: ホワイトリストの権限管理\n    details: root 権限を許可したアプリのみが su にアクセスでき、他のアプリは su を見つけられません。\n  - title: Metamodule システム\n    details: プラグ可能なモジュールインフラストラクチャにより、systemless方式で/systemを変更可能。metamodule(meta-overlayfsなど)をインストールすることでモジュールのマウント機能を有効化。\n  - title: オープンソース\n    details: KernelSU は GPL-3 でライセンスされたオープンソースプロジェクトです。\n\n"
  },
  {
    "path": "website/docs/pt_BR/guide/app-profile.md",
    "content": "# Perfil do Aplicativo\r\n\r\nO Perfil do Aplicativo é um mecanismo fornecido pelo KernelSU para personalizar a configuração de vários apps.\r\n\r\nPara apps com privilégios root (ou seja, capazes de usar `su`), o Perfil do Aplicativo também pode ser chamado de Perfil root. Ele permite a customização das regras `uid`, `gid`, `grupos`, `capacidades` e `SELinux` do comando `su`, restringindo assim os privilégios do usuário root. Por exemplo, ele pode conceder permissões de rede apenas para apps de firewall enquanto nega permissões de acesso a arquivos, ou pode conceder permissões de shell em vez de acesso root completo para apps congelados: **mantendo o poder confinado com o princípio do menor privilégio.**\r\n\r\nPara apps comuns sem privilégios root, o Perfil do Aplicativo pode controlar o comportamento do kernel e do sistema de módulos em relação a esses apps. Por exemplo, pode determinar se as modificações resultantes dos módulos devem ser abordadas. O kernel e o sistema de módulos podem tomar decisões com base nesta configuração, como realizar operações semelhantes a \"ocultar\".\r\n\r\n## Perfil root\r\n\r\n### UID, GID e Grupos\r\n\r\nOs sistemas Linux possuem dois conceitos: usuários e grupos. Cada usuário possui um ID de usuário (UID) e pode pertencer a vários grupos, cada um com seu próprio ID de grupo (GID). Esses IDs são usados ​​para identificar usuários no sistema e determinar quais recursos do sistema eles podem acessar.\r\n\r\nOs usuários com UID 0 são conhecidos como usuários root, e grupos com GID 0 são chamados de grupos root. O grupo de usuários root geralmente tem os privilégios mais altos no sistema.\r\n\r\nNo caso do sistema Android, cada app funciona como um usuário separado (exceto em casos de UID compartilhado) e recebe um UID exclusivo. Por exemplo, `0` representa o usuário root, `1000` representa `system`, `2000` ao ADB shell e os UIDs de `10000` a `19999` são atribuídos a apps comuns.\r\n\r\n::: info INFORMAÇÕES\r\nAqui, o UID mencionado não é o mesmo que o conceito de múltiplos usuários ou perfis de trabalho no sistema Android. Os perfis de trabalho são, na verdade, implementados particionando o intervalo UID. Por exemplo, 10000-19999 representa o usuário principal, enquanto 110000-119999 representa um perfil de trabalho. Cada app comum entre eles possui seu próprio UID exclusivo.\r\n:::\r\n\r\nCada app pode ter vários grupos, com o GID representando o grupo principal, que geralmente corresponde ao UID. Outros grupos são conhecidos como grupos suplementares. Certas permissões são controladas por meio de grupos, como permissões de acesso à rede ou acesso Bluetooth.\r\n\r\nPor exemplo, se executarmos o comando `id` no ADB shell, a saída pode ser semelhante a esta:\r\n\r\n```sh\r\noriole:/ $ id\r\nuid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),1078(ext_data_rw),1079(ext_obb_rw),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid),3012(readtracefs) context=u:r:shell:s0\r\n```\r\n\r\nAqui, o UID é `2000` e o GID (ID do grupo primário) também é `2000`. Além disso, pertence a vários grupos suplementares, como `inet` (indicando a capacidade de criar soquetes `AF_INET` e `AF_INET6`) e `sdcard_rw` (indicando permissões de leitura/gravação para o cartão SD).\r\n\r\nO Perfil root do KernelSU permite personalizar o UID, GID e grupos para o processo root após a execução de `su`. Por exemplo, o Perfil root de um app root pode definir seu UID como `2000`, o que significa que, ao usar `su`, as permissões reais do app estão no nível do ADB shell. Além disso, o grupo `inet` pode ser removido, evitando que o comando `su` tenha acesso à rede.\r\n\r\n::: tip OBSERVAÇÃO\r\nO Perfil do Aplicativo controla apenas as permissões do processo root após usar `su` e não afeta as permissões do próprio app. Se um app solicitou permissão para acessar a rede, ele ainda poderá acessar a rede mesmo sem usar `su`. Remover o grupo `inet` de `su` apenas impede que `su` acesse a rede.\r\n:::\r\n\r\nO Perfil root é aplicado no kernel e não depende do comportamento voluntário de apps root, ao contrário da troca de usuários ou grupos por meio de `su`. A concessão da permissão `su` depende inteiramente do usuário e não do desenvolvedor.\r\n\r\n### Capacidades\r\n\r\nAs capacidades são um mecanismo para separação de privilégios no Linux.\r\n\r\nPara realizar verificações de permissão, as implementações tradicionais do `UNIX` distinguem duas categorias de processos: processos privilegiados (cujo ID de usuário efetivo é `0`, referido como superusuário ou root) e processos sem privilégios (cujo UID efetivo é diferente de zero). Os processos privilegiados ignoram todas as verificações de permissão do kernel, enquanto os processos não privilegiados estão sujeitos à verificação completa de permissão com base nas credenciais do processo (geralmente: UID efetivo, GID efetivo e lista de grupos suplementares).\r\n\r\nA partir do Linux 2.2, o Linux divide os privilégios tradicionalmente associados ao superusuário em unidades distintas, conhecidas como capacidades, que podem ser ativadas e desativadas de forma independente.\r\n\r\nCada capacidade representa um ou mais privilégios. Por exemplo, `CAP_DAC_READ_SEARCH` representa a capacidade de ignorar verificações de permissão para leitura de arquivos, bem como permissões de leitura e execução de diretório. Se um usuário com um UID efetivo `0` (usuário root) não tiver a capacidade `CAP_DAC_READ_SEARCH` ou superiores, isso significa que mesmo sendo root, ele não pode ler arquivos à vontade.\r\n\r\nO Perfil root do KernelSU permite a personalização das capacidades do processo root após a execução de `su`, concedendo assim \"privilégios root\" de forma parcial. Ao contrário do UID e GID mencionados acima, certos apps root exigem um UID de `0` após usar `su`. Nesses casos, limitar as capacidades deste usuário root com UID `0` pode restringir as operações que ele pode realizar.\r\n\r\n::: tip FORTE RECOMENDAÇÃO\r\nA [documentação oficial](https://man7.org/linux/man-pages/man7/capabilities.7.html) da capacidade do Linux fornece explicações detalhadas das habilidades representadas por cada capacidade. Se você pretende customizar as capacidade, é altamente recomendável que você leia este documento primeiro.\r\n:::\r\n\r\n### SELinux\r\n\r\nSELinux é um poderoso mecanismo do Controle de Acesso Obrigatório (MAC). Ele opera com base no princípio de **negação padrão**. Qualquer ação não explicitamente permitida é negada.\r\n\r\nO SELinux pode ser executado em dois modos globais:\r\n\r\n1. Modo permissivo (Permissive): Os eventos de negação são registrados, mas não aplicados.\r\n2. Modo impondo (Enforcing): Os eventos de negação são registrados e aplicados.\r\n\r\n::: warning AVISO\r\nOs sistemas Android modernos dependem fortemente do SELinux para garantir a segurança geral do sistema. É altamente recomendável não usar nenhum sistema personalizado executado em \"Modo permissivo\", pois ele não oferece vantagens significativas em relação a um sistema completamente aberto.\r\n:::\r\n\r\nExplicar o conceito completo do SELinux é complexo e está além do objetivo deste documento. Recomenda-se primeiro entender seu funcionamento através dos seguintes recursos:\r\n\r\n1. [Wikipédia](https://en.wikipedia.org/wiki/Security-Enhanced_Linux)\r\n2. [Red Hat: O que é SELinux?](https://www.redhat.com/pt-br/topics/linux/what-is-selinux)\r\n3. [ArchLinux: SELinux](https://wiki.archlinux.org/title/SELinux)\r\n\r\nO Perfil root do KernelSU permite a personalização do contexto SELinux do processo root após a execução de `su`. Regras específicas de controle de acesso podem ser definidas para este contexto, possibilitando um controle refinado sobre os privilégios root.\r\n\r\nEm cenários típicos, quando um app executa `su`, ele alterna o processo para um domínio SELinux com **acesso irrestrito**, como `u:r:su:s0`. Através do Perfil root, esse domínio pode ser mudado para um domínio personalizado, como `u:r:app1:s0`, e uma série de regras podem ser definidas para esse domínio:\r\n\r\n```sh\r\ntype app1\r\nenforce app1\r\ntypeattribute app1 mlstrustedsubject\r\nallow app1 * * *\r\n```\r\n\r\nObserve que a regra `allow app1 * * *` é usada apenas para fins de demonstração. Na prática, esta regra não deve ser usada extensivamente, pois não difere muito do Modo permissivo.\r\n\r\n### Escalação\r\n\r\nSe a configuração do Perfil root não estiver definida corretamente, poderá ocorrer um cenário de escalação. As restrições impostas pelo Perfil root poderão falhar involuntariamente.\r\n\r\nPor exemplo, se você conceder permissão root a um usuário ADB shell (que é um caso comum) e, em seguida, conceder permissão root a um app normal, mas configurar seu Perfil root com o UID 2000 (o UID do usuário ADB shell), o app pode obter acesso root completo ao executar o comando `su` duas vezes:\r\n\r\n1. A primeira execução de `su` será sujeita ao Perfil do Aplicativo, e mudará para o UID `2000` (ADB shell) em vez de `0` (root).\r\n2. A segunda execução de `su`, como o UID é `2000` e você concedeu acesso root ao UID `2000` (ADB shell) na configuração, o app obterá privilégios root completo.\r\n\r\n::: warning OBSERVAÇÃO\r\nEste comportamento é totalmente esperado e não é um bug. Portanto, recomendamos o seguinte:\r\n\r\nSe você realmente precisa conceder privilégios root ao ADB (por exemplo, como desenvolvedor), não é aconselhável alterar o UID para `2000` ao configurar o Perfil root. Usar `1000` (sistema) seria uma melhor escolha.\r\n:::\r\n\r\n## Perfil não root\r\n\r\n### Desmontar módulos\r\n\r\nO KernelSU fornece um mecanismo sem sistema para modificar partições do sistema, obtido através da montagem do OverlayFS. No entanto, alguns apps podem ser sensíveis a esse comportamento. Nesse caso, podemos descarregar módulos montados nesses apps configurando a opção \"Desmontar módulos\".\r\n\r\nAlém disso, a interface de configurações do gerenciador do KernelSU oferece a opção \"Desmontar módulos por padrão\". Por padrão, essa opção está **ativada**, o que significa que o KernelSU ou alguns módulos descarregarão módulos para este app, a menos que configurações adicionais sejam aplicadas. Se você não preferir esta configuração ou se ela afetar determinados apps, você terá as seguintes opções:\r\n\r\n1. Manter a opção \"Desmontar módulos por padrão\" ativada e desative individualmente a opção \"Desmontar módulos\" no Perfil do Aplicativo para apps que exigem o carregamento do módulo (agindo como uma \"lista de permissões\").\r\n2. Desativar a opção \"Desmontar módulos por padrão\" e ativar individualmente a opção \"Desmontar módulos\" no Perfil do Aplicativo para apps que exigem o descarregamento do módulo (agindo como uma \"lista negra\").\r\n\r\n::: info INFORMAÇÕES\r\nEm dispositivos que utilizam a versão do kernel 5.10 ou superior, o kernel realiza qualquer ação adicional do descarregamento de módulos. No entanto, para dispositivos que executam versões do kernel abaixo de 5.10, essa opção é apenas uma opção de configuração e o próprio KernelSU não executa nenhuma ação. Se você quiser usar a opção \"Desmontar módulos\" em versões do kernel anteriores a 5.10, é necessário portar a função `path_umount` em `fs/namespace.c`. Você pode obter mais informações no final da página [Integração para dispositivos não-GKI](https://kernelsu.org/pt_BR/guide/how-to-integrate-for-non-gki.html). Alguns módulos, como ZygiskNext, também podem usar essa opção para determinar se o descarregamento do módulo é necessário.\r\n:::\r\n"
  },
  {
    "path": "website/docs/pt_BR/guide/difference-with-magisk.md",
    "content": "# Diferenças com Magisk\r\n\r\nEmbora os módulos do KernelSU e do Magisk tenham muitas semelhanças, existem inevitavelmente algumas diferenças devido aos seus mecanismos de implementação completamente diferentes. Se você deseja que seu módulo funcione tanto no Magisk quanto no KernelSU, é essencial compreender essas diferenças.\r\n\r\n## Semelhanças\r\n\r\n- Formato de arquivo do módulo: Ambos usam o formato ZIP para organizar os módulos, e o formato dos módulos é praticamente o mesmo.\r\n- Diretório de instalação do módulo: Ambos estão localizados em `/data/adb/modules`.\r\n- Sem sistema: Ambos suportam a modificação de `/system` de forma sem sistema por meio de módulos.\r\n- post-fs-data.sh: O tempo de execução e a semântica são exatamente os mesmos.\r\n- service.sh: O tempo de execução e a semântica são exatamente os mesmos.\r\n- system.prop: Completamente o mesmo.\r\n- sepolicy.rule: Completamente o mesmo.\r\n- BusyBox: Os scripts são executados no BusyBox com o \"Modo Autônomo\" ativado em ambos os casos.\r\n\r\n## Diferenças\r\n\r\nAntes de entender as diferenças, é importante saber como identificar se o seu módulo está sendo executado no KernelSU ou no Magisk. Você pode usar a variável de ambiente `KSU` para diferenciá-lo em todos os locais onde você pode executar os scripts do módulo (`customize.sh`, `post-fs-data.sh`, `service.sh`). No KernelSU, essa variável de ambiente será definida como `true`.\r\n\r\nAqui estão algumas diferenças:\r\n\r\n- Os módulos KernelSU não podem ser instalados no modo Recovery.\r\n- Os módulos KernelSU não oferece suporte nativo ao Zygisk, mas você pode usar módulos Zygisk através do [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext).\r\n- **Arquitetura de montagem de módulos**: KernelSU usa um [sistema metamodule](metamodule.md), delegando a montagem a metamodules plugáveis (por exemplo, `meta-overlayfs`), enquanto o Magisk tem a montagem integrada em seu núcleo. KernelSU requer instalar um metamodule para habilitar a montagem de módulos.\r\n- O método para substituir ou excluir arquivos nos módulos do KernelSU é completamente diferente do Magisk. O KernelSU não suporta o método `.replace`. Em vez disso, você deve criar um arquivo com o comando `mknod filename c 0 0` para excluir o arquivo correspondente.\r\n- Os diretórios do BusyBox são diferentes. O BusyBox integrado no KernelSU está localizado em `/data/adb/ksu/bin/busybox`, enquanto no Magisk está em `/data/adb/magisk/busybox`. **Observe que este é um comportamento interno do KernelSU e pode mudar no futuro!**\r\n- O KernelSU não suporta arquivos `.replace`, mas oferece suporte às variáveis ​​`REMOVE` e `REPLACE` para remover ou substituir arquivos e pastas.\r\n- O KernelSU adiciona um script `boot-completed.sh` para executar tarefas após a conclusão da inicialização do sistema Android.\r\n- O KernelSU adiciona um script `post-mount.sh` para executar tarefas após a conclusão da montagem do módulo.\r\n"
  },
  {
    "path": "website/docs/pt_BR/guide/faq.md",
    "content": "# Perguntas frequentes\r\n\r\n## KernelSU oferece suporte ao meu dispositivo?\r\n\r\nO KernelSU suporta dispositivos rodando Android com bootloader desbloqueado. No entanto, o suporte oficial é apenas para kernels Linux GKI 5.10+ (na prática isso significa que seu dispositivo precisa ter Android 12 de fábrica para ser compatível).\r\n\r\nVocê pode verificar facilmente o suporte para o seu dispositivo através do gerenciador do KernelSU, que está disponível [aqui](https://github.com/tiann/KernelSU/releases). \r\n\r\nSe o app mostrar `Não instalado`, significa que seu dispositivo é oficialmente suportado pelo KernelSU.\r\n\r\nSe o app mostrar `Sem suporte`, significa que seu dispositivo não é oficialmente suportado no momento. No entanto, você pode compilar o código-fonte do kernel e integrar o KernelSU para fazê-lo funcionar, ou usar [Dispositivos com suporte não oficial](unofficially-support-devices).\r\n\r\n## Para usar o KernelSU precisa desbloquear o bootloader?\r\n\r\nCertamente, sim.\r\n\r\n## KernelSU suporta módulos?\r\n\r\nSim, a maioria dos módulos Magisk funcionam no KernelSU. No entanto, se seu módulo precisar modificar arquivos `/system`, você precisa instalar um [metamodule](metamodule.md) (como `meta-overlayfs`). Outros recursos de módulos funcionam sem um metamodule. Confira o [Guia de módulos](module.md) para mais informações.\r\n\r\n## KernelSU suporta Xposed?\r\n\r\nSim, você pode usar LSPosed (ou outro derivado moderno do Xposed) com [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext).\r\n\r\n## KernelSU suporta Zygisk?\r\n\r\nKernelSU não tem suporte integrado ao Zygisk, mas você pode usar um módulo como [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) para suportá-lo.\r\n\r\n## Por que meus módulos não funcionam após uma instalação nova?\r\n\r\nSe seus módulos precisam modificar arquivos `/system`, você precisa instalar um [metamodule](metamodule.md) para montar o diretório `system`. Outros recursos de módulos (scripts, sepolicy, system.prop) funcionam sem um metamodule.\r\n\r\n**Solução**: Consulte o [Guia de Metamodule](metamodule.md) para instruções de instalação.\r\n\r\n## O que é um metamodule e por que preciso dele?\r\n\r\nUm metamodule é um módulo especial que fornece infraestrutura para montar módulos regulares. Consulte o [Guia de Metamodule](metamodule.md) para uma explicação completa.\r\n\r\n## KernelSU é compatível com o Magisk?\r\n\r\nO sistema de módulos do KernelSU está em conflito com a montagem mágica do Magisk. Se houver algum módulo ativado no KernelSU, todo o Magisk deixará de funcionar.\r\n\r\nNo entanto, se você usar apenas o `su` do KernelSU, ele funcionará bem com o Magisk. O KernelSU modifica o `kernel`, enquanto o Magisk modifica o `ramdisk`, permitindo que ambos trabalhem juntos.\r\n\r\n## KernelSU substituirá o Magisk?\r\n\r\nAcreditamos que não, e esse não é o nosso objetivo. O Magisk é bom o suficiente para solução root do espaço do usuário e terá uma longa vida. O objetivo do KernelSU é fornecer uma interface de kernel aos usuários, não substituindo o Magisk.\r\n\r\n## KernelSU oferece suporte a dispositivos não-GKI?\r\n\r\nÉ possível. Mas você deve baixar o código-fonte do kernel e integrar o KernelSU à árvore do código-fonte e compilar o kernel você mesmo.\r\n\r\n## KernelSU oferece suporte a dispositivos abaixo do Android 12?\r\n\r\nÉ o kernel do dispositivo que afeta a compatibilidade do KernelSU e não tem nada a ver com a versão do Android. A única restrição é que os dispositivos lançados com Android 12 devem ser kernel 5.10+ (dispositivos GKI). Então:\r\n\r\n1. Os dispositivos lançados com Android 12 devem ser compatíveis.\r\n2. Dispositivos com kernel antigo (alguns dispositivos com Android 12 também têm o kernel antigo) são compatíveis (você mesmo deve compilar o kernel).\r\n\r\n## KernelSU suporta kernel antigo?\r\n\r\nÉ possível, o KernelSU é portado para o kernel 4.14 agora. Para kernels mais antigo, você precisa portar manualmente e PRs são sempre bem-vindas!\r\n\r\n## Como integrar o KernelSU para um kernel antigo?\r\n\r\nPor favor, verifique o guia [Integração para dispositivos não-GKI](how-to-integrate-for-non-gki).\r\n\r\n## Por que a minha versão do Android é 13 e o kernel mostra \"android12-5.10\"?\r\n\r\nA versão do Kernel não tem nada a ver com a versão do Android. Se você precisar fazer o flash do kernel, use sempre a versão do kernel, a versão do Android não é tão importante.\r\n\r\n## Eu sou GKI 1.0, posso usar isso?\r\n\r\nGKI 1.0 é completamente diferente do GKI 2.0, você deve compilar o kernel sozinho.\r\n\r\n## Como posso fazer `/system` RW?\r\n\r\nNão recomendamos que você modifique a partição do sistema diretamente. Por favor, verifique [Guias de módulo](module.md) para modificá-lo sem sistema. Se você insiste em fazer isso, verifique [magisk_overlayfs](https://github.com/HuskyDG/magic_overlayfs).\r\n\r\n## KernelSU pode modificar hosts? Como posso usar AdAway?\r\n\r\nClaro. Mas o KernelSU não tem suporte a hosts integrados, você pode instalar um módulo como [systemless-hosts](https://github.com/symbuzzer/systemless-hosts-KernelSU-module) para fazer isso.\r\n"
  },
  {
    "path": "website/docs/pt_BR/guide/hidden-features.md",
    "content": "# Recursos ocultos\n\n## .ksurc\n\nPor padrão, `/system/bin/sh` carrega `/system/etc/mkshrc`.\n\nVocê pode fazer su carregar um arquivo rc personalizado criando um arquivo `/data/adb/ksu/.ksurc`.\n"
  },
  {
    "path": "website/docs/pt_BR/guide/how-to-build.md",
    "content": "# Como compilar\r\n\r\n::: warning\r\nEste documento é apenas para referência de arquivo e não é mais mantido.\r\nDesde o KernelSU v3.0, abandonamos o suporte oficial para o modo de imagem GKI para iteração e velocidade de compilação mais rápidas. É recomendado usar `Ylarod/ddk` para compilar o LKM.\r\n:::\r\n\r\nPrimeiro, você deve ler a documentação oficial do Android para compilação do kernel:\r\n\r\n1. [Como criar kernels](https://source.android.com/docs/setup/build/building-kernels)\r\n2. [Builds de versão de imagem genérica do kernel (GKI)](https://source.android.com/docs/core/architecture/kernel/gki-release-builds)\r\n\r\n::: warning AVISO\r\nEsta página é para dispositivos GKI, se você usa um kernel antigo, consulte [Integração para dispositivos não-GKI](how-to-integrate-for-non-gki).\r\n:::\r\n\r\n## Compilar o kernel\r\n\r\n### Sincronize o código-fonte do kernel\r\n\r\n```sh\r\nrepo init -u https://android.googlesource.com/kernel/manifest\r\nmv <kernel_manifest.xml> .repo/manifests\r\nrepo init -m manifest.xml\r\nrepo sync\r\n```\r\n\r\nO arquivo `<kernel_manifest.xml>` é um manifesto que pode determinar exclusivamente uma compilação, permitindo que você a torne reprodutível. Para isso, você deve baixar o arquivo de manifesto em [Builds de versão de imagem genérica do kernel (GKI)](https://source.android.com/docs/core/architecture/kernel/gki-release-builds).\r\n\r\n### Construir\r\n\r\nPor favor, verifique [Como criar kernels](https://source.android.com/docs/setup/build/building-kernels) primeiro.\r\n\r\nPor exemplo, para compilar uma imagem de kernel `aarch64`:\r\n\r\n```sh\r\nLTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh\r\n```\r\n\r\nNão se esqueça de adicionar o sinalizador `LTO=thin`, caso contrário a compilação poderá falhar se a memória do seu computador for inferior a 24 GB.\r\n\r\nA partir do Android 13, o kernel é compilado pelo `bazel`:\r\n\r\n```sh\r\ntools/bazel build --config=fast //common:kernel_aarch64_dist\r\n```\r\n\r\n::: info INFORMAÇÕES\r\nPara alguns kernel do Android 14, para fazer o Wi-Fi/Bluetooth funcionar, pode ser necessário remover todas as exportações protegidas pelo GKI:\r\n\r\n```sh\r\nrm common/android/abi_gki_protected_exports_*\r\n```\r\n:::\r\n\r\n## Compilar o kernel com KernelSU\r\n\r\nSe você conseguir compilar o kernel com sucesso, adicionar suporte ao KernelSU será relativamente simples. Na raiz do diretório de origem do kernel, execute qualquer uma das opções listadas abaixo:\r\n\r\n::: code-group\r\n\r\n```sh[Tag mais recente (estável)]\r\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\r\n```\r\n\r\n```sh[Branch main (dev)]\r\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\r\n```\r\n\r\n```sh[Selecionar tag (como v0.5.2)]\r\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\r\n```\r\n\r\n:::\r\n\r\nEntão, reconstrua o kernel e você obterá uma imagem do kernel com o KernelSU!\r\n"
  },
  {
    "path": "website/docs/pt_BR/guide/how-to-integrate-for-non-gki.md",
    "content": "# Integração para dispositivos não-GKI\r\n\r\n::: warning\r\nEste documento é apenas para referência de arquivo e não é mais mantido.\r\nDesde o KernelSU v1.0, abandonamos o suporte oficial para dispositivos não-GKI.\r\n:::\r\n\r\nO KernelSU pode ser integrado a kernels não-GKI e foi portado para 4.14 e versões anteriores.\r\n\r\nDevido à fragmentação dos kernels não-GKI, não temos um método universal para construí-lo, portanto, não podemos fornecer o boot.img não-GKI. No entanto, você pode compilar o kernel com o KernelSU integrado por conta própria.\r\n\r\nPrimeiro, você deve ser capaz de compilar um kernel inicializável a partir do código-fonte do kernel. Se o kernel não for de código aberto, será difícil executar o KernelSU para o seu dispositivo.\r\n\r\nSe você puder compilar um kernel inicializável, existem duas maneiras de integrar o KernelSU ao código-fonte do kernel:\r\n\r\n1. Automaticamente com `kprobe`\r\n2. Manualmente\r\n\r\n## Integrar com kprobe\r\n\r\nO KernelSU usa kprobe para fazer ganchos do kernel, se o kprobe funcionar bem em seu kernel, é recomendado usar desta forma.\r\n\r\nPrimeiro, adicione o KernelSU à árvore de origem do kernel:\r\n\r\n```sh\r\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.9.5\r\n```\r\n\r\n::: info INFORMAÇÕES\r\n[KernelSU 1.0 e versões posteriores não suportam mais kernels não-GKI](https://github.com/tiann/KernelSU/issues/1705). A última versão suportada é a `v0.9.5`, portanto, certifique-se de usar a versão correta.\r\n:::\r\n\r\nEntão, você deve verificar se o kprobe está ativado na configuração do seu kernel. Caso não esteja, adicione estas configurações a ele:\r\n\r\n```txt\r\nCONFIG_KPROBES=y\r\nCONFIG_HAVE_KPROBES=y\r\nCONFIG_KPROBE_EVENTS=y\r\n```\r\n\r\nAgora, ao recompilar seu kernel, o KernelSU deve funcionar corretamente.\r\n\r\nSe você descobrir que o KPROBES ainda não está ativado, pode tentar ativar `CONFIG_MODULES`. Se isso não resolver, use `make menuconfig` para procurar outras dependências do KPROBES.\r\n\r\nPorém, se você entrar em um bootloop após integrar o KernelSU, isso pode indicar que o **kprobe está quebrado no seu kernel**, o que significa que você precisará corrigir o bug do kprobe ou usar outra maneira.\r\n\r\n::: tip COMO VERIFICAR SE O KPROBE ESTÁ QUEBRADO?\r\nComente `ksu_sucompat_init()` e `ksu_ksud_init()` em `KernelSU/kernel/ksu.c`, se o dispositivo inicializar normalmente, então o kprobe pode estar quebrado.\r\n:::\r\n\r\n::: info COMO FAZER COM QUE O RECURSO DE DESMONTAR MÓDULOS FUNCIONE NO PRÉ-GKI?\r\nSe o seu kernel for inferior a 5.9, você deve portar `path_umount` para `fs/namespace.c`. Isso é necessário para que o recurso \"Desmontar módulos\" funcione corretamente. Caso você não porte `path_umount`, o recurso \"Desmontar módulos\" não funcionará. Você pode obter mais informações sobre como conseguir isso no final desta página.\r\n:::\r\n\r\n## Modifique manualmente a fonte do kernel\r\n\r\nSe o kprobe não funcionar no seu kernel (isso pode ser causado por um bug no upstream ou do kernel abaixo de 4.8), então você pode tentar o seguinte:\r\n\r\nPrimeiro, adicione o KernelSU à árvore de origem do kernel:\r\n\r\n```sh\r\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.9.5\r\n```\r\n\r\nTenha em mente que, em alguns dispositivos, seu defconfig pode estar localizado em `arch/arm64/configs` ou em outros casos pode estar em `arch/arm64/configs/vendor/your_defconfig`. Independentemente do defconfig que você estiver usando, certifique-se de ativar `CONFIG_KSU` com `y` para ativa-lo ou `n` para desativa-lo. Por exemplo, se optar por ativá-lo, seu defconfig deverá conter a seguinte linha:\r\n\r\n```txt\r\n# KernelSU\r\nCONFIG_KSU=y\r\n```\r\n\r\nEm seguida, adicione chamadas do KernelSU à fonte do kernel. Abaixo estão alguns patches para referência:\r\n\r\n::: code-group\r\n\r\n```diff[exec.c]\r\ndiff --git a/fs/exec.c b/fs/exec.c\r\nindex ac59664eaecf..bdd585e1d2cc 100644\r\n--- a/fs/exec.c\r\n+++ b/fs/exec.c\r\n@@ -1890,11 +1890,14 @@ static int __do_execve_file(int fd, struct filename *filename,\r\n \treturn retval;\r\n }\r\n\r\n+#ifdef CONFIG_KSU\r\n+extern bool ksu_execveat_hook __read_mostly;\r\n+extern int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv,\r\n+\t\t\tvoid *envp, int *flags);\r\n+extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,\r\n+\t\t\t\t void *argv, void *envp, int *flags);\r\n+#endif\r\n static int do_execveat_common(int fd, struct filename *filename,\r\n \t\t\t      struct user_arg_ptr argv,\r\n \t\t\t      struct user_arg_ptr envp,\r\n \t\t\t      int flags)\r\n {\r\n+   #ifdef CONFIG_KSU\r\n+\tif (unlikely(ksu_execveat_hook))\r\n+\t\tksu_handle_execveat(&fd, &filename, &argv, &envp, &flags);\r\n+\telse\r\n+\t\tksu_handle_execveat_sucompat(&fd, &filename, &argv, &envp, &flags);\r\n+   #endif\r\n \treturn __do_execve_file(fd, filename, argv, envp, flags, NULL);\r\n }\r\n```\r\n```diff[open.c]\r\ndiff --git a/fs/open.c b/fs/open.c\r\nindex 05036d819197..965b84d486b8 100644\r\n--- a/fs/open.c\r\n+++ b/fs/open.c\r\n@@ -348,6 +348,8 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\r\n \treturn ksys_fallocate(fd, mode, offset, len);\r\n }\r\n\r\n+#ifdef CONFIG_KSU\r\n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\r\n+\t\t\t int *flags);\r\n+#endif\r\n /*\r\n  * access() precisa usar o uid/gid real, não o uid/gid efetivo.\r\n  * Fazemos isso limpando temporariamente todos os recursos relacionados ao FS e\r\n@@ -355,6 +357,7 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\r\n  */\r\n long do_faccessat(int dfd, const char __user *filename, int mode)\r\n {\r\n \tconst struct cred *old_cred;\r\n \tstruct cred *override_cred;\r\n \tstruct path path;\r\n \tstruct inode *inode;\r\n \tstruct vfsmount *mnt;\r\n \tint res;\r\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\r\n+   #ifdef CONFIG_KSU\r\n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\r\n+   #endif\r\n \r\n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\r\n \t\treturn -EINVAL;\r\n```\r\n```diff[read_write.c]\r\ndiff --git a/fs/read_write.c b/fs/read_write.c\r\nindex 650fc7e0f3a6..55be193913b6 100644\r\n--- a/fs/read_write.c\r\n+++ b/fs/read_write.c\r\n@@ -434,10 +434,14 @@ ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)\r\n }\r\n EXPORT_SYMBOL(kernel_read);\r\n\r\n+#ifdef CONFIG_KSU\r\n+extern bool ksu_vfs_read_hook __read_mostly;\r\n+extern int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,\r\n+\t\t\tsize_t *count_ptr, loff_t **pos);\r\n+#endif\r\n ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)\r\n {\r\n \tssize_t ret;\r\n+   #ifdef CONFIG_KSU \r\n+\tif (unlikely(ksu_vfs_read_hook))\r\n+\t\tksu_handle_vfs_read(&file, &buf, &count, &pos);\r\n+   #endif\r\n+\r\n \tif (!(file->f_mode & FMODE_READ))\r\n \t\treturn -EBADF;\r\n \tif (!(file->f_mode & FMODE_CAN_READ))\r\n```\r\n```diff[stat.c]\r\ndiff --git a/fs/stat.c b/fs/stat.c\r\nindex 376543199b5a..82adcef03ecc 100644\r\n--- a/fs/stat.c\r\n+++ b/fs/stat.c\r\n@@ -148,6 +148,8 @@ int vfs_statx_fd(unsigned int fd, struct kstat *stat,\r\n }\r\n EXPORT_SYMBOL(vfs_statx_fd);\r\n\r\n+#ifdef CONFIG_KSU\r\n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\r\n+#endif\r\n+\r\n /**\r\n  * vfs_statx - Obtenha atributos básicos e extras por filename\r\n  * @dfd: Um descritor de arquivo que representa o diretório base para um filename relativo\r\n@@ -170,6 +172,7 @@ int vfs_statx(int dfd, const char __user *filename, int flags,\r\n \tint error = -EINVAL;\r\n \tunsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_AUTOMOUNT;\r\n\r\n+   #ifdef CONFIG_KSU\r\n+\tksu_handle_stat(&dfd, &filename, &flags);\r\n+   #endif\r\n \tif ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\r\n \t\t       AT_EMPTY_PATH | KSTAT_QUERY_FLAGS)) != 0)\r\n \t\treturn -EINVAL;\r\n```\r\n\r\n:::\r\n\r\nVocê deve encontrar as quatro funções no código-fonte do kernel:\r\n\r\n1. `do_faccessat`, geralmente em `fs/open.c`\r\n2. `do_execveat_common`, geralmente em `fs/exec.c`\r\n3. `vfs_read`, geralmente em `fs/read_write.c`\r\n4. `vfs_statx`, geralmente em `fs/stat.c`\r\n\r\nSe o seu kernel não tiver a função `vfs_statx`, use `vfs_fstatat`:\r\n\r\n```diff\r\ndiff --git a/fs/stat.c b/fs/stat.c\r\nindex 068fdbcc9e26..5348b7bb9db2 100644\r\n--- a/fs/stat.c\r\n+++ b/fs/stat.c\r\n@@ -87,6 +87,8 @@ int vfs_fstat(unsigned int fd, struct kstat *stat)\r\n }\r\n EXPORT_SYMBOL(vfs_fstat);\r\n\r\n+#ifdef CONFIG_KSU\r\n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\r\n+#endif\r\n int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\r\n \t\tint flag)\r\n {\r\n@@ -94,6 +96,8 @@ int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\r\n \tint error = -EINVAL;\r\n \tunsigned int lookup_flags = 0;\r\n+   #ifdef CONFIG_KSU \r\n+\tksu_handle_stat(&dfd, &filename, &flag);\r\n+   #endif\r\n+\r\n \tif ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\r\n \t\t      AT_EMPTY_PATH)) != 0)\r\n \t\tgoto out;\r\n```\r\n\r\nPara kernels anteriores ao 4.17, se você não conseguir encontrar `do_faccessat`, basta ir até a definição do syscall `faccessat` e fazer a chamada lá:\r\n\r\n```diff\r\ndiff --git a/fs/open.c b/fs/open.c\r\nindex 2ff887661237..e758d7db7663 100644\r\n--- a/fs/open.c\r\n+++ b/fs/open.c\r\n@@ -355,6 +355,9 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\r\n \treturn error;\r\n }\r\n\r\n+#ifdef CONFIG_KSU\r\n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\r\n+\t\t\t        int *flags);\r\n+#endif\r\n+\r\n /*\r\n  * access() precisa usar o uid/gid real, não o uid/gid efetivo.\r\n  * Fazemos isso limpando temporariamente todos os recursos relacionados ao FS e\r\n@@ -370,6 +373,8 @@ SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)\r\n \tint res;\r\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\r\n+   #ifdef CONFIG_KSU\r\n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\r\n+   #endif\r\n+\r\n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\r\n \t\treturn -EINVAL;\r\n```\r\n\r\n### Modo de Segurança\r\n\r\nPara ativar o Modo de Segurança integrado do KernelSU, você deve modificar a função `input_handle_event` em `drivers/input/input.c`:\r\n\r\n::: tip DICA\r\nÉ altamente recomendável ativar este recurso, é muito útil para evitar bootloops!\r\n:::\r\n\r\n```diff\r\ndiff --git a/drivers/input/input.c b/drivers/input/input.c\r\nindex 45306f9ef247..815091ebfca4 100755\r\n--- a/drivers/input/input.c\r\n+++ b/drivers/input/input.c\r\n@@ -367,10 +367,13 @@ static int input_get_disposition(struct input_dev *dev,\r\n \treturn disposition;\r\n }\r\n\r\n+#ifdef CONFIG_KSU\r\n+extern bool ksu_input_hook __read_mostly;\r\n+extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value);\r\n+#endif\r\n+\r\n static void input_handle_event(struct input_dev *dev,\r\n \t\t\t       unsigned int type, unsigned int code, int value)\r\n {\r\n\tint disposition = input_get_disposition(dev, type, code, &value);\r\n+   #ifdef CONFIG_KSU\r\n+\tif (unlikely(ksu_input_hook))\r\n+\t\tksu_handle_input_handle_event(&type, &code, &value);\r\n+   #endif\r\n \r\n \tif (disposition != INPUT_IGNORE_EVENT && type != EV_SYN)\r\n \t\tadd_input_randomness(type, code, value);\r\n```\r\n\r\n::: info ENTRANDO NO MODO DE SEGURANÇA ACIDENTALMENTE?\r\nSe você estiver usando a integração manual e não desativar `CONFIG_KPROBES`, o usuário poderá acionar o Modo de Segurança pressionando o botão de diminuir volume após a inicialização! Portanto, se estiver usando a integração manual, é necessário desativar `CONFIG_KPROBES`!\r\n:::\r\n\r\n### Falha ao executar `pm` no terminal?\r\n\r\nVocê deve modificar `fs/devpts/inode.c`. Referência:\r\n\r\n```diff\r\ndiff --git a/fs/devpts/inode.c b/fs/devpts/inode.c\r\nindex 32f6f1c68..d69d8eca2 100644\r\n--- a/fs/devpts/inode.c\r\n+++ b/fs/devpts/inode.c\r\n@@ -602,6 +602,8 @@ struct dentry *devpts_pty_new(struct pts_fs_info *fsi, int index, void *priv)\r\n        return dentry;\r\n }\r\n\r\n+#ifdef CONFIG_KSU\r\n+extern int ksu_handle_devpts(struct inode*);\r\n+#endif\r\n+\r\n /**\r\n  * devpts_get_priv -- get private data for a slave\r\n  * @pts_inode: inode of the slave\r\n@@ -610,6 +612,7 @@ struct dentry *devpts_pty_new(struct pts_fs_info *fsi, int index, void *priv)\r\n  */\r\n void *devpts_get_priv(struct dentry *dentry)\r\n {\r\n+       #ifdef CONFIG_KSU\r\n+       ksu_handle_devpts(dentry->d_inode);\r\n+       #ifdef CONFIG_KSU\r\n        if (dentry->d_sb->s_magic != DEVPTS_SUPER_MAGIC)\r\n                return NULL;\r\n        return dentry->d_fsdata;\r\n```\r\n\r\n### Como portar path_umount\r\n\r\nVocê pode fazer com que o recurso \"Desmontar módulos\" funcione em kernels pré-GKI portando manualmente `path_umount` da versão 5.9. Você pode usar este patch como referência:\r\n\r\n```diff\r\n--- a/fs/namespace.c\r\n+++ b/fs/namespace.c\r\n@@ -1739,6 +1739,39 @@ static inline bool may_mandlock(void)\r\n }\r\n #endif\r\n\r\n+static int can_umount(const struct path *path, int flags)\r\n+{\r\n+\tstruct mount *mnt = real_mount(path->mnt);\r\n+\r\n+\tif (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW))\r\n+\t\treturn -EINVAL;\r\n+\tif (!may_mount())\r\n+\t\treturn -EPERM;\r\n+\tif (path->dentry != path->mnt->mnt_root)\r\n+\t\treturn -EINVAL;\r\n+\tif (!check_mnt(mnt))\r\n+\t\treturn -EINVAL;\r\n+\tif (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */\r\n+\t\treturn -EINVAL;\r\n+\tif (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN))\r\n+\t\treturn -EPERM;\r\n+\treturn 0;\r\n+}\r\n+\r\n+int path_umount(struct path *path, int flags)\r\n+{\r\n+\tstruct mount *mnt = real_mount(path->mnt);\r\n+\tint ret;\r\n+\r\n+\tret = can_umount(path, flags);\r\n+\tif (!ret)\r\n+\t\tret = do_umount(mnt, flags);\r\n+\r\n+\t/* não devemos chamar path_put() pois isso limparia mnt_expiry_mark */\r\n+\tdput(path->dentry);\r\n+\tmntput_no_expire(mnt);\r\n+\treturn ret;\r\n+}\r\n /*\r\n  * Agora o umount pode lidar com pontos de montagem e também com dispositivos bloqueados.\r\n  * Isto é importante para filesystems que usam dispositivos bloqueados sem nome.\r\n```\r\n\r\nFinalmente, compile seu kernel novamente e o KernelSU deverá funcionar corretamente.\r\n"
  },
  {
    "path": "website/docs/pt_BR/guide/installation.md",
    "content": "# Instalação\r\n\r\n## Verifique se o seu dispositivo é compatível\r\n\r\nBaixe o gerenciador do KernelSU em [GitHub Releases](https://github.com/tiann/KernelSU/releases) e instale-o no seu dispositivo:\r\n\r\n- Se o app mostrar `Sem suporte`, significa que **você precisará compilar o kernel por conta própria**. O KernelSU não fornecerá e nunca fornecerá um arquivo boot.img para você instalar.\r\n- Se o app mostrar `Não instalado`, então seu dispositivo é oficialmente suportado pelo KernelSU.\r\n\r\n::: info INFORMAÇÕES\r\nPara dispositivos que mostram `Sem suporte`, você pode conferir a lista de [Dispositivos com suporte não oficial](unofficially-support-devices.md). Você mesmo pode compilar o kernel.\r\n:::\r\n\r\n## Backup padrão do boot.img\r\n\r\nAntes de fazer o flash, é essencial que você faça o backup do seu boot.img padrão. Se encontrar algum bootloop, você sempre pode restaurar o sistema voltando ao boot padrão de fábrica usando o fastboot.\r\n\r\n::: warning AVISO\r\nO flash pode causar perda de dados. Certifique-se de executar esta etapa bem antes de prosseguir para a próxima! Se necessário, também é recomendável fazer backup de todos os dados do seu dispositivo.\r\n:::\r\n\r\n## Conhecimento necessário\r\n\r\n### ADB e fastboot\r\n\r\nPor padrão, você usará as ferramentas ADB e fastboot neste tutorial, portanto, se você não as conhece, recomendamos pesquisar para aprender sobre elas primeiro.\r\n\r\n### KMI\r\n\r\nKernel Module Interface (KMI), versões de kernel com o mesmo KMI são **compatíveis**, isso é o que \"geral\" significa no GKI. Por outro lado, se o KMI for diferente, então esses kernels não são compatíveis entre si, e atualizar uma imagem do kernel com um KMI diferente do seu dispositivo pode causar um bootloop.\r\n\r\nEspecificamente, para dispositivos GKI, o formato da versão do kernel deve ser a seguinte:\r\n\r\n```txt\r\nKernelRelease :=\r\nVersion.PatchLevel.SubLevel-AndroidRelease-KmiGeneration-suffix\r\nw      .x         .y       -zzz           -k            -alguma coisa\r\n```\r\n\r\n`w.x-zzz-k` é a versão KMI. Por exemplo, se a versão do kernel de um dispositivo for `5.10.101-android12-9-g30979850fc20`, então seu KMI será `5.10-android12-9`. Teoricamente, ele pode inicializar normalmente com outros kernels KMI.\r\n\r\n::: tip DICA\r\nObserve que o SubLevel na versão do kernel não faz parte do KMI! Isso significa que `5.10.101-android12-9-g30979850fc20` tem o mesmo KMI que `5.10.137-android12-9-g30979850fc20`!\r\n:::\r\n\r\n### Nível do patch de segurança {#security-patch-level}\r\n\r\nDispositivos Android mais recentes podem ter mecanismos anti-rollback que impedem o flash de um boot.img com um nível de patch de segurança antigo. Por exemplo, se o kernel do seu dispositivo for `5.10.101-android12-9-g30979850fc20`, o patch de segurança será `2023-11`, mesmo se você atualizar o kernel correspondente ao KMI do kernel, se o nível do patch de segurança for anterior a `2023-11` (como `2023-06`), isso pode causar um bootloop.\r\n\r\nPortanto, kernels com os níveis de patch de segurança mais recentes são preferidos para manter a compatibilidade com o KMI.\r\n\r\n### Versão do kernel vs Versão do Android\r\n\r\nPor favor, observe: **A versão do kernel e a versão do Android não são necessariamente iguais!**\r\n\r\nSe você descobrir que a versão do seu kernel é `android12-5.10.101`, mas a versão do seu sistema Android é Android 13 ou outra, não se surpreenda, pois o número da versão do sistema Android não é necessariamente igual ao número da versão do kernel Linux. O número da versão do kernel Linux geralmente é correspondente à versão do sistema Android que acompanha o **dispositivo quando ele é enviado**. Se o sistema Android for atualizado posteriormente, a versão do kernel geralmente não será alterada. Então, antes de flashar qualquer coisa, **consulte sempre a versão do kernel!**\r\n\r\n## Introdução\r\n\r\nDesde a versão [0.9.0](https://github.com/tiann/KernelSU/releases/tag/v0.9.0), o KernelSU suporta dois modos de execução em dispositivos GKI:\r\n\r\n1. `GKI`: Substitue o kernel original do dispositivo pelo **Generic Kernel Image** (GKI) fornecido pelo KernelSU.\r\n2. `LKM`: Carregue o **Loadable Kernel Module** (LKM) no kernel do dispositivo sem substituir o kernel original.\r\n\r\nEsses dois modos são adequados para diferentes cenários, e você pode escolher o mais adequado conforme suas necessidades.\r\n\r\n### Modo GKI {#gki-mode}\r\n\r\nNo modo GKI, o kernel original do dispositivo será substituído pela imagem genérica do kernel fornecida pelo KernelSU. As vantagens do modo GKI são:\r\n\r\n1. Forte universalidade, adequada para a maioria dos dispositivos. Por exemplo, a Samsung ativou dispositivos KNOX, e o modo LKM não pode funcionar. Existem também alguns dispositivos modificados de nicho que só podem usar o modo GKI.\r\n2. Pode ser usado sem depender de firmware oficial, e não há necessidade de esperar por atualizações oficiais de firmware, desde que o KMI seja consistente, ele pode ser usado.\r\n\r\n### Modo LKM {#lkm-mode}\r\n\r\nNo modo LKM, o kernel original do dispositivo não será substituído, mas o módulo do kernel carregável será carregado no kernel do dispositivo. As vantagens do modo LKM são:\r\n\r\n1. Não substituirá o kernel original do dispositivo. Se você tiver os requisitos especiais para o kernel original do dispositivo ou quiser usar o KernelSU enquanto usa um kernel de terceiros, poderá usar o modo LKM.\r\n2. É mais conveniente atualizar o OTA. Ao atualizar o KernelSU, você pode instalá-lo diretamente no gerenciador sem flashar manualmente. Após o sistema OTA, você pode instalá-lo diretamente no segundo slot sem flashar manualmente.\r\n3. Adequado para alguns cenários especiais. Por exemplo, o LKM também pode ser carregado com privilégios root temporários. Como não é necessário substituir a partição boot, ele não acionará o AVB e não causará o bloqueio do dispositivo.\r\n4. O LKM pode ser desinstalado temporariamente. Se você deseja desativar temporariamente o acesso root, você pode desinstalar o LKM. Este processo não requer o flash de partições, nem mesmo a reinicialização do dispositivo. Se quiser ativar o root novamente, basta reiniciar o dispositivo.\r\n\r\n::: tip COEXISTÊNCIA DE DOIS MODOS\r\nApós abrir o gerenciador, você pode ver o modo atual do dispositivo na página inicial. Observe que a prioridade do modo GKI é maior que a do LKM. Por exemplo, se você usar o kernel GKI para substituir o kernel original e usar LKM para corrigir o kernel GKI, o LKM será ignorado e o dispositivo sempre será executado no modo GKI.\r\n:::\r\n\r\n### Qual escolher? {#which-one}\r\n\r\nSe o seu aparelho for um celular, recomendamos que você priorize o modo LKM. Se o seu dispositivo for um emulador, WSA ou Waydroid, recomendamos que você priorize o modo GKI.\r\n\r\n## Instalação do LKM\r\n\r\n### Obtenha o firmware oficial\r\n\r\nPara usar o modo LKM, você precisa obter o firmware oficial e corrigi-lo com base no firmware oficial. Se você usar um kernel de terceiros, poderá usar o `boot.img` do kernel de terceiros como firmware oficial.\r\n\r\nExistem muitas maneiras de obter o firmware oficial. Se o seu dispositivo suportar `fastboot boot`, então recomendamos **o método mais simples e indicado**, que consiste em usar `fastboot boot` para inicializar temporariamente o kernel GKI fornecido pelo KernelSU, depois instalar o gerenciador e, finalmente, instalá-lo diretamente pelo gerenciador. Este método não exige o download manual do firmware oficial nem a extração manual do boot.\r\n\r\nSe o seu dispositivo não suportar `fastboot boot`, pode ser necessário baixar manualmente o pacote de firmware oficial e extrair o boot dele.\r\n\r\nAo contrário do modo GKI, o modo LKM modifica o `ramdisk`. Portanto, em dispositivos com Android 13, ele precisa corrigir a partição `init_boot` em vez da partição `boot`, enquanto o modo GKI sempre opera sobre a partição `boot`.\r\n\r\n### Use o gerenciador\r\n\r\nAbra o gerenciador, clique no ícone de instalação no canto superior direito e diversas opções aparecerão:\r\n\r\n1. Selecione um arquivo. Se o seu dispositivo não tiver privilégios root, você pode escolher esta opção e, em seguida, selecionar o seu firmware oficial. O gerenciador corrigirá automaticamente o firmware. Após isso, basta fazer o flash deste arquivo corrigido para obter privilégios root permanentemente.\r\n2. Instalação direta. Se o seu dispositivo já estiver rooteado, você pode escolher esta opção. O gerenciador obterá automaticamente as informações do seu dispositivo, corrigirá o firmware oficial e realizará o flash automaticamente. Você também pode usar o comando `fastboot boot` junto com o kernel GKI do KernelSU para obter root temporário e instalar o gerenciador, e então usar esta opção. Esta também é a principal forma de atualizar o KernelSU.\r\n3. Instalar no slot inativo. Se o seu dispositivo suportar partição A/B, você pode escolher esta opção. O gerenciador corrigirá automaticamente o firmware oficial e o instalará em outra partição. Esse método é adequado para dispositivos após o OTA, você pode instalá-lo diretamente em outra partição após o OTA e, em seguida, reiniciar o dispositivo.\r\n\r\n### Use a linha de comando\r\n\r\nSe não quiser usar o gerenciador, você também pode usar a linha de comando para instalar o LKM. A ferramenta `ksud` fornecida pelo KernelSU pode ajudá-lo a corrigir rapidamente o firmware oficial e depois fazer o flash.\r\n\r\nEsta ferramenta oferece suporte ao macOS, Linux e Windows. Você pode baixar a versão correspondente em [GitHub Release](https://github.com/tiann/KernelSU/releases).\r\n\r\nUso: `ksud boot-patch` você pode verificar a ajuda da linha de comando para opções específicas.\r\n\r\n```sh\r\noriole:/ # ksud boot-patch -h\r\nPatch boot ou imagens init_boot para aplicar o KernelSU\r\n\r\nUso: ksud boot-patch [OPTIONS]\r\n\r\nOpções:\r\n  -b, --boot <BOOT>              Caminho da imagem boot. Se não especificado, tentará encontrar a imagem boot automaticamente\r\n  -k, --kernel <KERNEL>          Caminho da imagem do kernel a ser substituída\r\n  -m, --module <MODULE>          Caminho do módulo LKM a ser substituído. Se não especificado, usará o módulo integrado\r\n  -i, --init <INIT>              init a ser substituído\r\n  -u, --ota                      Usará outro slot se a imagem boot não for especificada\r\n  -f, --flash                    Flash para a partição boot após o patch\r\n  -o, --out <OUT>                Caminho de saída. Se não especificado, usará o diretório atual\r\n      --magiskboot <MAGISKBOOT>  Caminho do magiskboot. Se não especificado, usará a versão integrada\r\n      --kmi <KMI>                Versão do KMI. Se especificada, usará o KMI indicado\r\n  -h, --help                     Imprimir ajuda\r\n```\r\n\r\nAlgumas opções que precisam ser explicadas:\r\n\r\n1. A opção `--magiskboot` pode especificar o caminho do magiskboot. Se não for especificado, o ksud irá procurá-lo nas variáveis ​​de ambiente. Se você não souber como obter o magiskboot, você pode verificar [aqui](#patch-boot-image).\r\n2. A opção `--kmi` pode especificar a versão do `KMI`. Se o nome do kernel do seu dispositivo não seguir a especificação KMI, você poderá especificá-lo através desta opção.\r\n\r\nO uso mais comum é:\r\n\r\n```sh\r\nksud boot-patch -b <boot.img> --kmi android13-5.10\r\n```\r\n\r\n## Instalação no modo GKI\r\n\r\nExistem vários métodos de instalação para o modo GKI, cada um adequado para um cenário diferente, portanto escolha conforme necessário.\r\n\r\n1. Instalar com fastboot usando o boot.img fornecido pelo KernelSU.\r\n2. Instalar com um app kernel flash, como o [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases).\r\n3. Corrigir manualmente o boot.img e instalá-lo.\r\n4. Instalar com Recovery personalizado (por exemplo, TWRP).\r\n\r\n## Instalar com o boot.img fornecido pelo KernelSU\r\n\r\nSe o `boot.img` do seu dispositivo usa um formato de compactação comumente usado, você pode usar as imagens GKI fornecidas pelo KernelSU para atualizá-lo diretamente. Não requer TWRP ou autocorreção da imagem.\r\n\r\n### Encontre o boot.img adequado\r\n\r\nO KernelSU fornece um boot.img genérico para dispositivos GKI, e você deve fazer o flash do boot.img na partição boot do dispositivo.\r\n\r\nVocê pode baixar o boot.img em [GitHub Release](https://github.com/tiann/KernelSU/releases). Por favor, observe que você deve usar a versão correta do boot.img. Se você não sabe qual arquivo baixar, leia atentamente a descrição do [KMI](#kmi) e [Nível do patch de segurança](#security-patch-level) neste documento.\r\n\r\nNormalmente, existem três arquivos de inicialização em formatos diferentes para o mesmo KMI e nível de patch de segurança. Eles são idênticos, exceto pelo formato de compactação do kernel. Por favor, verifique o formato de compactação do kernel de seu boot.img original. Você deve usar o formato correto, como `lz4` ou `gz`. Se você usar um formato de compactação incorreto, poderá encontrar bootloop após o flash do boot.img.\r\n\r\n::: info FORMATO DE COMPACTAÇÃO DO BOOT.IMG\r\n1. Você pode usar o magiskboot para obter o formato de compactação do seu boot.img original. Alternativamente, você também pode perguntar a membros ou desenvolvedores da comunidade que possuam o mesmo modelo de dispositivo. Além disso, o formato de compactação do kernel geralmente não muda, portanto, se você inicializar com êxito com um determinado formato de compactação, poderá tentar esse formato mais tarde.\r\n2. Dispositivos Xiaomi geralmente usam `gz` ou `uncompressed`.\r\n3. Para dispositivos Pixel, siga as instruções abaixo:\r\n:::\r\n\r\n### Flash o boot.img para o dispositivo\r\n\r\nUse o `adb` para conectar seu dispositivo, execute `adb reboot bootloader` para entrar no modo fastboot e use este comando para flashar o KernelSU:\r\n\r\n```sh\r\nfastboot flash boot boot.img\r\n```\r\n\r\n::: info INFORMAÇÕES\r\nSe o seu dispositivo suportar `fastboot boot`, você pode usar primeiro `fastboot boot boot.img` para tentar usar o boot.img para inicializar o sistema primeiro. Se algo inesperado acontecer, reinicie-o novamente para inicializar.\r\n:::\r\n\r\n### Reiniciar\r\n\r\nApós a conclusão do flash, você deve reiniciar o dispositivo:\r\n\r\n```sh\r\nfastboot reboot\r\n```\r\n\r\n## Instalar com Kernel Flasher\r\n\r\nEtapa:\r\n\r\n1. Baixe o ZIP AnyKernel3. Se você não sabe qual arquivo baixar, leia atentamente a descrição do [KMI](#kmi) e [Nível do patch de segurança](#security-patch-level) neste documento.\r\n2. Abra o app Kernel Flasher, conceda as permissões de root necessárias e use o ZIP AnyKernel3 fornecido para fazer o flash.\r\n\r\nDessa forma, é necessário que o app Kernel Flasher tenha privilégios root. Você pode usar os seguintes métodos para conseguir isso:\r\n\r\n1. Seu dispositivo está rooteado. Por exemplo, você instalou o KernelSU e deseja atualizar para a versão mais recente ou fez o root por meio de outros métodos (como Magisk).\r\n2. Se o seu dispositivo não estiver rooteado, mas suportar o método de inicialização temporária como `fastboot boot boot.img`, você pode usar a imagem GKI fornecida pelo KernelSU para inicializar temporariamente o seu dispositivo, obter privilégios root temporário e, em seguida, usar o Kernel Flasher para obter privilégios root permanente.\r\n\r\nAqui estão alguns apps que podem ser usados para realizar o flash do kernel:\r\n\r\n1. [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases)\r\n2. [Franco Kernel Manager](https://play.google.com/store/apps/details?id=com.franco.kernel)\r\n3. [Ex Kernel Manager](https://play.google.com/store/apps/details?id=flar2.exkernelmanager)\r\n\r\nObservação: Este método é mais conveniente ao atualizar o KernelSU e pode ser feito sem um computador (faça um backup primeiro).\r\n\r\n## Corrigir boot.img manualmente {#patch-boot-image}\r\n\r\nPara alguns dispositivos, o formato boot.img não é tão comum como `lz4`, `gz` e `uncompressed`. Um exemplo típico é o Pixel, cujo boot.img é compactado no formato `lz4_legacy`, enquanto o ramdisk pode estar em `gz` ou também comprimido em `lz4_legacy`. Atualmente, se você flashar diretamente o boot.img fornecido pelo KernelSU, o dispositivo pode não conseguir inicializar. Nesse caso, é necessário corrigir manualmente o boot.img para conseguir isso.\r\n\r\nÉ sempre recomendado usar `magiskboot` para corrigir imagens, existem duas maneiras:\r\n\r\n1. [magiskboot](https://github.com/topjohnwu/Magisk/releases)\r\n2. [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci)\r\n\r\nA versão oficial do `magiskboot` só pode ser executada em dispositivos Android, se você quiser rodar no PC, você pode tentar a segunda opção.\r\n\r\n::: tip DICA\r\nAndroid-Image-Kitchen não é recomendado por enquanto, porque ele não lida corretamente com os metadados de inicialização (como o nível do patch de segurança). Portanto, pode não funcionar em alguns dispositivos.\r\n:::\r\n\r\n### Preparação\r\n\r\n1. Obtenha o boot.img padrão do dispositivo. Você pode obtê-lo com os fabricantes do seu dispositivo. Talvez você precise do [payload-dumper-go](https://github.com/ssut/payload-dumper-go).\r\n2. Baixe o arquivo ZIP AnyKernel3 fornecido pelo KernelSU que corresponde à versão KMI do seu dispositivo. Você pode consultar [Instalar com Recovery personalizado](#install-with-custom-recovery).\r\n3. Descompacte o pacote AnyKernel3 e obtenha o arquivo `Image`, que é o arquivo do kernel do KernelSU.\r\n\r\n### Usando o magiskboot em dispositivos Android {#using-magiskboot-on-Android-devices}\r\n\r\n1. Baixe o Magisk mais recente em [GitHub Releases](https://github.com/topjohnwu/Magisk/releases).\r\n2. Renomeie o `Magisk-*(versão).apk` para `Magisk-*.zip` e descompacte-o.\r\n3. Envie `Magisk-*/lib/arm64-v8a/libmagiskboot.so` para o seu dispositivo por ADB: `adb push Magisk-*/lib/arm64-v8a/libmagiskboot.so /data/local/tmp/magiskboot`.\r\n4. Envie o boot.img padrão e Image em AnyKernel3 para o seu dispositivo.\r\n5. Entre no ADB shell e execute o diretório `cd /data/local/tmp/`, em seguida, `chmod +x magiskboot`.\r\n6. Entre no ADB shell e execute o diretório `cd /data/local/tmp/`, execute `./magiskboot unpack boot.img` para descompactar `boot.img`, você obterá um arquivo `kernel`, este é o seu kernel padrão.\r\n7. Substitua `kernel` por `Image` executando o comando: `mv -f Image kernel`.\r\n8. Execute `./magiskboot repack boot.img` para reembalar o boot.img, e você obterá um arquivo `new-boot.img`, faça o flash deste arquivo para o dispositivo por fastboot.\r\n\r\n### Usando o magiskboot no PC Windows/macOS/Linux {#using-magiskboot-on-PC}\r\n\r\n1. Baixe o `magiskboot` adequado para o seu sistema operacional em [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci).\r\n2. Prepare o `boot.img` padrão e `Image` em seu PC.\r\n3. Execute `chmod +x magiskboot`.\r\n4. Entre no diretório apropriado, execute `./magiskboot unpack boot.img` para descompactar `boot.img`. Você obterá um arquivo `kernel`, este é o seu kernel padrão.\r\n5. Substitua `kernel` por `Image` executando o comando: `mv -f Image kernel`.\r\n6. Execute `./magiskboot repack boot.img` para reembalar o boot.img, e você obterá um arquivo `new-boot.img`, faça o flash deste arquivo para o dispositivo por fastboot.\r\n\r\n::: info INFORMAÇÕES\r\nO `magiskboot` oficial pode executar o dispositivo `Linux` normalmente. Se você for um usuário Linux, você pode usar a versão oficial.\r\n:::\r\n\r\n## Instalar com Recovery personalizado {#install-with-custom-recovery}\r\n\r\nPré-requisito: Seu dispositivo deve ter um Recovery personalizado, como TWRP. Se não houver Recovery personalizado disponível para o seu dispositivo, use outro método.\r\n\r\nEtapas:\r\n\r\n1. Em [GitHub Releases](https://github.com/tiann/KernelSU/releases), baixe o pacote ZIP começando com AnyKernel3 que corresponde à versão do seu dispositivo. Por exemplo, a versão do kernel do dispositivo é `android12-5.10.66`, então você deve baixar o arquivo `AnyKernel3-android12-5.10.66_yyyy-MM.zip` (onde `yyyy` é o ano e `MM` é o mês).\r\n2. Reinicie o dispositivo no TWRP.\r\n3. Use o ADB para colocar AnyKernel3-*.zip no dispositivo em `/sdcard` e escolha instalá-lo na interface do TWRP, ou você pode diretamente executar `adb sideload AnyKernel-*.zip` para instalar.\r\n\r\nObservação: Este método é adequado para qualquer instalação (não limitado à instalação inicial ou atualizações subsequentes), desde que você use o TWRP.\r\n\r\n## Outros métodos\r\n\r\nNa verdade, todos esses métodos de instalação têm apenas uma ideia principal, que é **substituir o kernel original pelo fornecido pelo KernelSU**, desde que isso possa ser alcançado, ele pode ser instalado. A seguir estão outros métodos possíveis:\r\n\r\n1. Primeiro instale o Magisk, obtenha privilégios root através do Magisk e então use o Kernel Flasher para fazer o flash no ZIP AnyKernel3 do KernelSU.\r\n2. Use algum kit de ferramentas de flash em PC para flashar no kernel fornecido pelo KernelSU.\r\n\r\nNo entanto, se não funcionar, por favor, tente o método `magiskboot`.\r\n\r\n## Pós-instalação: Suporte a Módulos {#post-installation}\r\n\r\n::: warning METAMODULE PARA MODIFICAÇÃO DE ARQUIVOS DO SISTEMA\r\nSe você deseja usar módulos que modificam arquivos `/system`, você precisa instalar um **metamodule** após instalar o KernelSU. Módulos que usam apenas scripts, sepolicy ou system.prop funcionam sem um metamodule.\r\n:::\r\n\r\n**Para suporte à modificação de `/system`**, consulte o [Guia de Metamodule](metamodule.md) para:\r\n- Entender o que são metamodules e por que são necessários\r\n- Instalar o metamodule oficial `meta-overlayfs`\r\n- Conhecer outras opções de metamodule\r\n"
  },
  {
    "path": "website/docs/pt_BR/guide/metamodule.md",
    "content": "# Metamódulo\n\nMetamódulos são um recurso revolucionário no KernelSU que transfere recursos críticos do sistema de módulos do daemon principal para módulos plugáveis. Essa mudança arquitetônica mantém a estabilidade e segurança do KernelSU enquanto libera um maior potencial de inovação para o ecossistema de módulos.\n\n## O que é um Metamódulo?\n\nUm metamódulo é um tipo especial de módulo KernelSU que fornece funcionalidade de infraestrutura central para o sistema de módulos. Ao contrário dos módulos regulares que modificam arquivos do sistema, os metamódulos controlam *como* os módulos regulares são instalados e montados.\n\nMetamódulos são um mecanismo de extensão baseado em plugins que permite a personalização completa da infraestrutura de gerenciamento de módulos do KernelSU. Ao delegar a lógica de montagem e instalação aos metamódulos, o KernelSU evita se tornar um ponto de detecção frágil enquanto permite diversas estratégias de implementação.\n\n**Características principais:**\n\n- **Papel de infraestrutura**: Metamódulos fornecem serviços dos quais os módulos regulares dependem\n- **Instância única**: Apenas um metamódulo pode ser instalado por vez\n- **Execução prioritária**: Scripts de metamódulos são executados antes dos scripts de módulos regulares\n- **Hooks especiais**: Fornece três scripts de hook para instalação, montagem e limpeza\n\n## Por que Metamódulos?\n\nSoluções root tradicionais incorporam a lógica de montagem em seu núcleo, tornando-as mais fáceis de detectar e mais difíceis de evoluir. A arquitetura de metamódulos do KernelSU resolve esses problemas através da separação de preocupações.\n\n**Vantagens estratégicas:**\n\n- **Superfície de detecção reduzida**: O próprio KernelSU não realiza montagens, reduzindo vetores de detecção\n- **Estabilidade**: O daemon central permanece estável enquanto as implementações de montagem podem evoluir\n- **Inovação**: A comunidade pode desenvolver estratégias alternativas de montagem sem bifurcar o KernelSU\n- **Escolha**: Os usuários podem selecionar a implementação que melhor se adapta às suas necessidades\n\n**Flexibilidade de montagem:**\n\n- **Sem montagem**: Para usuários com módulos somente sem montagem, evite completamente a sobrecarga de montagem\n- **Montagem OverlayFS**: Abordagem tradicional com suporte a camada de leitura-escrita (via `meta-overlayfs`)\n- **Magic mount**: Montagem compatível com Magisk para melhor compatibilidade de aplicativos\n- **Implementações personalizadas**: Sobreposições baseadas em FUSE, montagens VFS personalizadas ou abordagens totalmente novas\n\n**Além da montagem:**\n\n- **Extensibilidade**: Adicione recursos como suporte a módulos do kernel sem modificar o núcleo do KernelSU\n- **Modularidade**: Atualize implementações independentemente das versões do KernelSU\n- **Personalização**: Crie soluções especializadas para dispositivos ou casos de uso específicos\n\n::: warning IMPORTANTE\nSem um metamódulo instalado, os módulos **NÃO** serão montados. Instalações novas do KernelSU requerem a instalação de um metamódulo (como `meta-overlayfs`) para que os módulos funcionem.\n:::\n\n## Para Usuários\n\n### Instalando um Metamódulo\n\nInstale um metamódulo da mesma forma que módulos regulares:\n\n1. Baixe o arquivo ZIP do metamódulo (por exemplo, `meta-overlayfs.zip`)\n2. Abra o aplicativo KernelSU Manager\n3. Toque no botão de ação flutuante (➕)\n4. Selecione o arquivo ZIP do metamódulo\n5. Reinicie seu dispositivo\n\nO metamódulo `meta-overlayfs` é a implementação de referência oficial que fornece montagem de módulos baseada em overlayfs tradicional com suporte a imagem ext4.\n\n### Verificando o Metamódulo Ativo\n\nVocê pode verificar qual metamódulo está atualmente ativo na página de Módulos do aplicativo KernelSU Manager. O metamódulo ativo será exibido na sua lista de módulos com sua designação especial.\n\n### Desinstalando um Metamódulo\n\n::: danger AVISO\nDesinstalar um metamódulo afetará **TODOS** os módulos. Após a remoção, os módulos não serão mais montados até que você instale outro metamódulo.\n:::\n\nPara desinstalar:\n\n1. Abra o KernelSU Manager\n2. Encontre o metamódulo na sua lista de módulos\n3. Toque em desinstalar (você verá um aviso especial)\n4. Confirme a ação\n5. Reinicie seu dispositivo\n\nApós desinstalar, você deve instalar outro metamódulo se quiser que os módulos continuem funcionando.\n\n### Restrição de Metamódulo Único\n\nApenas um metamódulo pode ser instalado por vez. Se você tentar instalar um segundo metamódulo, o KernelSU impedirá a instalação para evitar conflitos.\n\nPara trocar metamódulos:\n\n1. Desinstale todos os módulos regulares\n2. Desinstale o metamódulo atual\n3. Reinicie\n4. Instale o novo metamódulo\n5. Reinstale seus módulos regulares\n6. Reinicie novamente\n\n## Para Desenvolvedores de Módulos\n\nSe você está desenvolvendo módulos KernelSU regulares, não precisa se preocupar muito com metamódulos. Seus módulos funcionarão desde que os usuários tenham um metamódulo compatível (como `meta-overlayfs`) instalado.\n\n**O que você precisa saber:**\n\n- **Montagem requer um metamódulo**: O diretório `system` no seu módulo só será montado se o usuário tiver um metamódulo instalado que forneça funcionalidade de montagem\n- **Nenhuma alteração de código necessária**: Módulos existentes continuam a funcionar sem modificação\n\n::: tip\nSe você está familiarizado com o desenvolvimento de módulos Magisk, seus módulos funcionarão da mesma forma no KernelSU quando o metamódulo estiver instalado, pois ele fornece montagem compatível com Magisk.\n:::\n\n## Para Desenvolvedores de Metamódulos\n\nCriar um metamódulo permite que você personalize como o KernelSU lida com instalação de módulos, montagem e desinstalação.\n\n### Requisitos Básicos\n\nUm metamódulo é identificado por uma propriedade especial em seu `module.prop`:\n\n```txt\nid=my_metamodule\nname=My Custom Metamodule\nversion=1.0\nversionCode=1\nauthor=Your Name\ndescription=Custom module mounting implementation\nmetamodule=1\n```\n\nA propriedade `metamodule=1` (ou `metamodule=true`) marca isso como um metamódulo. Sem essa propriedade, o módulo será tratado como um módulo regular.\n\n### Estrutura de Arquivos\n\nEstrutura de um metamódulo:\n\n```txt\nmy_metamodule/\n├── module.prop              (deve incluir metamodule=1)\n│\n│      *** Hooks específicos de metamódulo ***\n├── metamount.sh             (opcional: manipulador de montagem personalizado)\n├── metainstall.sh           (opcional: hook de instalação para módulos regulares)\n├── metauninstall.sh         (opcional: hook de limpeza para módulos regulares)\n│\n│      *** Arquivos de módulo padrão (todos opcionais) ***\n├── customize.sh             (personalização de instalação)\n├── post-fs-data.sh          (script de estágio post-fs-data)\n├── service.sh               (script late_start service)\n├── boot-completed.sh        (script de inicialização concluída)\n├── uninstall.sh             (script de desinstalação do próprio metamódulo)\n├── system/                  (modificações systemless, se necessário)\n└── [quaisquer arquivos adicionais]\n```\n\nMetamódulos podem usar todos os recursos de módulos padrão (scripts de ciclo de vida, etc.) além de seus hooks especiais de metamódulo.\n\n### Scripts de Hook\n\nMetamódulos podem fornecer até três scripts de hook especiais:\n\n#### 1. metamount.sh - Manipulador de Montagem\n\n**Propósito**: Controla como os módulos são montados durante a inicialização.\n\n**Quando executado**: Durante o estágio `post-fs-data`, antes de qualquer script de módulo ser executado.\n\n**Variáveis de ambiente:**\n\n- `MODDIR`: O caminho do diretório do metamódulo (por exemplo, `/data/adb/modules/my_metamodule`)\n- Todas as variáveis de ambiente padrão do KernelSU\n\n**Responsabilidades:**\n\n- Montar todos os módulos habilitados de forma systemless\n- Verificar flags `skip_mount`\n- Lidar com requisitos específicos de montagem de módulos\n\n::: danger REQUISITO CRÍTICO\nAo realizar operações de montagem, você **DEVE** definir o nome da origem/dispositivo como `\"KSU\"`. Isso identifica as montagens como pertencentes ao KernelSU.\n\n**Exemplo (correto):**\n\n```sh\nmount -t overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work KSU /target\n```\n\n**Para APIs de montagem modernas**, defina a string de origem:\n\n```rust\nfsconfig_set_string(fs, \"source\", \"KSU\")?;\n```\n\nIsso é essencial para o KernelSU identificar e gerenciar adequadamente suas montagens.\n:::\n\n**Script de exemplo:**\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\n\n# Exemplo: Implementação simples de bind mount\nfor module in /data/adb/modules/*; do\n    if [ -f \"$module/disable\" ] || [ -f \"$module/skip_mount\" ]; then\n        continue\n    fi\n\n    if [ -d \"$module/system\" ]; then\n        # Monte com source=KSU (OBRIGATÓRIO!)\n        mount -o bind,dev=KSU \"$module/system\" /system\n    fi\ndone\n```\n\n#### 2. metainstall.sh - Hook de Instalação\n\n**Propósito**: Personalizar como módulos regulares são instalados.\n\n**Quando executado**: Durante a instalação do módulo, após a extração dos arquivos, mas antes da conclusão da instalação. Este script é **executado por source** (não executado) pelo instalador embutido, semelhante a como `customize.sh` funciona.\n\n**Variáveis de ambiente e funções:**\n\nEste script herda todas as variáveis e funções do `install.sh` embutido:\n\n- **Variáveis**: `MODPATH`, `TMPDIR`, `ZIPFILE`, `ARCH`, `API`, `IS64BIT`, `KSU`, `KSU_VER`, `KSU_VER_CODE`, `BOOTMODE`, etc.\n- **Funções**:\n  - `ui_print <msg>` - Imprime mensagem no console\n  - `abort <msg>` - Imprime erro e encerra a instalação\n  - `set_perm <target> <owner> <group> <permission> [context]` - Define permissões de arquivo\n  - `set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]` - Define permissões recursivamente\n  - `install_module` - Chama o processo de instalação de módulo embutido\n\n**Casos de uso:**\n\n- Processar arquivos de módulo antes ou depois da instalação embutida (chame `install_module` quando estiver pronto)\n- Mover arquivos de módulo\n- Validar compatibilidade de módulo\n- Configurar estruturas de diretório especiais\n- Inicializar recursos específicos do módulo\n\n**Nota**: Este script **NÃO** é chamado ao instalar o próprio metamódulo.\n\n#### 3. metauninstall.sh - Hook de Limpeza\n\n**Propósito**: Limpar recursos quando módulos regulares são desinstalados.\n\n**Quando executado**: Durante a desinstalação do módulo, antes do diretório do módulo ser removido.\n\n**Variáveis de ambiente:**\n\n- `MODULE_ID`: O ID do módulo sendo desinstalado\n\n**Casos de uso:**\n\n- Processar arquivos\n- Limpar symlinks\n- Liberar recursos alocados\n- Atualizar rastreamento interno\n\n**Script de exemplo:**\n\n```sh\n#!/system/bin/sh\n# Chamado ao desinstalar módulos regulares\nMODULE_ID=\"$1\"\nIMG_MNT=\"/data/adb/metamodule/mnt\"\n\n# Remover arquivos do módulo da imagem\nif [ -d \"$IMG_MNT/$MODULE_ID\" ]; then\n    rm -rf \"$IMG_MNT/$MODULE_ID\"\nfi\n```\n\n### Ordem de Execução\n\nEntender a ordem de execução da inicialização é crucial para o desenvolvimento de metamódulos:\n\n```txt\nestágio post-fs-data:\n  1. Scripts comuns post-fs-data.d são executados\n  2. Limpar módulos, restorecon, carregar sepolicy.rule\n  3. post-fs-data.sh do metamódulo é executado (se existir)\n  4. post-fs-data.sh dos módulos regulares são executados\n  5. Carregar system.prop\n  6. metamount.sh do metamódulo é executado\n     └─> Monta todos os módulos de forma systemless\n  7. Estágio post-mount.d é executado\n     - Scripts comuns post-mount.d\n     - post-mount.sh do metamódulo (se existir)\n     - post-mount.sh dos módulos regulares\n\nestágio service:\n  1. Scripts comuns service.d são executados\n  2. service.sh do metamódulo é executado (se existir)\n  3. service.sh dos módulos regulares são executados\n\nestágio boot-completed:\n  1. Scripts comuns boot-completed.d são executados\n  2. boot-completed.sh do metamódulo é executado (se existir)\n  3. boot-completed.sh dos módulos regulares são executados\n```\n\n**Pontos-chave:**\n\n- `metamount.sh` é executado **APÓS** todos os scripts post-fs-data (tanto metamódulo quanto módulos regulares)\n- Scripts de ciclo de vida do metamódulo (`post-fs-data.sh`, `service.sh`, `boot-completed.sh`) sempre são executados antes dos scripts de módulos regulares\n- Scripts comuns em diretórios `.d` são executados antes dos scripts de metamódulo\n- O estágio `post-mount` é executado após a conclusão da montagem\n\n### Mecanismo de Symlink\n\nQuando um metamódulo é instalado, o KernelSU cria um symlink:\n\n```sh\n/data/adb/metamodule -> /data/adb/modules/<metamodule_id>\n```\n\nIsso fornece um caminho estável para acessar o metamódulo ativo, independentemente de seu ID.\n\n**Benefícios:**\n\n- Caminho de acesso consistente\n- Detecção fácil do metamódulo ativo\n- Simplifica a configuração\n\n### Exemplo do Mundo Real: meta-overlayfs\n\nO metamódulo `meta-overlayfs` é a implementação de referência oficial. Ele demonstra as melhores práticas para desenvolvimento de metamódulos.\n\n#### Arquitetura\n\n`meta-overlayfs` usa uma **arquitetura de diretório duplo**:\n\n1. **Diretório de metadados**: `/data/adb/modules/`\n   - Contém `module.prop`, `disable`, marcadores `skip_mount`\n   - Rápido para escanear durante a inicialização\n   - Pegada de armazenamento pequena\n\n2. **Diretório de conteúdo**: `/data/adb/metamodule/mnt/`\n   - Contém arquivos reais do módulo (system, vendor, product, etc.)\n   - Armazenado em uma imagem ext4 (`modules.img`)\n   - Otimizado de espaço com recursos ext4\n\n#### Implementação do metamount.sh\n\nVeja como `meta-overlayfs` implementa o manipulador de montagem:\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\nIMG_FILE=\"$MODDIR/modules.img\"\nMNT_DIR=\"$MODDIR/mnt\"\n\n# Montar imagem ext4 se ainda não estiver montada\nif ! mountpoint -q \"$MNT_DIR\"; then\n    mkdir -p \"$MNT_DIR\"\n    mount -t ext4 -o loop,rw,noatime \"$IMG_FILE\" \"$MNT_DIR\"\nfi\n\n# Definir variáveis de ambiente para suporte de diretório duplo\nexport MODULE_METADATA_DIR=\"/data/adb/modules\"\nexport MODULE_CONTENT_DIR=\"$MNT_DIR\"\n\n# Executar binário de montagem\n# (A lógica de montagem real está em um binário Rust)\n\"$MODDIR/meta-overlayfs\"\n```\n\n#### Recursos Principais\n\n**Montagem Overlayfs:**\n\n- Usa overlayfs do kernel para modificações systemless verdadeiras\n- Suporta múltiplas partições (system, vendor, product, system_ext, odm, oem)\n- Suporte a camada de leitura-escrita via `/data/adb/modules/.rw/`\n\n**Identificação de origem:**\n\n```rust\n// De meta-overlayfs/src/mount.rs\nfsconfig_set_string(fs, \"source\", \"KSU\")?;  // OBRIGATÓRIO!\n```\n\nIsso define `dev=KSU` para todas as montagens overlay, permitindo identificação adequada.\n\n### Melhores Práticas\n\nAo desenvolver metamódulos:\n\n1. **Sempre defina a origem como \"KSU\"** para operações de montagem - umount do kernel e umount do zygisksu precisam disso para desmontar corretamente\n2. **Trate erros graciosamente** - processos de inicialização são sensíveis ao tempo\n3. **Respeite flags padrão** - suporte `skip_mount` e `disable`\n4. **Registre operações** - use `echo` ou logging para depuração\n5. **Teste minuciosamente** - erros de montagem podem causar boot loops\n6. **Documente o comportamento** - explique claramente o que seu metamódulo faz\n7. **Forneça caminhos de migração** - ajude os usuários a mudar de outras soluções\n\n### Testando Seu Metamódulo\n\nAntes de lançar:\n\n1. **Teste a instalação** em uma configuração limpa do KernelSU\n2. **Verifique a montagem** com vários tipos de módulos\n3. **Verifique a compatibilidade** com módulos comuns\n4. **Teste a desinstalação** e limpeza\n5. **Valide o desempenho de inicialização** (metamount.sh está bloqueando!)\n6. **Garanta o tratamento adequado de erros** para evitar boot loops\n\n## Perguntas Frequentes\n\n### Eu preciso de um metamódulo?\n\n**Para usuários**: Apenas se você quiser usar módulos que requerem montagem. Se você usa apenas módulos que executam scripts sem modificar arquivos do sistema, não precisa de um metamódulo.\n\n**Para desenvolvedores de módulos**: Não, você desenvolve módulos normalmente. Os usuários precisam de um metamódulo apenas se seu módulo requer montagem.\n\n**Para usuários avançados**: Apenas se você quiser personalizar o comportamento de montagem ou criar implementações alternativas de montagem.\n\n### Posso ter vários metamódulos?\n\nNão. Apenas um metamódulo pode ser instalado por vez. Isso evita conflitos e garante comportamento previsível.\n\n### O que acontece se eu desinstalar meu único metamódulo?\n\nOs módulos não serão mais montados. Seu dispositivo inicializará normalmente, mas as modificações dos módulos não serão aplicadas até que você instale outro metamódulo.\n\n### O meta-overlayfs é obrigatório?\n\nNão. Ele fornece montagem overlayfs padrão compatível com a maioria dos módulos. Você pode criar seu próprio metamódulo se precisar de comportamento diferente.\n\n## Veja Também\n\n- [Guia de Módulos](module.md) - Desenvolvimento geral de módulos\n- [Diferença com Magisk](difference-with-magisk.md) - Comparando KernelSU e Magisk\n- [Como Compilar](how-to-build.md) - Compilando KernelSU a partir do código-fonte\n"
  },
  {
    "path": "website/docs/pt_BR/guide/module-config.md",
    "content": "# Configuração de Módulo\n\nO KernelSU fornece um sistema de configuração integrado que permite que os módulos armazenem configurações de chave-valor persistentes ou temporárias. As configurações são armazenadas em formato binário em `/data/adb/ksu/module_configs/<module_id>/` com as seguintes características:\n\n## Tipos de Configuração\n\n- **Configuração Persistente** (`persist.config`): Sobrevive às reinicializações até ser explicitamente excluída ou o módulo ser desinstalado\n- **Configuração Temporária** (`tmp.config`): Automaticamente limpa durante o estágio post-fs-data em cada inicialização\n\nAo ler configurações, os valores temporários têm prioridade sobre os valores persistentes para a mesma chave.\n\n## Usando Configuração em Scripts de Módulo\n\nTodos os scripts de módulo (`post-fs-data.sh`, `service.sh`, `boot-completed.sh`, etc.) são executados com a variável de ambiente `KSU_MODULE` definida como o ID do módulo. Você pode usar os comandos `ksud module config` para gerenciar a configuração do seu módulo:\n\n```bash\n# Obter um valor de configuração\nvalue=$(ksud module config get my_setting)\n\n# Definir um valor de configuração persistente\nksud module config set my_setting \"some value\"\n\n# Definir um valor de configuração temporário (limpo após a reinicialização)\nksud module config set --temp runtime_state \"active\"\n\n# Definir valor a partir de stdin (útil para texto multilinhas ou dados complexos)\nksud module config set my_key <<EOF\ntexto multilinhas\nvalor\nEOF\n\n# Ou transmitir de um comando\necho \"value\" | ksud module config set my_key\n\n# Sinalizador stdin explícito\ncat file.json | ksud module config set json_data --stdin\n\n# Listar todas as entradas de configuração (mesclando persistentes e temporárias)\nksud module config list\n\n# Excluir uma entrada de configuração\nksud module config delete my_setting\n\n# Excluir uma entrada de configuração temporária\nksud module config delete --temp runtime_state\n\n# Limpar todas as configurações persistentes\nksud module config clear\n\n# Limpar todas as configurações temporárias\nksud module config clear --temp\n```\n\n## Limites de Validação\n\nO sistema de configuração impõe os seguintes limites:\n\n- **Comprimento máximo da chave**: 256 bytes\n- **Comprimento máximo do valor**: 1MB (1048576 bytes)\n- **Número máximo de entradas de configuração**: 32 por módulo\n- **Formato de chave**: Deve corresponder a `^[a-zA-Z][a-zA-Z0-9._-]+$` (como ID do módulo)\n  - Deve começar com uma letra\n  - Pode conter letras, números, pontos, sublinhados ou hífens\n  - Comprimento mínimo: 2 caracteres\n- **Formato de valor**: Sem restrições - pode conter qualquer caractere UTF-8, incluindo quebras de linha e caracteres de controle\n  - Armazenado em formato binário com prefixo de comprimento para manuseio seguro de todos os dados\n\n## Ciclo de Vida\n\n- **Na inicialização**: Todas as configurações temporárias são limpas durante o estágio post-fs-data\n- **Na desinstalação do módulo**: Todas as configurações (persistentes e temporárias) são automaticamente removidas\n- As configurações são armazenadas em formato binário com número mágico `0x4b53554d` (\"KSUM\") e validação de versão\n\n## Casos de Uso\n\nO sistema de configuração é ideal para:\n\n- **Preferências do usuário**: Armazenar configurações de módulo que os usuários configuram por meio de WebUI ou scripts de ação\n- **Sinalizadores de recursos**: Ativar/desativar recursos do módulo sem reinstalar\n- **Estado de execução**: Rastrear estado temporário que deve ser redefinido na reinicialização (use configuração temporária)\n- **Configurações de instalação**: Lembrar escolhas feitas durante a instalação do módulo\n- **Dados complexos**: Armazenar JSON, texto multilinha, dados codificados em Base64 ou qualquer conteúdo estruturado (até 1MB)\n\n::: tip MELHORES PRÁTICAS\n- Use configurações persistentes para preferências do usuário que devem sobreviver às reinicializações\n- Use configurações temporárias para estado de execução ou sinalizadores de recursos que devem ser redefinidos na inicialização\n- Valide os valores de configuração em seus scripts antes de usá-los\n- Use o comando `ksud module config list` para depurar problemas de configuração\n:::\n\n## Recursos Avançados\n\nO sistema de configuração de módulos fornece chaves de configuração especiais para casos de uso avançados:\n\n### Substituindo a Descrição do Módulo {#overriding-module-description}\n\nVocê pode substituir dinamicamente o campo `description` do `module.prop` definindo a chave de configuração `override.description`:\n\n```bash\n# Substituir a descrição do módulo\nksud module config set override.description \"Descrição personalizada exibida no gerenciador\"\n```\n\nAo recuperar a lista de módulos, se a configuração `override.description` existir, ela substituirá a descrição original do `module.prop`. Isso é útil para:\n- Exibir informações dinâmicas de status na descrição do módulo\n- Mostrar detalhes de configuração em tempo de execução aos usuários\n- Atualizar a descrição com base no estado do módulo sem reinstalar\n\n### Declarando Recursos Gerenciados\n\nOs módulos podem declarar quais recursos do KernelSU eles gerenciam usando o padrão de configuração `manage.<feature>`. Os recursos suportados correspondem ao enum interno `FeatureId` do KernelSU:\n\n**Recursos Suportados:**\n- `su_compat` - Modo de compatibilidade SU\n- `kernel_umount` - Desmontagem automática do kernel\n\n```bash\n# Declarar que este módulo gerencia a compatibilidade SU e a habilita\nksud module config set manage.su_compat true\n\n# Declarar que este módulo gerencia a desmontagem do kernel e a desabilita\nksud module config set manage.kernel_umount false\n\n# Remover gerenciamento de recurso (o módulo não controla mais este recurso)\nksud module config delete manage.su_compat\n```\n\n**Como funciona:**\n- A presença de uma chave `manage.<feature>` indica que o módulo está gerenciando esse recurso\n- O valor indica o estado desejado: `true`/`1` para habilitado, `false`/`0` (ou qualquer outro valor) para desabilitado\n- Para parar de gerenciar um recurso, exclua completamente a chave de configuração\n\nOs recursos gerenciados são expostos através da API de lista de módulos como um campo `managedFeatures` (string separada por vírgulas). Isso permite:\n- O gerenciador do KernelSU detectar quais módulos gerenciam quais recursos do KernelSU\n- Prevenção de conflitos quando vários módulos tentam gerenciar o mesmo recurso\n- Melhor coordenação entre módulos e funcionalidade central do KernelSU\n\n::: warning APENAS RECURSOS SUPORTADOS\nUse apenas os nomes de recursos predefinidos listados acima (`su_compat`, `kernel_umount`). Eles correspondem aos recursos internos reais do KernelSU. Usar outros nomes de recursos não causará erros, mas não terá nenhum propósito funcional.\n:::\n"
  },
  {
    "path": "website/docs/pt_BR/guide/module-webui.md",
    "content": "# Módulo WebUI\n\nAlém de executar scripts de inicialização e modificar arquivos do sistema, os módulos do KernelSU também suportam a exibição de interfaces da UI e à interação direta com os usuários.\n\nO módulo pode escrever páginas HTML + CSS + JavaScript através de qualquer tecnologia web. O gerenciador do KernelSU exibirá essas páginas através do WebView. Ele também fornece algumas APIs para interagir com o sistema, como executar comandos shell.\n\n## Diretório `webroot`\n\nOs arquivos de recursos da web devem ser colocados no subdiretório `webroot` do diretório raiz do módulo, e **DEVE** haver um arquivo chamado `index.html`, que é a entrada da página do módulo. A estrutura do módulo mais simples contendo uma interface web é a seguinte:\n\n```txt\n❯ tree .\n.\n|-- module.prop\n`-- webroot\n    `-- index.html\n```\n\n::: warning AVISO\nAo instalar o módulo, KernelSU definirá automaticamente as permissões e o contexto do SELinux deste diretório. Se você não sabe o que está fazendo, não defina você mesmo as permissões deste diretório!\n:::\n\nSe sua página contém CSS e JavaScript, você também precisa colocá-la neste diretório.\n\n## API JavaScript\n\nSe for apenas uma página de exibição, ela funcionará como uma página web comum. No entanto, o mais importante é que o KernelSU oferece uma série de APIs de sistema, permitindo a implementação de funções exclusivas do módulo.\n\nO KernelSU disponibiliza uma biblioteca JavaScript, que está publicada no [npm](https://www.npmjs.com/package/kernelsu) e pode ser usada no código JavaScript das suas páginas web.\n\nPor exemplo, você pode executar um comando shell para obter uma configuração específica ou modificar uma propriedade:\n\n```JavaScript\nimport { exec } from 'kernelsu';\n\nconst { errno, stdout } = exec(\"getprop ro.product.model\");\n```\n\nPara outro exemplo, você pode fazer com que a página web seja exibida em tela inteira ou exibir um dica.\n\n[Documentação da API](https://www.npmjs.com/package/kernelsu)\n\nSe você achar que a API existente não atende às suas necessidades ou é inconveniente de usar, fique à vontade para nos dar sugestões [aqui](https://github.com/tiann/KernelSU/issues)!\n\n## Algumas dicas\n\n1. Você pode usar `localStorage` normalmente para armazenar alguns dados, mas tenha em mente que eles serão perdidos caso o app gerenciador seja desinstalado. Se precisar de armazenamento persistente, será necessário gravar os dados manualmente em algum diretório.\n2. Para páginas simples, recomendamos o uso do [parceljs](https://parceljs.org/) para empacotamento. Ele não exige configuração inicial e é extremamente prático de usar. No entanto, se você é um especialista em front-end ou possui suas próprias preferências, sinta-se à vontade para usar a ferramenta de sua escolha!\n"
  },
  {
    "path": "website/docs/pt_BR/guide/module.md",
    "content": "# Guias de módulo\r\n\r\nO KernelSU fornece um mecanismo de módulo que consegue modificar o diretório do sistema enquanto mantém a integridade da partição do sistema. Esse mecanismo é conhecido como \"sem sistema\".\r\n\r\nO mecanismo de módulos do KernelSU é quase o mesmo do Magisk. Se você já está familiarizado com o desenvolvimento de módulos Magisk, o desenvolvimento de módulos KernelSU é muito semelhante. Você pode pular a introdução dos módulos abaixo e só precisa ler [Diferenças com Magisk](difference-with-magisk.md).\r\n\r\n::: warning METAMODULE NECESSÁRIO APENAS PARA MODIFICAÇÃO DE ARQUIVOS DO SISTEMA\r\nKernelSU usa uma arquitetura [metamodule](metamodule.md) para montar o diretório `system`. **Somente se seu módulo precisar modificar arquivos `/system`** (via diretório `system`), você precisa instalar um metamodule (como [meta-overlayfs](https://github.com/tiann/KernelSU/releases)). Outros recursos de módulos como scripts, regras sepolicy e system.prop funcionam sem um metamodule.\r\n:::\r\n\r\n## WebUI\r\n\r\nOs módulos do KernelSU suportam a exibição de interfaces e a interação com os usuários. Para mais detalhes, consulte a [documentação do WebUI](module-webui.md).\r\n\r\n## Configuração de Módulo\r\n\r\nO KernelSU fornece um sistema de configuração integrado que permite que os módulos armazenem configurações de chave-valor persistentes ou temporárias. Para mais detalhes, consulte a [documentação de Configuração de Módulo](module-config.md).\r\n\r\n## BusyBox\r\n\r\nO KernelSU vem com um recurso binário BusyBox completo (incluindo suporte completo ao SELinux). O executável está localizado em `/data/adb/ksu/bin/busybox`. O BusyBox do KernelSU suporta \"ASH Standalone Shell Mode\" alternável em tempo de execução. O que este Modo Autônomo significa é que ao executar no shell `ash` do BusyBox, cada comando usará diretamente o miniaplicativo dentro do BusyBox, independentemente do que estiver definido em `PATH`. Por exemplo, comandos como `ls`, `rm`, `chmod` **NÃO** usarão o que está em `PATH` (no caso do Android, por padrão será `/system/bin/ls`, `/system/bin/rm` e `/system/bin/chmod` respectivamente), mas em vez disso chamará diretamente os miniaplicativos internos do BusyBox. Isso garante que os scripts sempre sejam executados em um ambiente previsível e sempre tenham o conjunto completo de comandos, independentemente da versão do Android em que estão sendo executados. Para forçar um comando a **NÃO** usar o BusyBox, você deve chamar o executável com caminhos completos.\r\n\r\nCada script shell executado no contexto do KernelSU será executado no shell `ash` do BusyBox com o Modo Autônomo ativado. Para o que é relevante para desenvolvedores terceirizados, isso inclui todos os scripts de inicialização e scripts de instalação de módulos.\r\n\r\nPara aqueles que desejam usar o recurso Modo Autônomo fora do KernelSU, existem 2 maneiras de ativá-los:\r\n\r\n1. Definir a variável de ambiente `ASH_STANDALONE` como `1`.<br>Exemplo: `ASH_STANDALONE=1 /data/adb/ksu/bin/busybox sh <script>`\r\n2. Alternar com opções de linha de comando:<br>`/data/adb/ksu/bin/busybox sh -o standalone <script>`\r\n\r\nPara garantir que todos os shells `sh` subsequentes executados também sejam executados no Modo Autônomo, a opção 1 é o método preferido (e é isso que o KernelSU e o gerenciador do KernelSU usam internamente), pois as variáveis ​​de ambiente são herdadas para os subprocesso.\r\n\r\n::: tip DIFERENÇAS COM MAGISK\r\nO BusyBox do KernelSU agora está usando o arquivo binário compilado diretamente do projeto Magisk. **Obrigado ao Magisk!** Portanto, você não precisa se preocupar com problemas de compatibilidade entre scripts BusyBox no Magisk e KernelSU porque eles são exatamente iguais!\r\n:::\r\n\r\n## Módulos KernelSU\r\n\r\nUm módulo KernelSU é uma pasta colocada em `/data/adb/modules` com a estrutura abaixo:\r\n\r\n```txt\r\n/data/adb/modules\r\n├── .\r\n├── .\r\n|\r\n├── $MODID                  <--- A pasta é nomeada com o ID do módulo\r\n│   │\r\n│   │      *** Identidade do módulo ***\r\n│   │\r\n│   ├── module.prop         <--- Este arquivo armazena os metadados do módulo\r\n│   │\r\n│   │      *** Conteúdo principal ***\r\n│   │\r\n│   ├── system              <--- Esta pasta será montada se skip_mount não existir\r\n│   │   ├── ...\r\n│   │   ├── ...\r\n│   │   └── ...\r\n│   │\r\n│   │      *** Sinalizadores de status ***\r\n│   │\r\n│   ├── skip_mount          <--- Se existir, o KernelSU não montará sua pasta de sistema\r\n│   ├── disable             <--- Se existir, o módulo será desativado\r\n│   ├── remove              <--- Se existir, o módulo será removido na próxima reinicialização\r\n│   │\r\n│   │      *** Arquivos opcionais ***\r\n│   │\r\n│   ├── post-fs-data.sh     <--- Este script será executado em post-fs-data\r\n│   ├── post-mount.sh       <--- Este script será executado em post-mount\r\n│   ├── service.sh          <--- Este script será executado no serviço late_start\r\n│   ├── boot-completed.sh   <--- Este script será executado na inicialização concluída\r\n|   ├── uninstall.sh        <--- Este script será executado quando o KernelSU remover seu módulo\r\n|   ├── action.sh           <--- Este script será executado quando o usuário clicar no botão Ação no KernelSU\r\n│   ├── system.prop         <--- As propriedades neste arquivo serão carregadas como propriedades do sistema por resetprop\r\n│   ├── sepolicy.rule       <--- Regras adicionais do sepolicy personalizadas\r\n│   │\r\n│   │      *** Gerado automaticamente, NÃO CRIE OU MODIFIQUE MANUALMENTE ***\r\n│   │\r\n│   ├── vendor              <--- Um link simbólico para $MODID/system/vendor\r\n│   ├── product             <--- Um link simbólico para $MODID/system/product\r\n│   ├── system_ext          <--- Um link simbólico para $MODID/system/system_ext\r\n│   │\r\n│   │      *** Quaisquer arquivos/pastas adicionais são permitidos ***\r\n│   │\r\n│   ├── ...\r\n│   └── ...\r\n|\r\n├── another_module\r\n│   ├── .\r\n│   └── .\r\n├── .\r\n├── .\r\n```\r\n\r\n::: tip DIFERENÇAS COM MAGISK\r\nO KernelSU não possui suporte integrado para o Zygisk, portanto não há conteúdo relacionado ao Zygisk no módulo. No entanto, você pode usar [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) para suportar módulos Zygisk. Neste caso, o conteúdo do módulo Zygisk é idêntico ao suportado pelo Magisk.\r\n:::\r\n\r\n### module.prop\r\n\r\n`module.prop` é um arquivo de configuração para um módulo. No KernelSU, se um módulo não contiver este arquivo, ele não será reconhecido como um módulo. O formato deste arquivo é o seguinte:\r\n\r\n```txt\r\nid=<string>\r\nname=<string>\r\nversion=<string>\r\nversionCode=<int>\r\nauthor=<string>\r\ndescription=<string>\r\nupdateJson=<url> (opcional)\r\nactionIcon=<path> (opcional)\r\nwebuiIcon=<path> (opcional)\r\n```\r\n\r\n- `id` deve corresponder a esta expressão regular: `^[a-zA-Z][a-zA-Z0-9._-]+$`<br>\r\n  Exemplo: ✓ `a_module`, ✓ `a.module`, ✓ `module-101`, ✗ `a module`, ✗ `1_module`, ✗ `-a-module`<br>\r\n  Este é o **identificador exclusivo** do seu módulo. Você não deve alterá-lo depois de publicado.\r\n- `versionCode` deve ser um **número inteiro**. Isso é usado para comparar versões.\r\n- Outros que não foram mencionados acima podem ser qualquer string de **linha única**.\r\n- Certifique-se de usar o tipo de quebra de linha `UNIX (LF)` e não o `Windows (CR+LF)` ou `Macintosh (CR)`.\r\n- `actionIcon` e `webuiIcon` são caminhos de imagem opcionais usados como ícones\r\n  padrão para o atalho de ação do módulo e o atalho WebUI do módulo no\r\n  aplicativo gerenciador. Esses caminhos devem ser relativos ao diretório raiz do módulo.\r\n  Por exemplo, `actionIcon=icon/icon.png` será resolvido como `<MODDIR>/icon/icon.png`.\r\n\r\n::: tip DESCRIÇÃO DINÂMICA\r\nO campo `description` pode ser substituído dinamicamente em tempo de execução usando o sistema de configuração de módulos. Veja [Substituindo a Descrição do Módulo](module-config.md#overriding-module-description) para detalhes.\r\n:::\r\n\r\n### Shell scripts\r\n\r\nPor favor, leia a seção [Scripts de inicialização](#scripts-de-inicializacao) para entender a diferença entre `post-fs-data.sh` e `service.sh`. Para a maioria dos desenvolvedores de módulos, `service.sh` deve ser bom o suficiente se você precisar apenas executar um script de inicialização. Se precisar executar o script após a inicialização ser concluída, use `boot-completed.sh`. Se você quiser fazer algo após montar OverlayFS, use `post-mount.sh`.\r\n\r\nEm todos os scripts do seu módulo, use `MODDIR=${0%/*}` para obter o caminho do diretório base do seu módulo, **NÃO** codifique o caminho do seu módulo nos scripts.\r\n\r\n::: tip DIFERENÇAS COM MAGISK\r\nVocê pode usar a variável de ambiente `KSU` para determinar se um script está sendo executado no KernelSU ou Magisk. Se estiver executando no KernelSU, esse valor será definido como `true`.\r\n:::\r\n\r\n### Diretório `system`\r\n\r\nO conteúdo deste diretório será sobreposto à partição `/system` do sistema usando OverlayFS após a inicialização do sistema. Isso significa que:\r\n\r\n1. Arquivos com o mesmo nome daqueles no diretório correspondente no sistema serão substituídos pelos arquivos deste diretório.\r\n2. Pastas com o mesmo nome daquelas no diretório correspondente no sistema serão mescladas com as pastas neste diretório.\r\n\r\nSe você deseja excluir um arquivo ou pasta no diretório original do sistema, você precisa criar um arquivo com o mesmo nome do arquivo/pasta no diretório do módulo usando `mknod filename c 0 0`. Dessa forma, o sistema OverlayFS irá automaticamente \"branquear\" este arquivo como se ele tivesse sido excluído (a partição /system não foi realmente alterada).\r\n\r\nVocê também pode declarar uma variável chamada `REMOVE` contendo uma lista de diretórios em `customize.sh` para executar operações de remoção, e o KernelSU executará automaticamente `mknod <TARGET> c 0 0` nos diretórios correspondentes do módulo. Por exemplo:\r\n\r\n```sh\r\nREMOVE=\"\r\n/system/app/YouTube\r\n/system/app/Bloatware\r\n\"\r\n```\r\n\r\nA lista acima irá executar `mknod $MODPATH/system/app/YouTube c 0 0` e `mknod $MODPATH/system/app/Bloatware c 0 0`, `/system/app/YouTube` e `/system/app/Bloatware` serão removidos após o módulo entrar em vigor.\r\n\r\nSe você deseja substituir um diretório no sistema, você precisa criar um diretório com o mesmo caminho no diretório do módulo e, em seguida, definir o atributo `setfattr -n trusted.overlay.opaque -v y <TARGET>` para este diretório. Desta forma, o sistema OverlayFS substituirá automaticamente o diretório correspondente no sistema (sem alterar a partição /system).\r\n\r\nVocê pode declarar uma variável chamada `REPLACE` em seu arquivo `customize.sh`, que inclui uma lista de diretórios a serem substituídos, e o KernelSU executará automaticamente as operações correspondentes em seu diretório de módulo. Por exemplo:\r\n\r\n```sh\r\nREPLACE=\"\r\n/system/app/YouTube\r\n/system/app/Bloatware\r\n\"\r\n```\r\n\r\nEsta lista criará automaticamente os diretórios `$MODPATH/system/app/YouTube` e `$MODPATH/system/app/Bloatware` e, em seguida, executará `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/YouTube` e `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/Bloatware`. Após o módulo entrar em vigor, `/system/app/YouTube` e `/system/app/Bloatware` serão substituídos por diretórios vazios.\r\n\r\n::: tip DIFERENÇAS COM MAGISK\r\nO mecanismo sem sistema do KernelSU é implementado através do OverlayFS do kernel, enquanto o Magisk atualmente usa montagem mágica (montagem de ligação). Os dois métodos de implementação têm diferenças significativas, mas o objetivo final é o mesmo: modificar os arquivos `/system` sem modificar fisicamente a partição `/system`.\r\n:::\r\n\r\nSe você estiver interessado em OverlayFS, é recomendável ler a [documentação sobre OverlayFS](https://docs.kernel.org/filesystems/overlayfs.html) do kernel Linux.\r\n\r\n### system.prop\r\n\r\nEste arquivo segue o mesmo formato de `build.prop`. Cada linha é composta por `[key]=[value]`.\r\n\r\n### sepolicy.rule\r\n\r\nSe o seu módulo exigir alguns patches adicionais do sepolicy, adicione essas regras a este arquivo. Cada linha neste arquivo será tratada como uma declaração de política.\r\n\r\n## Instalador do módulo\r\n\r\nUm instalador do módulo KernelSU é um módulo KernelSU empacotado em um arquivo ZIP que pode ser atualizado no gerenciador do KernelSU. O instalador do módulo KernelSU mais simples é apenas um módulo KernelSU compactado como um arquivo ZIP.\r\n\r\n```txt\r\nmodule.zip\r\n│\r\n├── customize.sh                       <--- (Opcional, mais detalhes posteriormente)\r\n│                                           Este script será fornecido por update-binary\r\n├── ...\r\n├── ...  /* O resto dos arquivos do módulo */\r\n│\r\n```\r\n\r\n::: warning AVISO\r\nO módulo KernelSU **NÃO** é compatível para instalação no Recovery personalizado!\r\n:::\r\n\r\n### Personalização\r\n\r\nSe você precisar personalizar o processo de instalação do módulo, opcionalmente você pode criar um script no instalador chamado `customize.sh`. Este script será **sourced** (não executado) pelo script do instalador do módulo depois que todos os arquivos forem extraídos e as permissões padrão e o contexto secundário forem aplicados. Isso é muito útil se o seu módulo exigir configuração adicional com base na ABI do dispositivo ou se você precisar definir permissões/secontext especiais para alguns dos arquivos do seu módulo.\r\n\r\nSe você quiser controlar e personalizar totalmente o processo de instalação, declare `SKIPUNZIP=1` em `customize.sh` para pular todas as etapas de instalação padrão. Ao fazer isso, seu `customize.sh` será responsável por instalar tudo sozinho.\r\n\r\nO script `customize.sh` é executado no shell BusyBox `ash` do KernelSU com o Modo Autônomo ativado. As seguintes variáveis ​​e funções estão disponíveis:\r\n\r\n#### Variáveis\r\n\r\n- `KSU` (bool): uma variável para marcar que o script está sendo executado no ambiente KernelSU, e o valor desta variável sempre será `true`. Você pode usá-lo para distinguir entre KernelSU e Magisk.\r\n- `KSU_VER` (string): a string da versão do KernelSU atualmente instalado (ex.: `v0.4.0`).\r\n- `KSU_VER_CODE` (int): o código da versão do KernelSU atualmente instalado no espaço do usuário (ex.: `10672`).\r\n- `KSU_KERNEL_VER_CODE` (int): o código da versão do KernelSU atualmente instalado no espaço do kernel (ex.: `10672`).\r\n- `BOOTMODE` (bool): sempre será `true` no KernelSU.\r\n- `MODPATH` (path): o caminho onde os arquivos do seu módulo devem ser instalados.\r\n- `TMPDIR` (path): um lugar onde você pode armazenar arquivos temporariamente.\r\n- `ZIPFILE` (path): ZIP de instalação do seu módulo.\r\n- `ARCH` (string): a arquitetura da CPU do dispositivo. O valor é `arm`, `arm64`, `x86` ou `x64`.\r\n- `IS64BIT` (bool): `true` se `$ARCH` for `arm64` ou `x64`.\r\n- `API` (int): o nível da API (versão do Android) do dispositivo (ex.: `23` para Android 6.0).\r\n\r\n::: warning AVISO\r\nNo KernelSU, `MAGISK_VER_CODE` é sempre `25200` e `MAGISK_VER` é sempre `v25.2`. Por favor, não use essas duas variáveis ​​para determinar se ele está sendo executado no KernelSU ou não.\r\n:::\r\n\r\n#### Funções\r\n\r\n```txt\r\nui_print <msg>\r\n    imprima <msg> no console\r\n    Evite usar 'echo', pois ele não será exibido no console de recovery personalizado\r\n\r\nabort <msg>\r\n    imprima mensagem de erro <msg> para consolar e encerrar a instalação\r\n    Evite usar 'exit', pois isso irá pular as etapas de limpeza de encerramento\r\n\r\nset_perm <target> <owner> <group> <permission> [context]\r\n    se [context] não estiver definido, o padrão é \"u:object_r:system_file:s0\"\r\n    esta função é uma abreviação para os seguintes comandos:\r\n       chown owner.group target\r\n       chmod permission target\r\n       chcon context target\r\n\r\nset_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]\r\n    se [context] não está definido, o padrão é \"u:object_r:system_file:s0\"\r\n    para todos os arquivos em <directory>, ele chamará:\r\n       set_perm arquivo proprietário do grupo filepermission\r\n    para todos os diretórios em <directory> (incluindo ele mesmo), ele vai ligar:\r\n       set_perm dir owner group dirpermission context\r\n```\r\n\r\n## Scripts de inicialização\r\n\r\nNo KernelSU, os scripts são divididos em dois tipos com base em seu modo de execução: modo post-fs-data e modo de serviço late_start.\r\n\r\n- modo post-fs-data\r\n  - Esta etapa está BLOQUEANDO. O processo de inicialização é pausado antes da conclusão da execução ou após 10 segundos.\r\n  - Os scripts são executados antes de qualquer módulo ser montado. Isso permite que um desenvolvedor de módulo ajuste dinamicamente seus módulos antes de serem montados.\r\n  - Este estágio acontece antes do início do Zygote, o que significa praticamente tudo no Android.\r\n  - **AVISO:** Usar `setprop` irá bloquear o processo de inicialização! Por favor, use `resetprop -n <prop_name> <prop_value>` em vez disso.\r\n  - **Execute scripts neste modo apenas se necessário**.\r\n- modo de serviço late_start\r\n  - Esta etapa é SEM BLOQUEIO. Seu script é executado em paralelo com o restante do processo de inicialização.\r\n  - **Este é o estágio recomendado para executar a maioria dos scripts**.\r\n\r\nNo KernelSU, os scripts de inicialização são divididos em dois tipos com base no local de armazenamento: scripts gerais e scripts de módulo.\r\n\r\n- Scripts gerais\r\n  - Colocado em `/data/adb/post-fs-data.d`, `/data/adb/service.d`, `/data/adb/post-mount.d` ou `/data/adb/boot-completed.d`.\r\n  - Somente executado se o script estiver definido como executável (`chmod +x script.sh`).\r\n  - Os scripts em `post-fs-data.d` são executados no modo post-fs-data e os scripts em `service.d` são executados no modo de serviço late_start.\r\n  - Os módulos **NÃO** devem adicionar scripts gerais durante a instalação.\r\n- Scripts de módulo\r\n  - Colocado na própria pasta do módulo.\r\n  - Executado apenas se o módulo estiver ativado.\r\n  - `post-fs-data.sh` é executado no modo post-fs-data, `service.sh` é executado no modo de serviço late_start, `boot-completed.sh` é executado na inicialização concluída e `post-mount.sh` é executado no OverlayFS montado.\r\n\r\nTodos os scripts de inicialização serão executados no shell BusyBox `ash` do KernelSU com o Modo Autônomo ativado.\r\n\r\n### Explicação do processo de scripts de inicialização\r\n\r\nA seguir está o processo de inicialização relevante para o Android (algumas partes foram omitidas), que inclui a operação do KernelSU (com asteriscos iniciais) e pode ajudá-lo a entender melhor o propósito desses scripts de módulo:\r\n\r\n```txt\r\n0. Bootloader (nada nesta tela)\r\nload patched boot.img\r\nload kernel:\r\n    - Modo GKI: kernel GKI com KernelSU integrado\r\n    - Modo LKM: kernel stock\r\n...\r\n\r\n1. kernel exec init (logo OEM na tela):\r\n    - Modo GKI: stock init\r\n    - Modo LKM: exec ksuinit, insmod kernelsu.ko, exec stock init\r\nmount /dev, /dev/pts, /proc, /sys, etc.\r\nproperty-init -> read default props\r\nread init.rc\r\n...\r\nearly-init -> init -> late_init\r\nearly-fs\r\n   start vold\r\nfs\r\n  mount /vendor, /system, /persist, etc.\r\npost-fs-data\r\n  *verificação do modo de segurança\r\n  *executar scripts gerais em post-fs-data.d/\r\n  *carregar sepolicy.rule\r\n  *montar tmpfs\r\n  *executar scripts de módulo post-fs-data.sh\r\n    **(Zygisk)./bin/zygisk-ptrace64 monitor\r\n  *(pré)carregamento de system.prop (igual a resetprop -n)\r\n  *remontar módulos em /system\r\n  *executar scripts gerais em post-mount.d/\r\n  *executar scripts de módulo post-mount.sh\r\nzygote-start\r\nload_all_props_action\r\n  *executar resetprop (defina adereços reais para resetprop com a opção -n)\r\n... -> boot\r\n  class_start core\r\n    start-service logd, console, vold, etc.\r\n  class_start main\r\n    start-service adb, netd (iptables), zygote, etc.\r\n\r\n2. kernel2user init (animação da ROM na tela, inicie pelo serviço bootanim)\r\n*executar scripts gerais em service.d/\r\n*executar scripts de módulo service.sh\r\n*definir adereços para resetprop sem a opção -p\r\n  **(Zygisk) hook zygote (iniciar o zygiskd)\r\n  **(Zygisk) montar zygisksu/module.prop\r\niniciar apps do sistema (início automático)\r\n...\r\ninicialização completa (transmitir evento ACTION_BOOT_COMPLETED)\r\n*executar scripts gerais em boot-completed.d/\r\n*executar scripts de módulo boot-completed.sh\r\n\r\n3. Operável pelo usuário (tela de bloqueio)\r\ninsira a senha para descriptografar /data/data\r\n*conjunto real de adereços para resetprop com opção -p\r\niniciar apps de usuário (início automático)\r\n```\r\n\r\nSe você estiver interessado na linguagem de inicialização do Android, é recomendável ler sua [documentação](https://android.googlesource.com/platform/system/core/+/master/init/README.md).\r\n\r\n## Modo late-load {#late-load-mode}\r\n\r\nAlém do fluxo de inicialização padrão descrito acima, o KernelSU suporta um **modo late-load** para cenários de LKM (Loadable Kernel Module). Neste modo, o módulo do kernel KernelSU é carregado **após o sistema ter sido totalmente iniciado**, em vez de durante o processo init.\r\n\r\n### Quando o late-load acontece?\r\n\r\nO late-load é acionado executando o comando `ksud late-load`. Este comando:\r\n\r\n1. Detecta a versão KMI atual e carrega o `kernelsu.ko` correspondente dos recursos incorporados.\r\n2. Realiza a inicialização do módulo (regras SELinux, lista de permissões, features, etc.) que normalmente aconteceria durante a inicialização.\r\n\r\nComo o sistema já está totalmente em execução, certos mecanismos de tempo de inicialização estão indisponíveis ou são desnecessários.\r\n\r\n### Diferenças em relação à inicialização padrão\r\n\r\n| Comportamento | Inicialização padrão | Modo late-load |\r\n|---------------|:---:|:---:|\r\n| Módulo do kernel carregado pelo init (PID 1) | Sim | Não (carregado após inicialização) |\r\n| Hooks kprobe do ksud (execve/read/fstat/input) | Sim | Ignorado |\r\n| Detecção de modo seguro (tecla de volume) | Sim | Sempre desativado |\r\n| Captura de log de inicialização (logcat/dmesg) | Sim | Ignorado |\r\n| Verificação de coexistência com Magisk | Sim | Ignorado |\r\n| Evento `post-fs-data` reportado ao kernel | Sim | Ignorado |\r\n| Evento `boot-completed` reportado ao kernel | Sim | Definido diretamente durante init |\r\n| Scripts `post-fs-data.sh` / `post-fs-data.d/` | Sim | Substituído pelo estágio `late-load` |\r\n| Carregamento de `system.prop` | Sim | Sim |\r\n| Montagem OverlayFS (metamodule) | Sim | Sim |\r\n| Scripts `post-mount.sh` / `post-mount.d/` | Sim | Sim |\r\n| Scripts `service.sh` / `service.d/` | Sim | Sim |\r\n| Scripts `boot-completed.sh` / `boot-completed.d/` | Sim | Sim |\r\n| Variável de ambiente `KSU_LATE_LOAD` | Não definida | Definida como `1` |\r\n| Flag de info do kernel `0x4` | Não definida | Definida |\r\n\r\n### Ordem de execução dos scripts\r\n\r\nNo modo late-load, a ordem de execução dos scripts é:\r\n\r\n```txt\r\nksud late-load:\r\n  1. Carregar kernelsu.ko (se ainda não carregado)\r\n  2. Extrair binários, processar atualizações de módulos, carregar regras SELinux, inicializar features\r\n  3. Executar scripts late-load.d/ e scripts late-load dos módulos (bloqueante)\r\n  4. Carregar system.prop (resetprop -n)\r\n  5. Executar script de montagem do metamodule (OverlayFS)\r\n  6. Executar scripts post-mount.d/ e post-mount.sh dos módulos (bloqueante)\r\n  7. Executar scripts service.d/ e service.sh dos módulos (não bloqueante)\r\n  8. Executar scripts boot-completed.d/ e boot-completed.sh dos módulos (não bloqueante)\r\n```\r\n\r\n### Scripts específicos do late-load\r\n\r\nMódulos podem fornecer um script `late-load.sh` que é executado **apenas** no modo late-load, como substituto do `post-fs-data.sh`. Este script é executado antes da montagem do OverlayFS, similar ao `post-fs-data.sh` no fluxo padrão.\r\n\r\nAlém disso, scripts gerais podem ser colocados em `/data/adb/late-load.d/` para serem executados neste estágio.\r\n\r\n### Detectando o modo late-load nos scripts\r\n\r\nMódulos podem detectar o modo late-load verificando a variável de ambiente `KSU_LATE_LOAD`:\r\n\r\n```sh\r\nif [ \"$KSU_LATE_LOAD\" = \"1\" ]; then\r\n    # Executando no modo late-load\r\n    echo \"Late-load mode detected\"\r\nfi\r\n```\r\n\r\nIsso permite que os módulos ajustem seu comportamento de acordo, por exemplo, pulando operações que só são necessárias durante a inicialização antecipada.\r\n"
  },
  {
    "path": "website/docs/pt_BR/guide/rescue-from-bootloop.md",
    "content": "# Resgate do bootloop\r\n\r\nAo atualizar um dispositivo, podem ocorrer situações em que o dispositivo fica \"bloqueado\". Em teoria, se você usar o fastboot apenas para atualizar a partição boot ou instalar módulos inadequados que causam falha na inicialização do dispositivo, isso pode ser restaurado por meio de operações apropriadas. Este documento tem como objetivo fornecer alguns métodos de emergência para ajudá-lo a recuperar um dispositivo \"bloqueado\".\r\n\r\n## Bloqueio por flashar partição boot\r\n\r\nNo KernelSU, as seguintes situações podem causar bloqueio de inicialização ao flashar a partição boot:\r\n\r\n1. Você flashou uma imagem boot no formato errado. Por exemplo, se o formato de boot do seu dispositivo for `gz`, mas você flashou uma imagem no formato `lz4`, o dispositivo não inicializá.\r\n2. Seu dispositivo precisa desativar a verificação AVB para inicializar corretamente, o que geralmente exige a limpeza de todos os dados do dispositivo.\r\n3. Seu kernel tem alguns bugs ou não é adequado para o flash do seu dispositivo.\r\n\r\nNão importa qual seja a situação, você pode recuperar **flashando a imagem de boot padrão**. Portanto, no início do guia de instalação, recomendamos fortemente que você faça backup de seu boot padrão antes de realizar o flash. Se você não fez backup, poderá obter o boot original de fábrica de outros usuários com o mesmo dispositivo ou do firmware oficial.\r\n\r\n## Bloqueio por módulos\r\n\r\nA instalação de módulos pode ser uma das causas mais comuns de bloqueio do seu dispositivo, mas devemos alertá-lo seriamente: **NÃO INSTALE MÓDULOS DE FONTES DESCONHECIDAS!** Como os módulos têm privilégios root, eles podem causar danos irreversíveis ao seu dispositivo!\r\n\r\n### Módulos normais\r\n\r\nSe você instalou um módulo que foi comprovadamente seguro, mas faz com que seu dispositivo não inicialize, então esta situação é facilmente recuperável no KernelSU sem qualquer preocupação. O KernelSU possui mecanismos integrados para recuperar seu dispositivo, incluindo o seguinte:\r\n\r\n1. Atualização AB\r\n2. Recupere pressionando o botão de diminuir volume\r\n\r\n#### Atualização AB\r\n\r\nAs atualizações do módulo KernelSU são baseadas no mecanismo de atualização AB do sistema Android usado em atualizações OTA. Quando você instala um novo módulo ou atualiza um existente, isso não modifica diretamente o arquivo do módulo atualmente em uso. Em vez disso, todos os módulos são integrados em uma nova imagem de atualização. Após o sistema ser reiniciado, ele tentará iniciar usando essa nova imagem de atualização. Se o sistema Android inicializar com sucesso, os módulos serão efetivamente atualizados.\r\n\r\nPortanto, o método mais simples e comumente usado para recuperar seu dispositivo é **forçar uma reinicialização**. Se você não conseguir iniciar o sistema após instalar um módulo, pode pressionar e segurar o botão liga/desliga por mais de 10 segundos, e o sistema será reiniciado automaticamente. Após a reinicialização, ele retornará ao estado anterior à atualização do módulo, e os módulos atualizados serão desativados automaticamente.\r\n\r\n#### Recupere pressionando o botão de diminuir volume\r\n\r\nSe a Atualização AB ainda não resolveu o problema, você pode tentar usar o **Modo de Segurança**. Nesse modo, todos os módulos são desativados.\r\n\r\nExistem duas maneiras de entrar no Modo de Segurança:\r\n\r\n1. O Modo de Segurança integrado de alguns sistemas: Alguns sistemas possuem um Modo de Segurança integrado que pode ser acessado pressionando longamente o botão de diminuir volume. Em outros sistemas (como o HyperOS), o Modo de Segurança pode ser ativado a partir do Recovery. Ao entrar no Modo de Segurança do sistema, o KernelSU também entrará nesse modo e desativará automaticamente os módulos.\r\n2. O Modo de Segurança integrado do KernelSU: Nesse caso, o método é **pressionar a tecla de diminuir volume continuamente por mais de três vezes** após a primeira tela de inicialização.\r\n\r\nApós entrar no Modo de Segurança, todos os módulos na página Módulos do gerenciador do KernelSU serão desativados. Porém, você ainda pode realizar a operação de \"desinstalação\" para desinstalar quaisquer módulos que possam estar causando problemas.\r\n\r\nO Modo de Segurança integrado é implementado no kernel, portanto não há possibilidade de perder eventos importantes devido à interceptação. No entanto, para kernels não-GKI, pode ser necessária uma integração manual do código. Para isso, consulte a documentação oficial para orientações.\r\n\r\n### Módulos maliciosos\r\n\r\nSe os métodos acima não conseguirem recuperar seu dispositivo, é muito provável que o módulo que você instalou tenha operações maliciosas ou tenha danificado seu dispositivo de outra forma. Nesse caso, há apenas duas sugestões:\r\n\r\n1. Limpar os dados e instalar o sistema oficial.\r\n2. Consultar o serviço pós-venda.\r\n"
  },
  {
    "path": "website/docs/pt_BR/guide/unofficially-support-devices.md",
    "content": "# Dispositivos com suporte não oficial\r\n\r\n::: warning AVISO\r\nEste documento é apenas para referência de arquivo e não é mais mantido.\r\nDesde o KernelSU v1.0, abandonamos o suporte oficial para dispositivos não-GKI.\r\n:::\r\n\r\n::: warning AVISO\r\nNesta página, existem kernels para dispositivos não-GKI que suportam o KernelSU mantidos por outros desenvolvedores.\r\n:::\r\n\r\n::: warning AVISO\r\nEsta página é destinada apenas para ajudá-lo a encontrar o código-fonte correspondente ao seu dispositivo. **NÃO** significa que o código-fonte foi revisado pelos desenvolvedores do KernelSU. Você deve usá-lo por sua própria conta e risco.\r\n:::\r\n\r\n<script setup>\r\nimport data from '../../repos.json'\r\n</script>\r\n\r\n<table>\r\n   <thead>\r\n      <tr>\r\n         <th>Mantenedor</th>\r\n         <th>Repositório</th>\r\n         <th>Dispositivos suportados</th>\r\n      </tr>\r\n   </thead>\r\n   <tbody>\r\n    <tr v-for=\"repo in data\" :key=\"repo.devices\">\r\n        <td><a :href=\"repo.maintainer_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.maintainer }}</a></td>\r\n        <td><a :href=\"repo.kernel_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.kernel_name }}</a></td>\r\n        <td>{{ repo.devices }}</td>\r\n    </tr>\r\n   </tbody>\r\n</table>\r\n"
  },
  {
    "path": "website/docs/pt_BR/guide/what-is-kernelsu.md",
    "content": "# O que é KernelSU?\r\n\r\nO KernelSU é uma solução root para dispositivos Android GKI, funciona no modo kernel e concede privilégios root para apps do espaço do usuário diretamente no espaço do kernel.\r\n\r\n## Características\r\n\r\nA principal característica do KernelSU é que ele é **baseado em kernel**. O KernelSU funciona no modo kernel, portanto pode fornecer uma interface de kernel que nunca tivemos antes. Por exemplo, é possível adicionar pontos de interrupção de hardware a qualquer processo no modo kernel, acessar a memória física de qualquer processo de forma invisível, interceptar qualquer chamada de sistema (syscall) no espaço do kernel, entre outras funcionalidades.\r\n\r\nAlém disso, o KernelSU fornece um [sistema metamodule](metamodule.md), que é uma arquitetura plugável para gerenciamento de módulos. Diferente das soluções root tradicionais que integram a lógica de montagem em seu núcleo, o KernelSU delega isso aos metamodules. Isso permite que você instale metamodules como [meta-overlayfs](https://github.com/tiann/KernelSU/tree/main/userspace/meta-overlayfs) para fornecer modificações systemless na partição `/system` e outras partições.\r\n\r\n## Como usar o KernelSU?\r\n\r\nPor favor, consulte: [Instalação](installation)\r\n\r\n## Como compilar o KernelSU?\r\n\r\nPor favor, consulte: [Como compilar](how-to-build)\r\n\r\n## Discussão\r\n\r\n- Telegram: [@KernelSU](https://t.me/KernelSU)\r\n"
  },
  {
    "path": "website/docs/pt_BR/index.md",
    "content": "---\r\nlayout: home\r\ntitle: Início\r\n\r\nhero:\r\n  name: KernelSU\r\n  text: Uma solução root baseada em kernel para Android\r\n  tagline: \"\"\r\n  image:\r\n    src: /logo.png\r\n    alt: KernelSU\r\n  actions:\r\n    - theme: brand\r\n      text: Iniciar\r\n      link: /pt_BR/guide/what-is-kernelsu\r\n    - theme: alt\r\n      text: Ver no GitHub\r\n      link: https://github.com/tiann/KernelSU\r\n\r\nfeatures:\r\n  - title: Baseado em kernel\r\n    details: Como o nome sugere, KernelSU funciona no kernel Linux, dando-lhe mais controle sobre os apps do espaço do usuário.\r\n  - title: Controle de acesso root\r\n    details: Somente apps permitidos podem acessar ou ver su, todos os outros apps não estão cientes disso.\r\n  - title: Privilégios root personalizáveis\r\n    details: KernelSU permite a personalização de su, uid, gid, grupos, capacidades e regras do SELinux, bloqueando privilégios root.\r\n  - title: Sistema Metamodule\r\n    details: Infraestrutura de módulos plugável permite modificações systemless em /system. Instale um metamodule como meta-overlayfs para habilitar a montagem de módulos.\r\n"
  },
  {
    "path": "website/docs/public/ads.txt",
    "content": "google.com, pub-8356785667482909, DIRECT, f08c47fec0942fa0"
  },
  {
    "path": "website/docs/public/templates/.gitkeep",
    "content": ""
  },
  {
    "path": "website/docs/public/templates/adaway.root",
    "content": "{\n    \"id\":\"adaway.root\",\n    \"name\":\"Adaway Root\",\n    \"author\":\"JohnRTitor\",\n    \"description\":\"Only essential permissions to let Adaway modify hosts file and operate a web server.\",\n    \"uid\":0,\n    \"gid\":0,\n    \"groups\":[\n        \"ROOT\"\n    ],\n    \"capabilities\":[\n        \"CAP_DAC_OVERRIDE\",\n        \"CAP_NET_BIND_SERVICE\",\n        \"CAP_SYS_PTRACE\"\n    ],\n    \"context\":\"u:r:su:s0\",\n    \"namespace\":\"INHERITED\",\n    \"locales\": {\n        \"zh_TW\": {\n            \"name\": \"Adaway Root\",\n            \"description\": \"僅允許 Adaway 修改 hosts 和執行 Web 伺服器的必要權限\"\n        },\n        \"bn\": {\n            \"name\": \"অ্যাডঅ্যাওয়ে রুট\",\n            \"description\": \"অ্যাডঅ্যাওয়ে সিস্টেমের হোস্ট ফাইল পরিবর্তন এবং ওয়েবসার্ভার চালু করতে কমপক্ষে যা অনুমতি লাগে।\"\n        },\n        \"pt_BR\": {\n            \"name\": \"Adaway Root\",\n            \"description\": \"Apenas permissões essenciais para permitir que Adaway modifique o arquivo hosts e opere um servidor web.\"\n        },\n        \"tr\": {\n            \"name\": \"Adaway Root\",\n            \"description\": \"Adaway'in hosts dosyasını değiştirmesine ve bir web sunucusunu çalıştırmasına izin vermek için gerekli izinler.\"\n        },\n        \"it_IT\": {\n            \"name\": \"Adaway Root\",\n            \"description\": \"Concede le autorizzazioni essenziali affinché Adaway possa modificare il file host e gestire un server web. \"\n        }        \n    }\n}\n"
  },
  {
    "path": "website/docs/public/templates/adb",
    "content": "{\n    \"id\":\"adb\",\n    \"name\":\"Adb\",\n    \"author\":\"kernelsu.org\",\n    \"description\":\"Minimal rules required by most apps using ADB privilege.\",\n    \"uid\":2000,\n    \"gid\":2000,\n    \"groups\":[\n        \"ADB\"\n    ],\n    \"locales\": {\n        \"bn\": {\n            \"name\": \"এডিবি\",\n            \"description\": \"নূন্যতম অনুমতি যার দ্বারা এডিবি স্বচ্ছন্দে কাজ করতে পারে।\"\n        },\n        \"zh_CN\": {\n            \"name\": \"Adb 模版\",\n            \"description\": \"大多数使用 ADB 权限应用所需要的最小权限\"\n        },\n        \"zh_TW\": {\n            \"name\": \"Adb\",\n            \"description\": \"大多數使用 ADB 權限應用程式所需要的最低權限\"\n        },\n        \"tr\": {\n            \"name\": \"Adb\",\n            \"description\": \"ADB ayrıcalığını kullanan çoğu uygulama için gereken minimum kurallar.\"\n        },\n        \"pt_BR\": {\n            \"name\": \"Adb\",\n            \"description\": \"Regras mínimas exigidas pela maioria dos apps que usam privilégio ADB.\"\n        },\n        \"ja\": {\n            \"name\": \"Adb\",\n            \"description\": \"ほとんどのアプリが使用するために必要な最小限のルール ADB.\"\n        },\n        \"it_IT\": {\n            \"name\": \"Adb\",\n            \"description\": \"Autorizzazioni minime richieste dalla maggior parte delle app che usano i privilegi di ADB.\"\n        } \n    }\n}\n"
  },
  {
    "path": "website/docs/public/templates/cemiuiler.readproc",
    "content": "{\n    \"id\":\"cemiuiler.readproc\",\n    \"name\":\"Cemiuiler\",\n    \"author\":\"refined-fish\",\n    \"description\":\"Grant Cemiuiler the minimum permissions to work properly-to restart the app.\",\n    \"namespace\":\"INHERITED\",\n    \"uid\":10000,\n    \"gid\":10000,\n    \"groups\":[\n        \"READPROC\"\n    ],\n    \"capabilities\":[\n        \"CAP_KILL\"\n    ],\n    \"context\":\"u:r:su:s0\",\n    \"rules\":\"\",\n    \"locales\": {\n        \"bn\": {\n            \"name\": \"সিমিউলার\",\n            \"description\": \"নূন্যতম অনুমতি যার দ্বারা সিমিউলার আবার কাজে বহাল হতে পারে।\"\n        },\n        \"zh_CN\": {\n            \"name\": \"西米露Cemiuiler\",\n            \"description\": \"授予Cemiuiler能正常工作——重启作用域应用的最小限度权限。\"\n        },\n        \"zh_TW\": {\n            \"name\": \"Cemiuiler\",\n            \"description\": \"授予Cemiuiler能正常運作－重啟作用域應用程式的最低權限\"\n        },\n        \"tr\": {\n            \"name\": \"Cemiuiler\",\n            \"description\": \"Cemiuiler uygulamasına düzgün çalışması için minimum izinleri verin.\"\n        },\n        \"pt_BR\": {\n            \"name\": \"Cemiuiler\",\n            \"description\": \"Conceda ao Cemiuiler as permissões mínimas para funcionar corretamente para reiniciar o app.\"\n        },\n        \"ja\": {\n            \"name\": \"Cemiuiler\",\n            \"description\": \"Cemiuiler に適切に動作するための最小限の権限を付与し、アプリケーションを再起動します。\"\n        },\n        \"it_IT\": {\n            \"name\": \"Cemiuiler\",\n            \"description\": \"Concedi a Cemiuiler le autorizzazioni minime per riavviare correttamente l'applicazione.\"\n        } \n    }\n}\n"
  },
  {
    "path": "website/docs/public/templates/hyperceiler.root",
    "content": "{\n    \"id\":\"hyperceiler.root\",\n    \"name\":\"HyperCeiler Official Root Template\",\n    \"author\":\"github@lingqiqi5211\",\n    \"description\":\"Minimum permissions to ensure HyperCeiler main functions work properly.\",\n    \"namespace\":\"INHERITED\",\n    \"uid\":0,\n    \"gid\":0,\n    \"groups\":[\n        \"ROOT\"\n    ],\n    \"capabilities\":[\n        \"CAP_DAC_OVERRIDE\",\n        \"CAP_FOWNER\",\n        \"CAP_KILL\",\n        \"CAP_SYS_CHROOT\",\n        \"CAP_SYS_PTRACE\",\n        \"CAP_SYS_ADMIN\",\n        \"CAP_SYS_BOOT\",\n        \"CAP_SETFCAP\",\n        \"CAP_BLOCK_SUSPEND\"\n    ],\n    \"context\":\"u:r:su:s0\",\n    \"rules\":\"\",\n    \"locales\": {\n        \"zh_CN\": {\n            \"name\": \"HyperCeiler 官方 Root 模板\",\n            \"description\": \"能保证 HyperCeiler 主要功能运行的最小权能\"\n        },\n        \"zh_TW\": {\n            \"name\": \"HyperCeiler 官方 Root 範本\",\n            \"description\": \"能保證 HyperCeiler 主要功能運作的最小權能\"\n        },\n        \"bn\": {\n            \"name\": \"হাইপারসিলার অফিসিয়াল রুট টেমপ্লেট\",\n            \"description\": \"হাইপারসিলারের মূল কার্যকারিতা নিশ্চিত করতে ন্যূনতম অনুমতি।\"\n        },\n        \"tr\": {\n            \"name\": \"HyperCeiler Resmi Root Şablonu\",\n            \"description\": \"HyperCeiler'in ana işlevlerinin düzgün çalışmasını sağlamak için minimum izinler.\"\n        },\n        \"pt_BR\": {\n            \"name\": \"Modelo Root Oficial do HyperCeiler\",\n            \"description\": \"Permissões mínimas para garantir que as funções principais do HyperCeiler funcionem corretamente.\"\n        },\n        \"ja\": {\n            \"name\": \"HyperCeiler 公式 Root テンプレート\",\n            \"description\": \"HyperCeiler の主要機能が正常に動作するための最小限の権限。\"\n        },\n        \"it_IT\": {\n            \"name\": \"Modello Root Ufficiale di HyperCeiler\",\n            \"description\": \"Autorizzazioni minime per garantire il corretto funzionamento delle funzioni principali di HyperCeiler.\"\n        }\n    }\n}\n"
  },
  {
    "path": "website/docs/public/templates/incompetent.root",
    "content": "{\n    \"id\":\"incompetent.root\",\n    \"name\":\"Incompetent root\",\n    \"author\":\"kernelsu.org\",\n    \"description\":\"A incompetent root user without any capability.\",\n    \"uid\":0,\n    \"gid\":0,\n    \"groups\":[\n        \"ROOT\"\n    ],\n    \"locales\": {\n        \"bn\": {\n            \"name\": \"অযোগ্য রুট ইউজার\",\n            \"description\": \"অযোগ্য রুট ইউজার যার কোনো স্পেশাল ক্ষমতা নেই।\"\n        },\n        \"zh_CN\": {\n            \"name\": \"无能的 Root\",\n            \"description\": \"无任何权能的 root 用户。\"\n        },\n        \"zh_TW\": {\n            \"name\": \"無能的 Root\",\n            \"description\": \"無任何權限的 root\"\n        },\n        \"tr\": {\n            \"name\": \"Yetersiz root\",\n            \"description\": \"Herhangi bir özelliği olmayan yetersiz bir root kullanıcısı.\"\n        },\n        \"pt_BR\": {\n            \"name\": \"Root incompetente\",\n            \"description\": \"Um usuário root incompetente sem qualquer capacidade.\"\n        },\n        \"ja\": {\n            \"name\": \"無能な root\",\n            \"description\": \"権限のない root ユーザー。\"\n        },\n        \"it_IT\": {\n            \"name\": \"Root incompetente\",\n            \"description\": \"Un utente root senza nessuna capacità.\"\n        } \n    }\n}\n"
  },
  {
    "path": "website/docs/public/templates/kernelmanager.root",
    "content": "{\n    \"id\":\"kernelmanager.root\",\n    \"name\":\"Kernel Manager\",\n    \"author\":\"Rem01Gaming\",\n    \"description\":\"Commonly used in Kernel managers such as FKM and SmartPack.\",\n    \"uid\":0,\n    \"gid\":0,\n    \"groups\":[\n        \"ROOT\",\n        \"READPROC\"\n    ],\n    \"capabilities\":[\n        \"CAP_KILL\",\n        \"CAP_SYSLOG\",\n        \"CAP_SYS_BOOT\",\n        \"CAP_DAC_OVERRIDE\"\n    ],\n    \"context\":\"u:r:su:s0\",\n    \"namespace\":\"INHERITED\",\n    \"locales\": {\n        \"bn\": {\n            \"name\": \"কার্নেল ম্যানেজার\",\n            \"description\": \"এফ কে এম বা স্মার্টপ্যাক কার্নেল ম্যানেজারের যে অনুমতি লাগে।\"\n        },\n        \"zh_TW\": {\n            \"name\": \"核心管理器\",\n            \"description\": \"常用於 FKM 和 SmartPack 等核心管理器\"\n        },\n        \"in\": {\n            \"name\": \"Kernel Manager\",\n            \"description\": \"Umumnya digunakan pada Kernel manager seperti FKM dan SmartPack.\"\n        },\n        \"tr\": {\n            \"name\": \"Kernel Yöneticisi\",\n            \"description\": \"FKM ve SmartPack gibi Kernel Yöneticilerinde yaygın olarak kullanılır.\"\n        },\n        \"pt_BR\": {\n            \"name\": \"Gerenciador de Kernel\",\n            \"description\": \"Comumente usado em gerenciadores de Kernel como FKM e SmartPack.\"\n        },\n        \"ja\": {\n            \"name\": \"Kernel マネージャー\",\n            \"description\": \"FKM や SmartPack などの XXX マネージャーでよく使用されます。\"\n        },\n        \"it_IT\": {\n            \"name\": \"Gestore Kernel\",\n            \"description\": \"Autorizzazioni comunemente richieste in applicazioni per gestire il Kernel come FKM e SmartPack.\"\n        } \n    }\n}\n"
  },
  {
    "path": "website/docs/public/templates/nethunter.root",
    "content": "{\n    \"id\": \"nethunter.root\",\n    \"name\": \"Kali NetHunter\",\n    \"author\": \"cachiusa\",\n    \"description\": \"Required permissions for Kali NetHunter app to chroot.\",\n    \"namespace\": \"INHERITED\",\n    \"uid\": 0,\n    \"gid\": 0,\n    \"groups\": [\n        \"ROOT\"\n    ],\n    \"capabilities\": [\n        \"CAP_CHOWN\",\n        \"CAP_DAC_OVERRIDE\",\n        \"CAP_DAC_READ_SEARCH\",\n        \"CAP_FOWNER\",\n        \"CAP_FSETID\",\n        \"CAP_SETGID\",\n        \"CAP_SETUID\",\n        \"CAP_NET_BIND_SERVICE\",\n        \"CAP_NET_ADMIN\",\n        \"CAP_NET_RAW\",\n        \"CAP_SYS_MODULE\",\n        \"CAP_SYS_RAWIO\",\n        \"CAP_SYS_CHROOT\",\n        \"CAP_SYS_PTRACE\",\n        \"CAP_SYS_ADMIN\",\n        \"CAP_SYS_BOOT\",\n        \"CAP_SYS_RESOURCE\",\n        \"CAP_SYS_TTY_CONFIG\",\n        \"CAP_MKNOD\",\n        \"CAP_SYSLOG\",\n        \"CAP_AUDIT_WRITE\"\n    ],\n    \"context\": \"u:r:su:s0\",\n    \"locales\": {\n        \"bn\": {\n            \"name\": \"কালি নেটহানটার\",\n            \"description\": \"কালি নেটহানটার অ্যাপের কার্যকলাপের জন্য যে অনুমতিগুলি লাগে।\"\n        },\n        \"zh_TW\": {\n            \"name\": \"Kali NetHunter\",\n            \"description\": \"提供Kali NetHunter使用chroot\"\n        },\n        \"pt_BR\": {\n            \"name\": \"Kali NetHunter\",\n            \"description\": \"Permissões necessárias para o app Kali NetHunter fazer chroot.\"\n        },\n        \"it_IT\": {\n            \"name\": \"Kali NetHunter\",\n            \"description\": \"Autorizzazioni richieste per il chroot dell'applicazione Kali NetHunter.\"\n        } \n    }\n}\n"
  },
  {
    "path": "website/docs/public/templates/rootexploler.root",
    "content": "{\n    \"id\":\"rootexploler.root\",\n    \"name\":\"Root Explorer\",\n    \"author\":\"Rem01Gaming\",\n    \"description\":\"File manager with Root capabilities.\",\n    \"uid\":0,\n    \"gid\":0,\n    \"groups\":[\n        \"ROOT\"\n    ],\n    \"capabilities\":[\n        \"CAP_DAC_READ_SEARCH\",\n        \"CAP_DAC_OVERRIDE\",\n        \"CAP_SYS_ADMIN\"\n    ],\n    \"context\":\"u:r:su:s0\",\n    \"namespace\":\"INHERITED\",\n    \"locales\": {\n        \"bn\": {\n            \"name\": \"রুট এক্সপ্লোরার\",\n            \"description\": \"রুট অনুমতি যুক্ত ফাইল ম্যানেজার।\"\n        },\n        \"zh_TW\": {\n            \"name\": \"Root Explorer\",\n            \"description\": \"具有根目錄讀寫權限的檔案管理器\"\n        },\n        \"in\": {\n            \"name\": \"Root Explorer\",\n            \"description\": \"File manager dengan kemampuan Root.\"\n        },\n        \"tr\": {\n            \"name\": \"Root Dosya Tarayıcı\",\n            \"description\": \"Root özelliklerine sahip dosya yöneticisi.\"\n        },\n        \"pt_BR\": {\n            \"name\": \"Explorador root\",\n            \"description\": \"Gerenciador de arquivos com recursos root.\"\n        },\n        \"ja\": {\n            \"name\": \"Root ブラウザ\",\n            \"description\": \"Root 機能を備えたファイル マネージャー。\"\n        },\n        \"it_IT\": {\n            \"name\": \"Gestore file root\",\n            \"description\": \"Gestore file con permessi root.\"\n        } \n    }\n}\n"
  },
  {
    "path": "website/docs/public/templates/shizuku.root",
    "content": "{\n    \"id\":\"shizuku.root\",\n    \"name\":\"Shizuku Service\",\n    \"author\":\"Rem01Gaming & JohnRTitor\",\n    \"description\":\"Only essential permissions to start Shizuku service. Shizuku modules aren't guaranteed to work.\",\n    \"uid\":0,\n    \"gid\":0,\n    \"groups\":[\n        \"SHELL\"\n    ],\n    \"capabilities\":[\n        \"CAP_DAC_OVERRIDE\",\n        \"CAP_CHOWN\"\n    ],\n    \"context\":\"u:r:su:s0\",\n    \"namespace\":\"INHERITED\",\n    \"locales\": {\n        \"zh_TW\": {\n            \"name\": \"Shizuku\",\n            \"description\": \"只有啟動 Shizuku 服務所需的權限\"\n        },\n        \"bn\": {\n            \"name\": \"শিজুকু সার্ভিস\",\n            \"description\": \"শিজুকু চালানোর জন্য শুধু যে অনুমতি গুলি লাগে।\"\n        },\n        \"tr\": {\n            \"name\": \"Shizuku\",\n            \"description\": \"Shizuku servisini başlatmak için yalnızca gerekli izinler.\"\n        },\n        \"pt_BR\": {\n            \"name\": \"Shizuku\",\n            \"description\": \"Apenas permissões essenciais para iniciar o serviço Shizuku. Não há garantia de que os módulos Shizuku funcionem.\"\n        },\n        \"ja\": {\n            \"name\": \"Shizuku\",\n            \"description\": \"Shizuku サービスを開始するために必要な権限のみ。\"\n        },\n        \"it_IT\": {\n            \"name\": \"Shizuku\",\n            \"description\": \"Permessi essenziali per avviare il servizio Shizuku.\"\n        } \n    }\n}\n"
  },
  {
    "path": "website/docs/public/templates/system",
    "content": "{\n    \"id\":\"system\",\n    \"author\":\"kernelsu.org\",\n    \"name\":\"System\",\n    \"description\":\"The permission level at which the Android system runs without any capability.\",\n    \"uid\":1000,\n    \"gid\":1000,\n    \"groups\":[\n        \"SYSTEM\"\n    ],\n    \"locales\": {\n        \"bn\": {\n            \"name\": \"সিস্টেম\",\n            \"description\": \"অ্যান্ড্রয়েড সিস্টেম যে অনুমতিগুলি নিয়ে কাজ করে।\"\n        },\n        \"zh_CN\": {\n            \"name\": \"Android 系统\",\n            \"description\": \"Android 系统运行的权限级别，但没有任何权能。\"\n        },\n        \"zh_TW\": {\n            \"name\": \"Android 系統\",\n            \"description\": \"Android 系統運作的權限級別，但沒有任何權能\"\n        },\n        \"tr\": {\n            \"name\": \"Sistem\",\n            \"description\": \"Android sisteminin herhangi bir yetenek olmadan çalıştığı izin düzeyi.\"\n        },\n        \"pt_BR\": {\n            \"name\": \"Sistema\",\n            \"description\": \"O nível de permissão no qual o sistema Android é executado sem qualquer capacidade.\"\n        },\n        \"ja\": {\n            \"name\": \"System\",\n            \"description\": \"Android システムが実行される許可レベルですが、機能はありません。\"\n        },\n        \"it_IT\": {\n            \"name\": \"Sistema\",\n            \"description\": \"Il livello di permessi con il quale viene eseguito il sistema operativo Android senza alcuna capacità.\"\n        } \n    }\n}\n"
  },
  {
    "path": "website/docs/public/templates/wireguard.root",
    "content": "{\n    \"id\":\"wireguard.root\",\n    \"name\":\"Wireguard kernel module function\",\n    \"author\":\"hotfur\",\n    \"description\":\"Essential permissions for a working Wireguard kernel module backend. The optional Wireguard command line tools installation requires DAC_OVERRIDE for writing binaries to /system/bin. Because it is optional for operation, DAC_OVERRIDE is not granted here but you can grant the capability temporarily then revoke it after the app installed the command line binaries.\",\n    \"uid\":0,\n    \"gid\":0,\n    \"groups\":[\n        \"ROOT\"\n    ],\n    \"capabilities\":[\n        \"CAP_DAC_READ_SEARCH\",\n        \"CAP_NET_ADMIN\",\n        \"CAP_NET_RAW\"\n    ],\n    \"context\":\"u:r:su:s0\",\n    \"namespace\":\"INHERITED\",\n    \"locales\": {\n        \"pt_BR\": {\n            \"name\": \"Função do módulo kernel Wireguard\",\n            \"description\": \"Permissões essenciais para um backend funcional do módulo do kernel do Wireguard. A instalação opcional das ferramentas da linha de comando do Wireguard requer DAC_OVERRIDE para gravar binários em /system/bin. Como é opcional para a operação, DAC_OVERRIDE não é concedido aqui, mas você pode conceder o recurso temporariamente e revogá-lo após o app instalar os binários da linha de comando.\"\n        }\n    }\n}\n"
  },
  {
    "path": "website/docs/repos.json",
    "content": "[\n    {\n        \"maintainer\": \"diphons\",\n        \"maintainer_link\": \"https://github.com/diphons\",\n        \"kernel_name\": \"kernel_xiaomi_sm8250\",\n        \"kernel_link\": \"https://github.com/diphons/kernel_xiaomi_sm8250/tree/main\",\n        \"devices\": \"Poco F3: alioth | Poco F4: munch | MI10T/PRO: Apollo\"\n    },\n    {\n        \"maintainer\": \"diphons\",\n        \"maintainer_link\": \"https://github.com/diphons\",\n        \"kernel_name\": \"D8G_Kernel_SM8150\",\n        \"kernel_link\": \"https://github.com/diphons/D8G_Kernel_SM8150/tree/13\",\n        \"devices\": \"Poco X3 Pro: Vayu | Bhima\"\n    },\n    {\n        \"maintainer\": \"diphons\",\n        \"maintainer_link\": \"https://github.com/diphons\",\n        \"kernel_name\": \"kernel_xiaomi_sdm845\",\n        \"kernel_link\": \"https://github.com/diphons/kernel_xiaomi_sdm845/tree/main\",\n        \"devices\": \"Poco F1\"\n    },\n    {\n        \"maintainer\": \"diphons\",\n        \"maintainer_link\": \"https://github.com/diphons\",\n        \"kernel_name\": \"kernel_xiaomi_sm8350\",\n        \"kernel_link\": \"https://github.com/diphons/kernel_xiaomi_sm8350/tree/15\",\n        \"devices\": \"MI11T Pro: vili\"\n    },\n    {\n        \"maintainer\": \"diphons\",\n        \"maintainer_link\": \"https://github.com/diphons\",\n        \"kernel_name\": \"D8G_Kernel_Marble\",\n        \"kernel_link\": \"https://github.com/diphons/D8G_Kernel_Marble/tree/op\",\n        \"devices\": \"Poco F5 | Redmi Note 12 Turbo | Marble\"\n    },\n    {\n        \"maintainer\": \"diphons\",\n        \"maintainer_link\": \"https://github.com/diphons\",\n        \"kernel_name\": \"kernel_xiaomi_sm8635\",\n        \"kernel_link\": \"https://github.com/diphons/kernel_xiaomi_sm8635/tree/main\",\n        \"devices\": \"Poco F6: peridot\"\n    },\n    {\n        \"maintainer\": \"th1nhhdk\",\n        \"maintainer_link\": \"https://github.com/th1nhhdk\",\n        \"kernel_name\": \"android_kernel_sony_sm8250-kernelsu\",\n        \"kernel_link\": \"https://github.com/th1nhhdk/android_kernel_sony_sm8250-kernelsu\",\n        \"devices\": \"Sony Xperia 1 II | Sony Xperia 5 II\"\n    },\n    {\n        \"maintainer\": \"akash07k\",\n        \"maintainer_link\": \"https://github.com/akash07k\",\n        \"kernel_name\": \"nexus_kernel_xiaomi_sm8250\",\n        \"kernel_link\": \"https://github.com/akash07k/nexus_kernel_xiaomi_sm8250/tree/lychee\",\n        \"devices\": \"Poco F4: munch\"\n    },\n    {\n        \"maintainer\": \"Hadad\",\n        \"maintainer_link\": \"https://xdaforums.com/m/hadad.8351572/\",\n        \"kernel_name\": \"android_kernel_xiaomi_onc\",\n        \"kernel_link\": \"https://github.com/hadadarjt/android_kernel_xiaomi_onc/tree/los21-KSU/local-non-gerrit-review\",\n        \"devices\": \"Redmi 7: onclite | Redmi Y3: onc\"\n    },\n    {\n        \"maintainer\": \"HMTheBoy154\",\n        \"maintainer_link\": \"https://github.com/hmtheboy154\",\n        \"kernel_name\": \"Darkmatter-kernel\",\n        \"kernel_link\": \"https://github.com/hmtheboy154/Darkmatter-kernel\",\n        \"devices\": \"Generic x86_64 devices running Android-x86\"\n    },\n    {\n        \"maintainer\": \"Asuka-mio\",\n        \"maintainer_link\": \"https://github.com/asuka-mio\",\n        \"kernel_name\": \"android_kernel_xiaomi_cas\",\n        \"kernel_link\": \"https://github.com/AcmeUI-Devices/android_kernel_xiaomi_cas\",\n        \"devices\": \"Mi 10 Ultra: cas\"\n    },\n    {\n        \"maintainer\": \"xiaoleGun\",\n        \"maintainer_link\": \"https://github.com/xiaoleGun\",\n        \"kernel_name\": \"Miku_kernel_xiaomi_wayne\",\n        \"kernel_link\": \"https://github.com/Diva-Room/Miku_kernel_xiaomi_wayne\",\n        \"devices\": \"wayne\"\n    },\n    {\n        \"maintainer\": \"SakuraNotStupid\",\n        \"maintainer_link\": \"https://github.com/SakuraKyuo\",\n        \"kernel_name\": \"android_kernel_xiaomi_sdm710\",\n        \"kernel_link\": \"https://github.com/SakuraKyuo/android_kernel_xiaomi_sdm710\",\n        \"devices\": \"Xiaomi MI 8 SE(sirius/xmsirius)\"\n    },\n    {\n        \"maintainer\": \"zlm324\",\n        \"maintainer_link\": \"https://github.com/zlm324\",\n        \"kernel_name\": \"android_kernel_xiaomi_msm8998\",\n        \"kernel_link\": \"https://github.com/zlm324/android_kernel_xiaomi_msm8998_ksu\",\n        \"devices\": \"MI 6 (sagit) and MIX 2 (chiron) for LineageOS\"\n    },\n    {\n        \"maintainer\": \"SlackerState\",\n        \"maintainer_link\": \"https://github.com/SlackerState\",\n        \"kernel_name\": \"android_kernel_xiaomi_sm6150\",\n        \"kernel_link\": \"https://github.com/SlackerState/android_kernel_xiaomi_sm6150\",\n        \"devices\": \"Redmi K30 4G (phoenix)\"\n    },\n    {\n        \"maintainer\": \"RooGhz720\",\n        \"maintainer_link\": \"https://github.com/RooGhz720\",\n        \"kernel_name\": \"kernel_xiaomi_sm6150\",\n        \"kernel_link\": \"https://github.com/RooGhz720/kernel_xiaomi_sm6150\",\n        \"devices\": \"REDMI NOTE 10 PRO (sweet)\"\n    },\n    {\n        \"maintainer\": \"OnlyTomInSecond\",\n        \"maintainer_link\": \"https://github.com/onlyTomInSecond/\",\n        \"kernel_name\": \"android_kernel_xiaomi_sdm845\",\n        \"kernel_link\": \"https://github.com/OnlyTomInSecond/android_kernel_xiaomi_sdm845\",\n        \"devices\": \"Mi 8 (dipper) for LineageOS\"\n    },\n    {\n        \"maintainer\": \"Rohail33\",\n        \"maintainer_link\": \"https://github.com/Rohail33\",\n        \"kernel_name\": \"RealKing Kernel\",\n        \"kernel_link\": \"https://github.com/Rohail33/Realking_kernel_sm8250\",\n        \"devices\": \"Apollo(Redmi K30S Ultra/Mi 10T/Mi 10T Pro)\\uff0cAlioth(Redmi K40/POCO F3/Mi 11X)\\uff0cMunch(Redmi K40S/POCO F4), both MIUI and AOSP.\"\n    },\n    {\n        \"maintainer\": \"Sreeshankar K\",\n        \"maintainer_link\": \"https://github.com/sreeshankark\",\n        \"kernel_name\": \"NeverSettle Kernel\",\n        \"kernel_link\": \"https://github.com/sreeshankark/android_kernel_oneplus_avicii\",\n        \"devices\": \"OnePlus Nord (avicii)\"\n    },\n    {\n        \"maintainer\": \"PSavarMattas\",\n        \"maintainer_link\": \"https://github.com/psavarmattas\",\n        \"kernel_name\": \"PSM Kernel\",\n        \"kernel_link\": \"https://github.com/psavarmattas/android_kernel_oneplus_sm7250-WKSU\",\n        \"devices\": \"OnePlus Nord (avicii)\"\n    },\n    {\n        \"maintainer\": \"Molyuu\",\n        \"maintainer_link\": \"https://github.com/Molyuu\",\n        \"kernel_name\": \"neko_kernel_xiaomi_gauguin\",\n        \"kernel_link\": \"https://github.com/Molyuu/neko_kernel_xiaomi_gauguin\",\n        \"devices\": \"Redmi Note 9 Pro/ Mi 10T Lite/ Mi 10i \"\n    },\n    {\n        \"maintainer\": \"guh0613\",\n        \"maintainer_link\": \"https://github.com/guh0613\",\n        \"kernel_name\": \"android_kernel_oppo_sm8150\",\n        \"kernel_link\": \"https://github.com/guh0613/android_kernel_oppo_sm8150\",\n        \"devices\": \"OPPO Reno Ace (OP4A89)\"\n    },\n    {\n        \"maintainer\": \"LeviMarvin\",\n        \"maintainer_link\": \"https://github.com/LeviMarvin\",\n        \"kernel_name\": \"android_kernel_xiaomi_alioth\",\n        \"kernel_link\": \"https://github.com/LeviMarvin/android_kernel_xiaomi_alioth\",\n        \"devices\": \"Redmi K40 / POCO F3\"\n    },\n    {\n        \"maintainer\": \"cibimo\",\n        \"maintainer_link\": \"https://github.com/cibimo\",\n        \"kernel_name\": \"kernel_xiaomi_raphael_ksu\",\n        \"kernel_link\": \"https://github.com/cibimo/kernel_xiaomi_raphael_ksu\",\n        \"devices\": \"Xiaomi Redmi K20 Pro / Mi 9T Pro (raphael)\"\n    },\n    {\n        \"maintainer\": \"EndCredits\",\n        \"maintainer_link\": \"https://github.com/EndCredits\",\n        \"kernel_name\": \"kernel_xiaomi_sm7250\",\n        \"kernel_link\": \"https://github.com/EndCredits/kernel_xiaomi_sm7250\",\n        \"devices\": \"Redmi K30 5G ( picasso ) , Redmi K30i 5G ( picasso_48m)\"\n    },\n    {\n        \"maintainer\": \"msnx\",\n        \"maintainer_link\": \"https://github.com/msnx\",\n        \"kernel_name\": \"android-msm-coral-4.14-android13\",\n        \"kernel_link\": \"https://github.com/msnx/KernelSU-Pixel4XL\",\n        \"devices\": \"Pixel 4 XL\"\n    },\n    {\n        \"maintainer\": \"SoDebug\",\n        \"maintainer_link\": \"https://github.com/SoDebug\",\n        \"kernel_name\": \"kernel_xiaomi_raphael\",\n        \"kernel_link\": \"https://github.com/SoDebug/KernelSU_Kernel_Raphael\",\n        \"devices\": \"Xiaomi Redmi K20 Pro / Mi 9T Pro (raphael)\"\n    },\n    {\n        \"maintainer\": \"H1mJT\",\n        \"maintainer_link\": \"https://github.com/H1mJT\",\n        \"kernel_name\": \"kernel_realme_RMX1901\",\n        \"kernel_link\": \"https://github.com/H1mJT/kernel_realme_RMX1901\",\n        \"devices\": \"Realme X (RMX1901/RMX1901CN)\"\n    },\n    {\n        \"maintainer\": \"SonalSingh18\",\n        \"maintainer_link\": \"https://github.com/SonalSingh18\",\n        \"kernel_name\": \"android_kernel_xiaomi_sm6250\",\n        \"kernel_link\": \"https://github.com/SonalSingh18/android_kernel_xiaomi_sm6250\",\n        \"devices\": \"Miatoll [curtana, excalibur, gram, joyeuse]\"\n    },\n    {\n        \"maintainer\": \"RooGhz720\",\n        \"maintainer_link\": \"https://github.com/RooGhz720\",\n        \"kernel_name\": \"kernel_xiaomi_lavender\",\n        \"kernel_link\": \"https://github.com/RooGhz720/kernel_xiaomi_lavender\",\n        \"devices\": \"Redmi Note 7 (Lavender)\"\n    },\n    {\n        \"maintainer\": \"JunASAKA\",\n        \"maintainer_link\": \"https://github.com/JunASAKA\",\n        \"kernel_name\": \"kernel_google_msm-4.9_KernelSU\",\n        \"kernel_link\": \"https://github.com/JunASAKA/kernel_google_msm-4.9_KernelSU\",\n        \"devices\": \"Google Pixel 3a & 3a XL (sargo & bonito)\"\n    },\n    {\n        \"maintainer\": \"RooGhz720\",\n        \"maintainer_link\": \"https://github.com/RooGhz720\",\n        \"kernel_name\": \"kernel_asus_X01BD\",\n        \"kernel_link\": \"https://github.com/RooGhz720/kernel_asus_X01BD\",\n        \"devices\": \"Asus Zenfone Max Pro M1/M2\"\n    },\n    {\n        \"maintainer\": \"Evans Mike\",\n        \"maintainer_link\": \"https://github.com/etnperlong\",\n        \"kernel_name\": \"kernel_xiaomi_raphael_bool-x\",\n        \"kernel_link\": \"https://github.com/etnperlong/kernel_xiaomi_raphael_bool-x\",\n        \"devices\": \"Xiaomi Redmi K20 Pro / Mi 9T Pro (raphael)\"\n    },\n    {\n        \"maintainer\": \"easterNday\",\n        \"maintainer_link\": \"https://github.com/DogDayAndroid\",\n        \"kernel_name\": \"KSU_Thyme_BuildBot\",\n        \"kernel_link\": \"https://github.com/DogDayAndroid/KSU_Thyme_BuildBot\",\n        \"devices\": \"Xiaomi 10S\"\n    },\n    {\n        \"maintainer\": \"tedomi2705\",\n        \"maintainer_link\": \"https://github.com/tedomi2705\",\n        \"kernel_name\": \"kernel_xiaomi_sdm660\",\n        \"kernel_link\": \"https://github.com/tedomi2705/kernel_xiaomi_sdm660\",\n        \"devices\": \"Xiaomi Redmi Note 6 Pro (tulip)\"\n    },\n    {\n        \"maintainer\": \"RyuujiX\",\n        \"maintainer_link\": \"https://github.com/RyuujiX\",\n        \"kernel_name\": \"android_kernel_asus_sdm660-4.19\",\n        \"kernel_link\": \"https://github.com/RyuujiX/android_kernel_asus_sdm660-4.19\",\n        \"devices\": \"Asus Zenfone Max Pro M1 (X00TD)/ M2 (X01BD) (Linux 4.19)\"\n    },\n    {\n        \"maintainer\": \"liqidecg\",\n        \"maintainer_link\": \"https://github.com/liqidecg\",\n        \"kernel_name\": \"android_kernel_xiaomi_sdm710\",\n        \"kernel_link\": \"https://github.com/liqidecg/android_kernel_xiaomi_sdm710\",\n        \"devices\": \"Mi 8 SE (sirius) (Linux 4.9)\"\n    },\n    {\n        \"maintainer\": \"Abdelhay-Ali\",\n        \"maintainer_link\": \"https://github.com/Abdelhay-Ali\",\n        \"kernel_name\": \"android_kernel_huawei_hi6250\",\n        \"kernel_link\": \"https://github.com/Abdelhay-Ali/android_kernel_huawei_hi6250_KernelSU\",\n        \"devices\": \"Huawei P20 lite (hi6250) (Linux 4.9)\"\n    },\n    {\n        \"maintainer\": \"reocat\",\n        \"maintainer_link\": \"https://github.com/reocat\",\n        \"kernel_name\": \"android_kernel_nokia_sdm660_ksu\",\n        \"kernel_link\": \"https://github.com/reocat/android_kernel_nokia_sdm660_ksu\",\n        \"devices\": \"Nokia 6.1 (2018)\"\n    },\n    {\n        \"maintainer\": \"Tuan Anh\",\n        \"maintainer_link\": \"https://github.com/log1cs\",\n        \"kernel_name\": \"kernel_nokia_msm8998\",\n        \"kernel_link\": \"https://github.com/log1cs/kernel_nokia_msm8998\",\n        \"devices\": \"Nokia 8 (Repartitioned)/8 Sirocco (NLA/A1N)\"\n    },\n    {\n        \"maintainer\": \"bggRGjQaUbCoE\",\n        \"maintainer_link\": \"https://github.com/bggRGjQaUbCoE\",\n        \"kernel_name\": \"android_kernel_samsung_sm8250\",\n        \"kernel_link\": \"https://github.com/bggRGjQaUbCoE/android_kernel_samsung_sm8250-mohammad92\",\n        \"devices\": \"Samsung Galaxy S20+ 5G (y2q)\"\n    },\n    {\n        \"maintainer\": \"SOVIET-ANDROID\",\n        \"maintainer_link\": \"https://github.com/SOVIET-ANDROID\",\n        \"kernel_name\": \"kernel_xiaomi_raphael\",\n        \"kernel_link\": \"https://github.com/SOVIET-ANDROID/kernel_xiaomi_raphael\",\n        \"devices\": \"XK20 Pro Raphael  DSP & A only SAR support \"\n    },\n    {\n        \"maintainer\": \"dabao1955\",\n        \"maintainer_link\": \"https://github.com/dabao1955\",\n        \"kernel_name\": \"android_kernel_OPPO_PEQM00\",\n        \"kernel_link\": \"https://github.com/dabao1955/android_kernel_OPPO_PEQM00\",\n        \"devices\": \"MediaTek devices in the OPPO Reno series on ColorOS13.x\"\n    },\n    {\n        \"maintainer\": \"Aflaungos\",\n        \"maintainer_link\": \"https://github.com/Aflaungos\",\n        \"kernel_name\": \"android_kernel_motorola_msm8998\",\n        \"kernel_link\": \"https://github.com/Aflaungos/android_kernel_motorola_msm8998\",\n        \"devices\": \"Moto G6 Plus (evert)\"\n    },\n    {\n        \"maintainer\": \"TheNoFace\",\n        \"maintainer_link\": \"https://github.com/TheNoFace\",\n        \"kernel_name\": \"kernel_oneplus_msm8998\",\n        \"kernel_link\": \"https://github.com/TheNoFace/kernel_oneplus_msm8998/tree/pe-13-ksu\",\n        \"devices\": \"OnePlus 5/5T (cheeseburger/dumpling)\"\n    },\n    {\n        \"maintainer\": \"Nipin NA (Joker-V2)\",\n        \"maintainer_link\": \"https://github.com/Joker-V2\",\n        \"kernel_name\": \"Xcalibur kernel (violet)\",\n        \"kernel_link\": \"https://github.com/Joker-V2/kernel_xiaomi_violet/tree/kernelsu\",\n        \"devices\": \"Xiaomi Redmi Note 7 Pro (violet)\"\n    },\n    {\n        \"maintainer\": \"Sr-Han\",\n        \"maintainer_link\": \"https://github.com/Sr-Han\",\n        \"kernel_name\": \"kernel_xiaomi_mojito\",\n        \"kernel_link\": \"https://github.com/Sr-Han/kernel_xiaomi_mojito\",\n        \"devices\": \"Redmi Note 10 (Sunny/Mojito)\"\n    },\n    {\n        \"maintainer\": \"rushiranpise\",\n        \"maintainer_link\": \"https://github.com/rushiranpise\",\n        \"kernel_name\": \"android_kernel_samsung_exynos9610_mint\",\n        \"kernel_link\": \"https://github.com/rushiranpise/android_kernel_samsung_exynos9610_mint\",\n        \"devices\": \"Kernel 4.14.194 exynos9610 Non-GKI Device, Added KernelSu using manual method\"\n    },\n    {\n        \"maintainer\": \"CN-Scars\",\n        \"maintainer_link\": \"https://github.com/CN-Scars\",\n        \"kernel_name\": \"pixel_experience_kernel_xiaomi_sm8250_kernelSU\",\n        \"kernel_link\": \"https://github.com/CN-Scars/pixel_experience_kernel_xiaomi_sm8250_kernelSU\",\n        \"devices\": \"Pixel Experience Kernel 4.19.282 for Xiaomi Redmi K40S / Xiaomi Poco F4 (munch)\"\n    },\n    {\n        \"maintainer\": \"exer\",\n        \"maintainer_link\": \"https://github.com/ekkusa\",\n        \"kernel_name\": \"Miyo Toku\",\n        \"kernel_link\": \"https://github.com/Miiyo/android_kernel_sony_sdm845/tree/KSU/Toku\",\n        \"devices\": \"Sony Tama (akari, apollo, aurora, akatsuki) (Linux 4.9)\"\n    },\n    {\n        \"maintainer\": \"likkai\",\n        \"maintainer_link\": \"https://github.com/likkai\",\n        \"kernel_name\": \"Quicksilver Kernel\",\n        \"kernel_link\": \"https://github.com/likkai/ksu_kernel_xiaomi_lisa\",\n        \"devices\": \"Xiaomi 11 Lite 5G NE (lisa)\"\n    },\n    {\n        \"maintainer\": \"awakened1712\",\n        \"maintainer_link\": \"https://github.com/awakened1712\",\n        \"kernel_name\": \"android_kernel_samsung_exynos9820\",\n        \"kernel_link\": \"https://github.com/awakened1712/android_kernel_samsung_exynos9820\",\n        \"devices\": \"Samsung S10/N10\"\n    },\n    {\n        \"maintainer\": \"awakened1712\",\n        \"maintainer_link\": \"https://github.com/awakened1712\",\n        \"kernel_name\": \"android_kernel_oneplus_sm8350\",\n        \"kernel_link\": \"https://github.com/awakened1712/android_kernel_oneplus_sm8350\",\n        \"devices\": \"Oneplus 9/9Pro\"\n    },\n    {\n        \"maintainer\": \"siimsek\",\n        \"maintainer_link\": \"https://github.com/siimsek\",\n        \"kernel_name\": \"Lightning Kernel\",\n        \"kernel_link\": \"https://github.com/siimsek/Lightning-Kernel\",\n        \"devices\": \"Xiaomi Redmi Note 8/8T (ginkgo/willow)\"\n    },\n    {\n        \"maintainer\": \"GiovanYCringe\",\n        \"maintainer_link\": \"https://github.com/GiovanYCringe\",\n        \"kernel_name\": \"kernel_a50\",\n        \"kernel_link\": \"https://github.com/GiovanYCringe-Experiments/kernel_a50\",\n        \"devices\": \"Galaxy A50 (A505f,fn,g,gn,gt)\"\n    },\n    {\n        \"maintainer\": \"Asriadi Rahim\",\n        \"maintainer_link\": \"https://github.com/asriadirahim\",\n        \"kernel_name\": \"android_kernel_google_wahoo\",\n        \"kernel_link\": \"https://github.com/Google-Pixel2-2XL/kernel_google_wahoo\",\n        \"devices\": \"Google Pixel 2/2XL\"\n    },\n    {\n        \"maintainer\": \"DawfukFR\",\n        \"maintainer_link\": \"https://github.com/DawfukFR\",\n        \"kernel_name\": \"kernel_oneplus_sm8250 (Stellaris-Kernel)\",\n        \"kernel_link\": \"https://github.com/DawfukFR/kernel_oneplus_sm8250\",\n        \"devices\": \"Oneplus 8 (Instantnoodle), Oneplus 8T (Kebab), Oneplus 8 Pro (Instantnoodlep), Oneplus 9R (lemonades) [msm-4.19]\"\n    },\n    {\n        \"maintainer\": \"Tejas Singh\",\n        \"maintainer_link\": \"https://github.com/tejas101k\",\n        \"kernel_name\": \"kernel_xiaomi_ginkgo (Cuh Kernel)\",\n        \"kernel_link\": \"https://github.com/tejas101k/Cuh-KerneL\",\n        \"devices\": \"Xiaomi Redmi Note 8/8T (ginkgo/willow)\"\n    },\n    {\n        \"maintainer\": \"Abdul Wahid Khan\",\n        \"maintainer_link\": \"https://github.com/Wahid7852\",\n        \"kernel_name\": \"kernel_xiaomi_begonia\",\n        \"kernel_link\": \"https://github.com/Nova-Kernels/kernel_xiaomi_mt6785\",\n        \"devices\": \"Redmi Note 8 Pro (Begonia/Begoniain)\"\n    },\n    {\n        \"maintainer\": \"zfdx123\",\n        \"maintainer_link\": \"https://github.com/zfdx123\",\n        \"kernel_name\": \"kernel_xiaomi_alioth\",\n        \"kernel_link\": \"https://github.com/zfdx123/kernel_xiaomi_alioth\",\n        \"devices\": \"Redmi K40 (alioth)\"\n    },\n    {\n        \"maintainer\": \"Bot-wxt1221\",\n        \"maintainer_link\": \"https://github.com/Bot-wxt1221\",\n        \"kernel_name\": \"android_kernel_oneplus_sdm845\",\n        \"kernel_link\": \"https://github.com/Bot-wxt1221/android_kernel_oneplus_sdm845\",\n        \"devices\": \"Oneplus 6 (enchilada) | Oneplus 6T (fajita) with Retrofit Dynamic Partitions Only with crdroid\"\n    },\n    {\n        \"maintainer\": \"zlm324\",\n        \"maintainer_link\": \"https://github.com/zlm324\",\n        \"kernel_name\": \"android_kernel_xiaomi_msm8998_ksu\",\n        \"kernel_link\": \"https://github.com/zlm324/android_kernel_xiaomi_msm8998_ksu\",\n        \"devices\": \"a lineage kernel forked from LineageOS official repository, version 4.4.302, added ksu.\"\n    },\n    {\n        \"maintainer\": \"zlm324\",\n        \"maintainer_link\": \"https://github.com/zlm324\",\n        \"kernel_name\": \"android_kernel_xiaomi_msm8953_ksu\",\n        \"kernel_link\": \"https://github.com/zlm324/android_kernel_xiaomi_msm8953_ksu\",\n        \"devices\": \"Xiaomi 5X (tiffany)\"\n    },\n    {\n        \"maintainer\": \"Vincent4440\",\n        \"maintainer_link\": \"https://github.com/Vincent4440\",\n        \"kernel_name\": \"android_kernel_xiaomi_sm8250\",\n        \"kernel_link\": \"https://github.com/Vincent4440/android_kernel_xiaomi_sm8250\",\n        \"devices\": \"Poco F4: munch\"\n    },\n    {\n        \"maintainer\": \"tiandoufayale\",\n        \"maintainer_link\": \"https://github.com/tiandoufayale\",\n        \"kernel_name\": \"android_kernel_samsung_sm8250\",\n        \"kernel_link\": \"https://github.com/tiandoufayale/android_kernel_samsung_sm8250\",\n        \"devices\": \"Samsung Tab S7 WIFI(T870)\"\n    },\n    {\n        \"maintainer\": \"starmoe\",\n        \"maintainer_link\": \"https://github.com/bro-xun\",\n        \"kernel_name\": \"android_kernel_oneplus_msm8998\",\n        \"kernel_link\": \"https://github.com/Bro-Xun/kernel_op5t_ksu\",\n        \"devices\": \"Oneplus 5/5T, kernel version 4.4.302\"\n    },\n    {\n        \"maintainer\": \"supechicken\",\n        \"maintainer_link\": \"https://github.com/supechicken\",\n        \"kernel_name\": \"kernel_xiaomi_sm8350\",\n        \"kernel_link\": \"https://github.com/supechicken/kernel_xiaomi_sm8350\",\n        \"devices\": \" Xiaomi 11T Pro (vili)\"\n    },\n    {\n        \"maintainer\": \"Vaz15k\",\n        \"maintainer_link\": \"https://github.com/Vaz15k\",\n        \"kernel_name\": \"kernelSU-A70q\",\n        \"kernel_link\": \"https://github.com/Vaz15k/kernelSU-A70q\",\n        \"devices\": \"Samsung Galaxy A70 ( A705MN / A705FN )\"\n    },\n    {\n        \"maintainer\": \"whyakari\",\n        \"maintainer_link\": \"https://github.com/whyakari\",\n        \"kernel_name\": \"android_kernel_xiaomi_ginkgo\",\n        \"kernel_link\": \"https://github.com/whyakari/android_kernel_xiaomi_ginkgo\",\n        \"devices\": \"Xiaomi Redmi Note 8/8T (ginkgo/willow)\"\n    },\n    {\n        \"maintainer\": \"wulan17\",\n        \"maintainer_link\": \"https://github.com/wulan17\",\n        \"kernel_name\": \"android_kernel_xiaomi_mt6765\",\n        \"kernel_link\": \"https://github.com/Mayuri-Chan/android_kernel_xiaomi_mt6765/tree/10-ksu\",\n        \"devices\": \"Redmi 6/6A (cereus/cactus)\"\n    },\n    {\n        \"maintainer\": \"officialputuid\",\n        \"maintainer_link\": \"https://github.com/officialputuid\",\n        \"kernel_name\": \"android_kernel_realme_mt6785\",\n        \"kernel_link\": \"https://github.com/psionicprjkt/android_kernel_realme_mt6785\",\n        \"devices\": \"Realme 6/6i(Indian)/6s/7/Narzo/Narzo 20 Pro/Narzo 30 4G (RM6785)\"\n    },\n    {\n        \"maintainer\": \"liquidprjkt\",\n        \"maintainer_link\": \"https://github.com/liquidprjkt\",\n        \"kernel_name\": \"liquid_kernel_realme_even\",\n        \"kernel_link\": \"https://github.com/liquidprjkt/liquid_kernel_realme_even\",\n        \"devices\": \"Realme C25/s and Narzo50A (even)\"\n    },\n    {\n        \"maintainer\": \"Tonklaistonton\",\n        \"maintainer_link\": \"https://github.com/Tonklaistonton\",\n        \"kernel_name\": \"NEXGatokernel\",\n        \"kernel_link\": \"https://github.com/Tonklaistonton/android_kernel_oneplus_sm6375\",\n        \"devices\": \"Realme 9 pro 5g  | Oneplus nord ce 2 lite \"\n    },\n    {\n        \"maintainer\": \"CoolestEnoch\",\n        \"maintainer_link\": \"https://github.com/CoolestEnoch\",\n        \"kernel_name\": \"kernel-su-huawei-nova2\",\n        \"kernel_link\": \"https://github.com/CoolestEnoch/kernel-su-huawei-nova2\",\n        \"devices\": \"Huawei nova 2 (pic)\"\n    },\n    {\n        \"maintainer\": \"RisenID\",\n        \"maintainer_link\": \"https://github.com/RisenID\",\n        \"kernel_name\": \"kernel_samsung_ascendia_sm7325\",\n        \"kernel_link\": \"https://github.com/RisenID/kernel_samsung_ascendia_sm7325\",\n        \"devices\": \"Samsung Galaxy A52s 5G (a52sxq)\"\n    },\n    {\n        \"maintainer\": \"RisenID\",\n        \"maintainer_link\": \"https://github.com/RisenID\",\n        \"kernel_name\": \"kernel_samsung_ascendia_sm7125\",\n        \"kernel_link\": \"https://github.com/RisenID/kernel_samsung_ascendia_sm7125\",\n        \"devices\": \"Samsung Galaxy A52 4G (a52q)\"\n    },\n    {\n        \"maintainer\": \"RisenID\",\n        \"maintainer_link\": \"https://github.com/RisenID\",\n        \"kernel_name\": \"kernel_samsung_ascendia_sm7125\",\n        \"kernel_link\": \"https://github.com/RisenID/kernel_samsung_ascendia_sm7125\",\n        \"devices\": \"Samsung Galaxy A72 (a72q)\"\n    },\n    {\n        \"maintainer\": \"RisenID\",\n        \"maintainer_link\": \"https://github.com/RisenID\",\n        \"kernel_name\": \"kernel_samsung_ascendia_sm7325\",\n        \"kernel_link\": \"https://github.com/RisenID/kernel_samsung_ascendia_sm7325\",\n        \"devices\": \"Samsung Galaxy M52 5G (m52xq)\"\n    },\n    {\n        \"maintainer\": \"A-JiuA\",\n        \"maintainer_link\": \"https://github.com/A-JiuA\",\n        \"kernel_name\": \"sirius_Kernel\",\n        \"kernel_link\": \"https://github.com/A-JiuA/sirius_Kernel\",\n        \"devices\": \"Xiaomi MI 8 SE(MIUI12/12.5)\"\n    },\n    {\n        \"maintainer\": \"picasso09\",\n        \"maintainer_link\": \"https://github.com/picasso09\",\n        \"kernel_name\": \"KernelSU-RM6765R\",\n        \"kernel_link\": \"https://github.com/picasso09/RMX2189R/tree/c05-ksu\",\n        \"devices\": \"Realme C11 12 15 (RM6765)\"\n    },\n    {\n        \"maintainer\": \"josedelinux\",\n        \"maintainer_link\": \"https://github.com/josedelinux\",\n        \"kernel_name\": \"kernel_google_msm-4.9\",\n        \"kernel_link\": \"https://github.com/josedelinux/kernel_google_msm-4.9\",\n        \"devices\": \"Google Pixel 3a & 3a XL (sargo & bonito)\"\n    },\n    {\n        \"maintainer\": \"Lonelystar9x\",\n        \"maintainer_link\": \"https://github.com/lonelystar9x\",\n        \"kernel_name\": \"Kernel_xiaomi_sdm845\",\n        \"kernel_link\": \"https://github.com/lonelystar9x/android_kernel_xiaomi_sdm845\",\n        \"devices\": \"Poco F1 | MI8 | MiMix2S | MiMix3\"\n    },\n    {\n        \"maintainer\": \"iceBear67\",\n        \"maintainer_link\": \"https://github.com/iceBear67\",\n        \"kernel_name\": \"android_kernel_exynos9810_kernelsu\",\n        \"kernel_link\": \"https://github.com/iceBear67/android_kernel_exynos9810_kernelsu\",\n        \"devices\": \"Galaxy Note 9(N960N/F)\"\n    },\n    {\n        \"maintainer\": \"BlackMesa123\",\n        \"maintainer_link\": \"https://xdaforums.com/t/kernel-a546b-e-kernelsu-v0-7-5-for-galaxy-a54-5g.4607539/\",\n        \"kernel_name\": \"android_kernel_samsung_s5e8835\",\n        \"kernel_link\": \"https://github.com/BlackMesa123/android_kernel_samsung_s5e8835\",\n        \"devices\": \"Samsung Galaxy A54 5G (a54x)\"\n    },\n    {\n        \"maintainer\": \"Fede2782\",\n        \"maintainer_link\": \"https://github.com/Fede2782\",\n        \"kernel_name\": \"android_kernel_samsung_a34x\",\n        \"kernel_link\": \"https://github.com/Fede2782/android_kernel_samsung_a34x/\",\n        \"devices\": \"Samsung Galaxy A34 5G (a34x)\"\n    },\n    {\n        \"maintainer\": \"SnowWolf725    \",\n        \"maintainer_link\": \"https://github.com/snowwolf725\",\n        \"kernel_name\": \"android_kernel_oneplus_sm8150\",\n        \"kernel_link\": \"https://github.com/snowwolf725/android_kernel_oneplus_sm8150\",\n        \"devices\": \"Oneplus 7(guacamoleb)/7 Pro(guacamole)/7T(hotdog)/7TPro (hotdogb)\"\n    },\n    {\n        \"maintainer\": \"Fede2782\",\n        \"maintainer_link\": \"https://github.com/Fede2782\",\n        \"kernel_name\": \"android_kernel_samsung_gta4xl\",\n        \"kernel_link\": \"https://github.com/Fede2782/android_kernel_samsung_gta4xl\",\n        \"devices\": \"Galaxy Tab S6 Lite LTE (gta4xl)\"\n    },\n    {\n        \"maintainer\": \"ArchVisions\",\n        \"maintainer_link\": \"https://github.com/ArchVisions\",\n        \"kernel_name\": \"android_kernel_samsung_gts9fewifi\",\n        \"kernel_link\": \"https://github.com/ArchVisions/android_kernel_samsung_gts9fewifi\",\n        \"devices\": \"Samsung Tab S9 FE (gts9fewifi) | Exynos 1380\"\n    },\n    {\n        \"maintainer\": \"ravindu644\",\n        \"maintainer_link\": \"https://github.com/ravindu644\",\n        \"kernel_name\": \"android_kernel_beyondx_lpos\",\n        \"kernel_link\": \"https://github.com/ravindu644/android_kernel_beyondx_lpos\",\n        \"devices\": \"Galaxy S10 5G (KOR)\"\n    },\n    {\n        \"maintainer\": \"ravindu644\",\n        \"maintainer_link\": \"https://github.com/ravindu644\",\n        \"kernel_name\": \"android_kernel_beyond1_lpos\",\n        \"kernel_link\": \"https://github.com/ravindu644/android_kernel_beyond1_lpos\",\n        \"devices\": \"Galaxy S10\"\n    },\n    {\n        \"maintainer\": \"ravindu644\",\n        \"maintainer_link\": \"https://github.com/ravindu644\",\n        \"kernel_name\": \"android_kernel_beyond2\",\n        \"kernel_link\": \"https://github.com/ravindu644/android_kernel_beyond2\",\n        \"devices\": \"Galaxy S10+\"\n    },\n    {\n        \"maintainer\": \"ravindu644\",\n        \"maintainer_link\": \"https://github.com/ravindu644\",\n        \"kernel_name\": \"android_kernel_beyond0\",\n        \"kernel_link\": \"https://github.com/ravindu644/android_kernel_beyond0\",\n        \"devices\": \"Galaxy S10e\"\n    },\n    {\n        \"maintainer\": \"Rofikkernel\",\n        \"maintainer_link\": \"https://github.com/Rofikkernel\",\n        \"kernel_name\": \"kernel_lgv600tm_4.19\",\n        \"kernel_link\": \"https://github.com/Rofikkernel/kernel_lgv600tm_4.19\",\n        \"devices\": \"LG V60 timelm\"\n    },\n    {\n        \"maintainer\": \"Dawid2849\",\n        \"maintainer_link\": \"https://github.com/Dawid2849\",\n        \"kernel_name\": \"android_kernel_lge_sm8150\",\n        \"kernel_link\": \"https://github.com/Dawid2849/android_kernel_lge_sm8150\",\n        \"devices\": \"LG G8 (alphaplus) / LG G8s (betalm) / LG G8X (mh2mml)\"\n    },\n    {\n        \"maintainer\": \"lyc8503\",\n        \"maintainer_link\": \"https://github.com/lyc8503\",\n        \"kernel_name\": \"kernel_xiaomi_chopin\",\n        \"kernel_link\": \"https://github.com/ChopinKernels/kernel_xiaomi_chopin_android_S\",\n        \"devices\": \"Redmi Note 10 Pro 5G (chopin) / Poco X3 GT (choping)\"\n    },\n    {\n        \"maintainer\": \"Fede2782\",\n        \"maintainer_link\": \"https://github.com/Fede2782\",\n        \"kernel_name\": \"android_kernel_motorola_sdm632\",\n        \"kernel_link\": \"https://github.com/Fede2782/android_kernel_motorola_sdm632\",\n        \"devices\": \"Motorola Moto G7 (river)\"\n    },\n    {\n        \"maintainer\": \"EmanuelCN\",\n        \"maintainer_link\": \"https://github.com/EmanuelCN\",\n        \"kernel_name\": \"N0Kernel\",\n        \"kernel_link\": \"https://github.com/EmanuelCN/kernel_xiaomi_sm8250\",\n        \"devices\": \"Poco F3 (alioth) | Mi 10T/Pro (apollo) | Poco F4 (munch)\"\n    },\n    {\n        \"maintainer\": \"kvsnr113\",\n        \"maintainer_link\": \"https://github.com/kvsnr113\",\n        \"kernel_name\": \"NOVA Kernel\",\n        \"kernel_link\": \"https://github.com/kvsnr113/xiaomi_sm8250_kernel\",\n        \"devices\": \"Poco F3 (alioth) | Mi 10T/Pro (apollo) | Poco F4 (munch)\"\n    },\n    {\n        \"maintainer\": \"anVzdGFtb25\",\n        \"maintainer_link\": \"https://github.com/anVzdGFtb25\",\n        \"kernel_name\": \"android_kernel_xiaomi_mt6768\",\n        \"kernel_link\": \"https://github.com/anVzdGFtb25/android_kernel_xiaomi_mt6768\",\n        \"devices\": \"Redmi 9 ( lancelot )\"\n    },\n    {\n        \"maintainer\": \"rifsxd\",\n        \"maintainer_link\": \"https://github.com/rifsxd\",\n        \"kernel_name\": \"android_kernel_realme_RMX3511\",\n        \"kernel_link\": \"https://github.com/rifsxd/android_kernel_realme_RMX3511\",\n        \"devices\": \"Realme C35 (RMX3511)\"\n    },\n    {\n        \"maintainer\": \"qlAD\",\n        \"maintainer_link\": \"https://github.com/qlAD\",\n        \"kernel_name\": \"kernel_oneplus_sdm845\",\n        \"kernel_link\": \"https://github.com/qlAD/kernel_oneplus_sdm845\",\n        \"devices\": \"OnePlus 6 (enchilada) with PixelExperience \"\n    },\n    {\n      \"maintainer\": \"rsuntkOrgs\",\n        \"maintainer_link\": \"https://github.com/rsuntkOrgs\",\n        \"kernel_name\": \"kernel_samsung_wingtech\",\n        \"kernel_link\": \"https://github.com/rsuntkOrgs/kernel_samsung_wingtech\",\n        \"devices\": \"Samsung Galaxy A05 (a05m) and A04 (a04)\"\n     },\n    {\n        \"maintainer\": \"itejo443\",\n        \"maintainer_link\": \"https://github.com/itejo443\",\n        \"kernel_name\": \"android_kernel_samsung_sm7225\",\n        \"kernel_link\": \"https://github.com/itejo443/android_kernel_samsung_sm7225\",\n        \"devices\": \"Samsung Galaxy F23 and M23 (m23xq)\"\n    },\n    {\n        \"maintainer\": \"JuiIm\",\n        \"maintainer_link\": \"https://github.com/JuiIm\",\n        \"kernel_name\": \"M30s-custom-kernel\",\n        \"kernel_link\": \"https://github.com/JuiIm/M30s-custom-kernel-M307f---KernelSU-support/\",\n        \"devices\": \"Samsung Galaxy M30s (M307F)\"\n    },\n    {\n        \"maintainer\": \"poqdavid\",\n        \"maintainer_link\": \"https://github.com/poqdavid\",\n        \"kernel_name\": \"android_kernel_samsung_sma155f\",\n        \"kernel_link\": \"https://github.com/poqdavid/android_kernel_samsung_sma155f\",\n        \"devices\": \"Samsung Galaxy A15 4G (sma155f) | MediaTek Helio G99\"\n    },\n    {\n        \"maintainer\": \"xixiaobei\",\n        \"maintainer_link\": \"https://github.com/xixiaobei-bei\",\n        \"kernel_name\": \"KernelSU_on_Huawei\",\n        \"kernel_link\": \"https://github.com/xixiaobei-bei/KernelSU_on_Huawei\",\n        \"devices\": \"All Huawei & Honor devices\"\n    }\n]\n"
  },
  {
    "path": "website/docs/ru_RU/guide/app-profile.md",
    "content": "# Профиль приложений\n\nПрофиль приложений - это механизм, предоставляемый KernelSU для настройки конфигурации различных приложений.\n\nДля приложений, получивших права root (т.е. имеющих возможность использовать `su`), App Profile может также называться Root Profile. Он позволяет настраивать правила `uid`, `gid`, `groups`, `capabilities` и `SELinux` команды `su`, тем самым ограничивая привилегии пользователя root. Например, она может предоставлять сетевые права только приложениям межсетевого экрана, отказывая в праве доступа к файлам, или предоставлять права shell вместо полного root-доступа для приложений freeze: **сохранение власти в рамках принципа наименьших привилегий*.\n\nДля обычных приложений, не имеющих прав root, App Profile может управлять поведением ядра и системы модулей по отношению к этим приложениям. Например, он может определять, следует ли обращать внимание на модификации, возникающие в результате работы модулей. На основе этой конфигурации ядро и система модулей могут принимать решения, например, выполнять операции, аналогичные \"скрытию\".\n\n## Корневой профиль\n\n### UID, GID и группы\n\nВ системах Linux существуют два понятия: пользователи и группы. Каждый пользователь имеет идентификатор пользователя (UID), а пользователь может принадлежать к нескольким группам, каждая из которых имеет свой идентификатор группы (GID). Эти идентификаторы используются для идентификации пользователей в системе и определяют, к каким системным ресурсам они могут получить доступ.\n\nПользователи с UID, равным 0, называются корневыми пользователями, а группы с GID, равным 0, - корневыми группами. Группа пользователей root, как правило, обладает самыми высокими системными привилегиями.\n\nВ случае системы Android каждое приложение является отдельным пользователем (исключая сценарии с общим UID) с уникальным UID. Например, `0` представляет пользователя root, `1000` - `system`, `2000` - ADB shell, а UID в диапазоне от 10000 до 19999 - обычные приложения.\n\n:::info\nЗдесь упомянутый UID не совпадает с концепцией нескольких пользователей или рабочих профилей в системе Android. На самом деле рабочие профили реализуются путем разделения диапазона UID. Например, 10000-19999 представляет собой основного пользователя, а 110000-119999 - рабочий профиль. Каждое обычное приложение среди них имеет свой уникальный UID.\n:::\n\nКаждое приложение может иметь несколько групп, причем GID представляет собой основную группу, которая обычно совпадает с UID. Другие группы называются дополнительными. Определенные разрешения контролируются через группы, например, разрешения на доступ к сети или доступ к Bluetooth.\n\nНапример, если мы выполним команду `id` в оболочке ADB, то результат может выглядеть следующим образом:\n\n```sh\noriole:/ $ id\nuid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),1078(ext_data_rw),1079(ext_obb_rw),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid),3012(readtracefs) context=u:r:shell:s0\n```\n\nЗдесь UID равен `2000`, а GID (идентификатор основной группы) также равен `2000`. Кроме того, он входит в несколько дополнительных групп, таких как `inet` (указывает на возможность создания сокетов `AF_INET` и `AF_INET6`) и `sdcard_rw` (указывает на права чтения/записи на SD-карту).\n\nКорневой профиль KernelSU позволяет настраивать UID, GID и группы для корневого процесса после выполнения команды `su`. Например, в корневом профиле корневого приложения можно установить его UID на `2000`, что означает, что при использовании `su` фактические разрешения приложения будут находиться на уровне оболочки ADB. Группа `inet` может быть удалена, что не позволит команде `su` получить доступ к сети.\n\n:::tip Примечание\nПрофиль приложений контролирует только разрешения корневого процесса после использования `su`; он не контролирует разрешения самого приложения. Если приложение запросило разрешение на доступ к сети, оно может получить доступ к сети даже без использования `su`. Удаление группы `inet` из `su` только предотвращает доступ `su` к сети.\n:::\n\nКорневой профиль реализуется в ядре и не зависит от добровольного поведения root-приложений, в отличие от переключения пользователей или групп через `su`, предоставление прав `su` полностью зависит от пользователя, а не от разработчика.\n\n### Привилегии\n\nПривилегии - это механизм разделения привилегий в Linux.\n\nВ традиционных реализациях UNIX для проверки прав доступа выделяются две категории процессов: привилегированные процессы (эффективный идентификатор пользователя равен 0 и называется суперпользователем или root) и непривилегированные процессы (эффективный UID которых не равен нулю).  Привилегированные процессы обходят все проверки прав ядра, в то время как непривилегированные процессы подвергаются полной проверке прав на основе учетных данных процесса (обычно: эффективный UID, эффективный GID и список дополнительных групп).\n\nНачиная с версии Linux 2.2, в Linux привилегии, традиционно ассоциируемые с суперпользователем, разделены на отдельные единицы, называемые возможностями, которые могут быть независимо включены и выключены.\n\nКаждая способность представляет собой одну или несколько привилегий. Например, `CAP_DAC_READ_SEARCH` представляет собой возможность обхода проверок прав на чтение файлов, а также прав на чтение и выполнение каталогов. Если пользователь с эффективным UID `0` (пользователь root) не имеет возможности `CAP_DAC_READ_SEARCH` или более высоких возможностей, это означает, что, хотя он и является пользователем root, он не может читать файлы по своему усмотрению.\n\nКорневой профиль KernelSU позволяет настраивать возможности корневого процесса после выполнения `su`, тем самым добиваясь частичного предоставления \"прав root\". В отличие от вышеупомянутых UID и GID, некоторые root-приложения после использования `su` требуют UID, равный `0`. В таких случаях ограничение возможностей данного root-пользователя с UID `0` может ограничить их разрешенные операции.\n\n:::tip Настоятельная рекомендация\nВ документе привелегий Linux [официальной документации](https://man7.org/linux/man-pages/man7/capabilities.7.html) дается подробное объяснение возможностей, представленных каждой привелегией. Если вы собираетесь настраивать привелегии, настоятельно рекомендуется сначала прочитать этот документ.\n:::\n\n### SELinux\n\nSELinux - это мощный механизм обязательного контроля доступа (MAC). Он работает по принципу **запрет по умолчанию**: любое действие, не разрешенное в явном виде, запрещается.\n\nSELinux может работать в двух глобальных режимах:\n\n1. Разрешительный режим: События запрета регистрируются, но не выполняются.\n2. Принудительный режим: События запрета регистрируются и выполняются.\n\n:::warning Предупреждение\nСовременные системы Android в значительной степени опираются на SELinux для обеспечения общей безопасности системы. Настоятельно не рекомендуется использовать пользовательские системы, работающие в \"разрешительном режиме\", поскольку это не дает существенных преимуществ перед полностью открытой системой.\n:::\n\nОбъяснение полной концепции SELinux является сложным и выходит за рамки данного документа. Рекомендуется сначала разобраться в его работе с помощью следующих ресурсов:\n\n1. [Wikipedia](https://en.wikipedia.org/wiki/Security-Enhanced_Linux)\n2. [Red Hat: Что такое SELinux?](https://www.redhat.com/en/topics/linux/what-is-selinux)\n3. [ArchLinux: SELinux](https://wiki.archlinux.org/title/SELinux)\n\nКорневой профиль KernelSU позволяет настраивать SELinux-контекст корневого процесса после выполнения команды `su`. Для этого контекста могут быть заданы специальные правила управления доступом, позволяющие осуществлять тонкий контроль над правами root.\n\nВ типичных сценариях, когда приложение выполняет команду `su`, оно переключает процесс на домен SELinux с **неограниченным доступом**, например `u:r:su:s0`. С помощью профиля Root Profile этот домен может быть переключен на пользовательский домен, например `u:r:app1:s0`, и для него может быть определен ряд правил:\n\n```sh\ntype app1\nenforce app1\ntypeattribute app1 mlstrustedsubject\nallow app1 * * *\n```\n\nОбратите внимание, что правило `allow app1 * * *` используется только в демонстрационных целях. На практике это правило не должно широко использоваться, поскольку оно мало чем отличается от разрешительного режима.\n\n### Эскалация\n\nПри неправильной настройке корневого профиля может возникнуть сценарий эскалации: ограничения, накладываемые корневым профилем, могут непреднамеренно не сработать.\n\nНапример, если предоставить права root пользователю ADB shell (что является обычным случаем), а затем предоставить права root обычному приложению, но настроить его профиль root с UID 2000 (это UID пользователя ADB shell), то приложение может получить полный доступ root, выполнив команду `su` дважды:\n\n1. При первом выполнении команды `su` будет применен профиль App Profile и произойдет переход на UID `2000` (adb shell) вместо `0` (root).\n2. При втором выполнении команды `su`, поскольку UID равен `2000`, а в конфигурации вы предоставили доступ root к UID `2000` (adb shell), приложение получит полные привилегии root.\n\n:::warning Примечание\nТакое поведение вполне ожидаемо и не является ошибкой. Поэтому мы рекомендуем следующее:\n\nЕсли вам действительно необходимо предоставить права root в ADB (например, как разработчику), не рекомендуется изменять UID на `2000` при настройке корневого профиля. Лучше использовать `1000` (система).\n:::\n\n## Некорневой профиль\n\n### Размонтирование модулей\n\nKernelSU предоставляет бессистемный механизм модификации системных разделов, реализуемый через монтирование overlayfs. Однако некоторые приложения могут быть чувствительны к такому поведению. Поэтому мы можем выгрузить модули, смонтированные в этих приложениях, установив опцию \"размонтирование модулей\".\n\nКроме того, в интерфейсе настроек менеджера KernelSU имеется переключатель \"размонтирование модулей по умолчанию\". По умолчанию этот переключатель **включен**, что означает, что KernelSU или некоторые модули будут выгружать модули для данного приложения, если не будут применены дополнительные настройки. Если вам не нравится эта настройка или если она влияет на определенные приложения, у вас есть следующие возможности:\n\n1. Оставить переключатель \"размонтирование модулей по умолчанию\" и индивидуально отключить опцию \"размонтирование модулей\" в профиле приложений для приложений, требующих загрузки модулей (действует как \"белый список\").\n2. Отключить переключатель \"размонтирование модулей по умолчанию\" и индивидуально включить опцию \"размонтирование модулей\" в App Profile для приложений, требующих выгрузки модулей (действует как \"черный список\").\n\n:::info\nВ устройствах, использующих ядро версии 5.10 и выше, выгрузку модулей выполняет само ядро. Однако для устройств с ядром версии ниже 5.10 этот переключатель является лишь опцией конфигурации, и KernelSU сам по себе не предпринимает никаких действий. Некоторые модули, например, Zygisksu, могут использовать этот переключатель для определения необходимости выгрузки модулей.\n:::\n"
  },
  {
    "path": "website/docs/ru_RU/guide/difference-with-magisk.md",
    "content": "# Различия с Magisk {#title}\n\nНесмотря на большое количество сходств между модулями KernelSU и модулями Magisk, неизбежно возникают и различия, обусловленные совершенно разными механизмами их реализации. Если вы хотите, чтобы ваш модуль работал как на Magisk, так и на KernelSU, вы должны понимать эти различия.\n\n## Сходства {#similarities}\n\n- Формат файлов модулей: оба используют формат zip для организации модулей, и формат модулей практически одинаков\n- Каталог установки модулей: оба расположены в `/data/adb/modules`.\n- Бессистемность: оба поддерживают модификацию /system бессистемным способом через модули\n- post-fs-data.sh: время выполнения и семантика полностью совпадают\n- service.sh: время выполнения и семантика полностью совпадают\n- system.prop: полностью совпадает\n- sepolicy.rule: полностью совпадает\n- BusyBox: скрипты запускаются в BusyBox с включенным \"автономным режимом\" в обоих случаях\n\n## Различия {#differences}\n\nПрежде чем разбираться в различиях, необходимо знать, как отличить, в каком режиме работает ваш модуль - KernelSU или Magisk. Для этого можно использовать переменную окружения `KSU` во всех местах, где можно запустить скрипты модуля (`customize.sh`, `post-fs-data.sh`, `service.sh`). В KernelSU эта переменная окружения будет установлена в значение `true`.\n\nВот некоторые отличия:\n\n- Модули KernelSU не могут быть установлены в режиме Recovery.\n- Модули KernelSU не имеют встроенной поддержки Zygisk (но вы можете использовать модули Zygisk через [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext).\n- Метод замены или удаления файлов в модулях KernelSU полностью отличается от Magisk. KernelSU не поддерживает метод `.replace`. Вместо этого необходимо создать одноименный файл с помощью команды `mknod filename c 0 0` для удаления соответствующего файла.\n- Каталоги для BusyBox отличаются. Встроенный BusyBox в KernelSU находится в каталоге `/data/adb/ksu/bin/busybox`, а в Magisk - в каталоге `/data/adb/magisk/busybox`. **Обратите внимание, что это внутреннее поведение KernelSU и в будущем оно может измениться!**\n- KernelSU не поддерживает файлы `.replace`; однако KernelSU поддерживает переменные `REMOVE` и `REPLACE` для удаления или замены файлов и папок.\n"
  },
  {
    "path": "website/docs/ru_RU/guide/faq.md",
    "content": "# FAQ\n\n## Поддерживает ли KernelSU мое устройство?\n\nKernelSU поддерживает устройства под управлением Android с разблокированным загрузчиком. Однако официальная поддержка предоставляется только для GKI Linux Kernel 5.10+ (на практике это означает, что ваше устройство должно иметь Android 12 из коробки для поддержки).\n\nВы можете легко проверить поддержку вашего устройства через менеджер KernelSU, который доступен [здесь](https://github.com/tiann/KernelSU/releases).\n\nЕсли приложение показывает `Not installed`, значит ваше устройство официально поддерживается KernelSU.\n\nЕсли приложение показывает `Unsupported`, значит ваше устройство в настоящее время не поддерживается официально. Однако вы можете собрать исходный код ядра и интегрировать KernelSU, чтобы заставить его работать, или использовать [Неофициально поддерживаемые устройства](unofficially-support-devices).\n\n## Нужно ли для KernelSU разблокировать загрузчик?\n\nБезусловно, да.\n\n## Поддерживает ли KernelSU модули?\n\nДа, большинство модулей Magisk работают с KernelSU. Однако, если вашему модулю нужно модифицировать файлы `/system`, вам необходимо установить [метамодуль](metamodule.md) (например, `meta-overlayfs`). Другие функции модулей работают без метамодуля. Проверьте [Руководство по модулям](module.md) для получения дополнительной информации.\n\n## Поддерживает ли KernelSU Xposed?\n\nДа, вы можете использовать LSPosed (или другие современные производные Xposed) с [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext).\n\n## Поддерживает ли KernelSU Zygisk?\n\nKernelSU не имеет встроенной поддержки Zygisk, но вы можете использовать модуль типа [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) для его поддержки.\n\n## Совместим ли KernelSU с Magisk?\n\nСистема модулей KernelSU конфликтует с магическим монтированием Magisk. Если в KernelSU включен какой-либо модуль, Magisk полностью перестанет работать.\n\nОднако, если вы используете только `su` из KernelSU, он будет хорошо работать с Magisk. KernelSU модифицирует `kernel`, а Magisk модифицирует `ramdisk`, позволяя обоим работать вместе.\n\n## Заменит ли KernelSU Magisk?\n\nМы считаем, что нет, и это не наша цель. Magisk достаточно хорош для решения root в пользовательском пространстве и будет жить долго. Цель KernelSU - предоставить пользователям интерфейс ядра, а не заменить Magisk.\n\n## Может ли KernelSU поддерживать устройства, не относящиеся к GKI?\n\nЭто возможно. Но вам нужно скачать исходный код ядра и интегрировать KernelSU в дерево исходных текстов и скомпилировать ядро самостоятельно.\n\n## Может ли KernelSU поддерживать устройства ниже Android 12?\n\nЭто ядро устройства влияет на совместимость KernelSU, и это не имеет ничего общего с версией Android. Единственное ограничение заключается в том, что устройства, выпущенные с Android 12, должны иметь версию ядра 5.10+ (устройства GKI). Итак:\n\n1. Устройства, выпущенные с Android 12, должны поддерживаться.\n2. Устройства со старым ядром (некоторые устройства с Android 12 также имеют старое ядро) совместимы (вам нужно самостоятельно собрать ядро).\n\n## Может ли KernelSU поддерживать старое ядро?\n\nЭто возможно. KernelSU теперь бэкпортирован на ядро 4.14. Для более старых ядер вам нужно выполнить бэкпорт вручную, и PR приветствуются!\n\n## Как интегрировать KernelSU для старого ядра?\n\nПожалуйста, ознакомьтесь с руководством [Интеграция для устройств не-GKI](how-to-integrate-for-non-gki).\n\n## Почему моя версия Android - 13, а ядро показывает \"android12-5.10\"?\n\nВерсия ядра не имеет ничего общего с версией Android. Если вам нужно прошить ядро, всегда используйте версию ядра; версия Android не так важна.\n\n## Я GKI 1.0, могу ли я использовать это?\n\nGKI 1.0 полностью отличается от GKI 2.0, вы должны скомпилировать ядро самостоятельно.\n\n## Как можно сделать `/system` RW?\n\nМы не рекомендуем напрямую модифицировать системный раздел. Пожалуйста, ознакомьтесь с [Руководством по модулям](module.md) для его бессистемной модификации. Если вы настаиваете на этом, проверьте [magisk_overlayfs](https://github.com/HuskyDG/magic_overlayfs).\n\n## Может ли KernelSU модифицировать hosts? Как использовать AdAway?\n\nКонечно. Но KernelSU не имеет встроенной поддержки hosts, вы можете установить модуль типа [systemless-hosts](https://github.com/symbuzzer/systemless-hosts-KernelSU-module) для этого.\n\n## Почему мои модули не работают после новой установки?\n\nЕсли вашим модулям нужно модифицировать файлы `/system`, вам необходимо установить [метамодуль](metamodule.md) для монтирования директории `system`. Другие функции модулей (скрипты, sepolicy, system.prop) работают без метамодуля.\n\n**Решение**: См. [Руководство по метамодулям](metamodule.md) для инструкций по установке.\n\n## Что такое метамодуль и зачем он мне нужен?\n\nМетамодуль - это специальный модуль, который предоставляет инфраструктуру для монтирования обычных модулей. См. [Руководство по метамодулям](metamodule.md) для полного объяснения.\n"
  },
  {
    "path": "website/docs/ru_RU/guide/hidden-features.md",
    "content": "# Скрытые возможности\n\n## .ksurc\n\nПо умолчанию `/system/bin/sh` загружает `/system/etc/mkshrc`.\n\nВы можете заставить su загружать пользовательский rc-файл, создав файл `/data/adb/ksu/.ksurc`."
  },
  {
    "path": "website/docs/ru_RU/guide/how-to-build.md",
    "content": "# Как собрать KernelSU?\n\n::: warning\nЭтот документ предназначен только для архивных ссылок и больше не обновляется.\nНачиная с KernelSU v3.0, мы отказались от официальной поддержки режима образов GKI для более быстрой итерации и скорости сборки. Рекомендуется использовать `Ylarod/ddk` для сборки LKM.\n:::\n\nПрежде всего, необходимо ознакомиться с официальной документацией Android по сборке ядра:\n\n1. [Сборка ядер](https://source.android.com/docs/setup/build/building-kernels)\n2. [Сборки релизов GKI](https://source.android.com/docs/core/architecture/kernel/gki-release-builds)\n\n::: warning\nЭта страница предназначена для устройств GKI, если вы используете старое ядро, пожалуйста, обратитесь к [Как интегрировать KernelSU для не GKI ядер?](how-to-integrate-for-non-gki).\n:::\n\n## Сборка ядра\n\n### Синхронизация исходного кода ядра\n\n```sh\nrepo init -u https://android.googlesource.com/kernel/manifest\nmv <kernel_manifest.xml> .repo/manifests\nrepo init -m manifest.xml\nrepo sync\n```\n\nФайл `<kernel_manifest.xml>` - это файл манифеста, который может однозначно определять сборку, с его помощью можно выполнить пересборку. Файл манифеста следует загрузить с сайта [Сборки релизов Google GKI](https://source.android.com/docs/core/architecture/kernel/gki-release-builds)\n\n### Построение\n\nПожалуйста, сначала ознакомьтесь с [официальной документацией](https://source.android.com/docs/setup/build/building-kernels).\n\nНапример, нам необходимо собрать образ ядра aarch64:\n\n```sh\nLTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh\n```\n\nНе забудьте добавить флаг `LTO=thin`, иначе сборка может завершиться неудачей, если память вашего компьютера меньше 24 Гб.\n\nНачиная с Android 13, сборка ядра осуществляется с помощью `bazel`:\n\n```sh\ntools/bazel build --config=fast //common:kernel_aarch64_dist\n```\n\n## Сборка ядра с помощью KernelSU\n\nЕсли вы успешно собрали ядро, то собрать KernelSU очень просто, выберите любой запуск в корневом каталоге исходного кода ядра:\n\n- Последний тэг(стабильный)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\n- Основная ветвь(разработка)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\n```\n\n- Выбранный тэг(Например, версия v0.5.2)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\n```\n\nА затем пересоберите ядро и получите образ ядра с KernelSU!\n"
  },
  {
    "path": "website/docs/ru_RU/guide/how-to-integrate-for-non-gki.md",
    "content": "# Как интегрировать KernelSU для не GKI ядер? {#introduction}\n\n::: warning\nЭтот документ предназначен только для архивных ссылок и больше не обновляется.\nНачиная с KernelSU v1.0, мы отказались от официальной поддержки устройств не-GKI.\n:::\n\nKernelSU может быть интегрирован в ядра, отличные от GKI, и был перенесен на версии 4.14 и ниже.\n\nВ связи с фрагментацией ядер, отличных от GKI, у нас нет единого способа их сборки, поэтому мы не можем предоставить загрузочные образы, отличные от GKI. Однако вы можете собрать ядро самостоятельно с помощью интегрированной программы KernelSU.\n\nВо-первых, вы должны уметь собирать загрузочное ядро из исходных текстов ядра. Если ядро не является открытым, то запустить KernelSU для вашего устройства будет затруднительно.\n\nЕсли вы можете собрать загрузочное ядро, то существует два способа интеграции KernelSU в исходный код ядра:\n\n1. Автоматически с помощью `kprobe`\n2. Вручную\n\n## Интеграция с kprobe {#using-kprobes}\n\nKernelSU использует kprobe для выполнения хуков ядра, если *kprobe* хорошо работает в вашем ядре, то рекомендуется использовать именно этот способ.\n\nСначала добавьте KernelSU в дерево исходных текстов ядра:\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\nЗатем необходимо проверить, включена ли функция *kprobe* в конфигурации ядра, если нет, то добавьте в нее эти настройки:\n\n```\nCONFIG_KPROBES=y\nCONFIG_HAVE_KPROBES=y\nCONFIG_KPROBE_EVENTS=y\n```\n\nИ снова соберите ядро, KernelSU должен работать нормально.\n\nЕсли вы обнаружите, что KPROBES по-прежнему не активирован, попробуйте включить `CONFIG_MODULES`. (Если это все равно не даст результата, используйте `make menuconfig` для поиска других зависимостей KPROBES).\n\nЕсли же при интеграции KernelSU возникает зацикливание загрузки, то, возможно, в вашем ядре *kprobe неисправен*, следует исправить ошибку kprobe или воспользоваться вторым способом.\n\n:::tip Как проверить, не сломан ли kprobe？\n\nзакомментируйте `ksu_sucompat_init()` и `ksu_ksud_init()` в файле `KernelSU/kernel/ksu.c`, если устройство загружается нормально, то может быть нарушена работа kprobe.\n:::\n\n## Ручная модификация исходного кода ядра {#modify-kernel-source-code}\n\nЕсли kprobe не работает в вашем ядре (возможно, это ошибка апстрима или ядра ниже 4.8), то можно попробовать следующий способ:\n\nСначала добавьте Kernel SU в дерево исходного кода вашего ядра:\n\n- Последний тэг(стабильный)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\n- Основная ветвь(разработка)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\n```\n\n- Выбранный тэг(Например, версия v0.5.2)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\n```\n\nЗатем добавьте вызовы KernelSU в исходный код ядра, вот патч, на который можно сослаться:\n\n```diff\ndiff --git a/fs/exec.c b/fs/exec.c\nindex ac59664eaecf..bdd585e1d2cc 100644\n--- a/fs/exec.c\n+++ b/fs/exec.c\n@@ -1890,11 +1890,14 @@ static int __do_execve_file(int fd, struct filename *filename,\n \treturn retval;\n }\n \n+extern bool ksu_execveat_hook __read_mostly;\n+extern int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv,\n+\t\t\tvoid *envp, int *flags);\n+extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,\n+\t\t\t\t void *argv, void *envp, int *flags);\n static int do_execveat_common(int fd, struct filename *filename,\n \t\t\t      struct user_arg_ptr argv,\n \t\t\t      struct user_arg_ptr envp,\n \t\t\t      int flags)\n {\n+\tif (unlikely(ksu_execveat_hook))\n+\t\tksu_handle_execveat(&fd, &filename, &argv, &envp, &flags);\n+\telse\n+\t\tksu_handle_execveat_sucompat(&fd, &filename, &argv, &envp, &flags);\n \treturn __do_execve_file(fd, filename, argv, envp, flags, NULL);\n }\n```\n```diff\ndiff --git a/fs/open.c b/fs/open.c\nindex 05036d819197..965b84d486b8 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -348,6 +348,8 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn ksys_fallocate(fd, mode, offset, len);\n }\n \n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t int *flags);\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -355,6 +357,7 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n  */\n long do_faccessat(int dfd, const char __user *filename, int mode)\n {\n \tconst struct cred *old_cred;\n \tstruct cred *override_cred;\n \tstruct path path;\n \tstruct inode *inode;\n \tstruct vfsmount *mnt;\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n \n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n \n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n```diff\ndiff --git a/fs/read_write.c b/fs/read_write.c\nindex 650fc7e0f3a6..55be193913b6 100644\n--- a/fs/read_write.c\n+++ b/fs/read_write.c\n@@ -434,10 +434,14 @@ ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)\n }\n EXPORT_SYMBOL(kernel_read);\n \n+extern bool ksu_vfs_read_hook __read_mostly;\n+extern int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,\n+\t\t\tsize_t *count_ptr, loff_t **pos);\n ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)\n {\n \tssize_t ret;\n \n+\tif (unlikely(ksu_vfs_read_hook))\n+\t\tksu_handle_vfs_read(&file, &buf, &count, &pos);\n+\n \tif (!(file->f_mode & FMODE_READ))\n \t\treturn -EBADF;\n \tif (!(file->f_mode & FMODE_CAN_READ))\n```\n```diff\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 376543199b5a..82adcef03ecc 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -148,6 +148,8 @@ int vfs_statx_fd(unsigned int fd, struct kstat *stat,\n }\n EXPORT_SYMBOL(vfs_statx_fd);\n \n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+\n /**\n  * vfs_statx - Get basic and extra attributes by filename\n  * @dfd: A file descriptor representing the base dir for a relative filename\n@@ -170,6 +172,7 @@ int vfs_statx(int dfd, const char __user *filename, int flags,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_AUTOMOUNT;\n \n+\tksu_handle_stat(&dfd, &filename, &flags);\n \tif ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t       AT_EMPTY_PATH | KSTAT_QUERY_FLAGS)) != 0)\n \t\treturn -EINVAL;\n```\n\nВ исходных кодах ядра можно найти эти четыре функции:\n\n1. do_faccessat, обычно в `fs/open.c`.\n2. do_execveat_common, обычно в `fs/exec.c`.\n3. vfs_read, обычно в `fs/read_write.c`.\n4. vfs_statx, обычно в `fs/stat.c`.\n\nЕсли в вашем ядре нет `vfs_statx`, используйте вместо него `vfs_fstatat`:\n\n```diff\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 068fdbcc9e26..5348b7bb9db2 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -87,6 +87,8 @@ int vfs_fstat(unsigned int fd, struct kstat *stat)\n }\n EXPORT_SYMBOL(vfs_fstat);\n \n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+\n int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \t\tint flag)\n {\n@@ -94,6 +96,8 @@ int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = 0;\n \n+\tksu_handle_stat(&dfd, &filename, &flag);\n+\n \tif ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t      AT_EMPTY_PATH)) != 0)\n \t\tgoto out;\n```\n\nДля ядер младше 4.17, если вы не можете найти `do_faccessat`, просто перейдите к определению системного вызова `faccessat` и поместите вызов туда:\n\n```diff\ndiff --git a/fs/open.c b/fs/open.c\nindex 2ff887661237..e758d7db7663 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -355,6 +355,9 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn error;\n }\n \n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t        int *flags);\n+\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -370,6 +373,8 @@ SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n \n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n+\n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n\nЧтобы включить встроенный в KernelSU безопасный режим, необходимо также изменить `input_handle_event` в файле `drivers/input/input.c`:\n\n:::tip\nНастоятельно рекомендуется включить эту функцию, она очень помогает предотвратить циклическую загрузку!\n:::\n\n```diff\ndiff --git a/drivers/input/input.c b/drivers/input/input.c\nindex 45306f9ef247..815091ebfca4 100755\n--- a/drivers/input/input.c\n+++ b/drivers/input/input.c\n@@ -367,10 +367,13 @@ static int input_get_disposition(struct input_dev *dev,\n \treturn disposition;\n }\n \n+extern bool ksu_input_hook __read_mostly;\n+extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value);\n+\n static void input_handle_event(struct input_dev *dev,\n \t\t\t       unsigned int type, unsigned int code, int value)\n {\n\tint disposition = input_get_disposition(dev, type, code, &value);\n+\n+\tif (unlikely(ksu_input_hook))\n+\t\tksu_handle_input_handle_event(&type, &code, &value);\n \n \tif (disposition != INPUT_IGNORE_EVENT && type != EV_SYN)\n \t\tadd_input_randomness(type, code, value);\n```\n\nНаконец, снова соберите ядро, KernelSU должен работать хорошо.\n\n### How to backport path_umount\n\nYou can make the \"Umount modules\" feature work on pre-GKI kernels by manually backporting `path_umount` from 5.9. You can use this patch as reference:\n\n```diff\n--- a/fs/namespace.c\n+++ b/fs/namespace.c\n@@ -1739,6 +1739,39 @@ static inline bool may_mandlock(void)\n }\n #endif\n\n+static int can_umount(const struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\n+\tif (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW))\n+\t\treturn -EINVAL;\n+\tif (!may_mount())\n+\t\treturn -EPERM;\n+\tif (path->dentry != path->mnt->mnt_root)\n+\t\treturn -EINVAL;\n+\tif (!check_mnt(mnt))\n+\t\treturn -EINVAL;\n+\tif (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */\n+\t\treturn -EINVAL;\n+\tif (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN))\n+\t\treturn -EPERM;\n+\treturn 0;\n+}\n+\n+int path_umount(struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\tint ret;\n+\n+\tret = can_umount(path, flags);\n+\tif (!ret)\n+\t\tret = do_umount(mnt, flags);\n+\n+\t/* we mustn't call path_put() as that would clear mnt_expiry_mark */\n+\tdput(path->dentry);\n+\tmntput_no_expire(mnt);\n+\treturn ret;\n+}\n /*\n  * Now umount can handle mount points as well as block devices.\n  * This is important for filesystems which use unnamed block devices.\n```\n\nFinally, build your kernel again, and KernelSU should work correctly.\n"
  },
  {
    "path": "website/docs/ru_RU/guide/installation.md",
    "content": "# Установка {#title}\n\n## Проверьте, поддерживается ли ваше устройство {#check-if-supported}\n\nСкачайте приложение менеджера KernelSU с сайта [GitHub Releases](https://github.com/tiann/KernelSU/releases) и установите его на устройство:\n\n- Если приложение показывает `Unsupported`, это означает, что **Вы должны скомпилировать ядро самостоятельно**, KernelSU не будет и никогда не предоставит Вам загрузочный образ для прошивки.\n- Если приложение показывает `Не установлено`, значит, ваши устройства официально поддерживаются KernelSU.\n\n:::info\nДля устройств, показывающих `Unsupported`, здесь находится [Unofficially-support-devices](unofficially-support-devices.md), вы можете скомпилировать ядро самостоятельно.\n:::\n\n## Резервное копирование стокового файла boot.img {#backup-boot-image}\n\nПеред прошивкой необходимо создать резервную копию файла boot.img. Если возникнет ошибка загрузки, вы всегда сможете восстановить систему, перепрошив ее на заводскую загрузку с помощью fastboot.\n\n::: warning\nПрошивка может привести к потере данных, поэтому обязательно выполните этот шаг перед переходом к следующему шагу!!! При необходимости можно также создать резервную копию всех данных на телефоне.\n:::\n\n## Необходимые знания {#acknowage}\n\n### ADB и fastboot {#adb-and-fastboot}\n\nПо умолчанию в этом руководстве вы будете использовать инструменты ADB и fastboot, поэтому, если вы их не знаете, рекомендуем сначала воспользоваться поисковой системой, чтобы узнать о них.\n\n### KMI\n\nKernel Module Interface (KMI), версии ядра с одинаковым KMI **совместимы** Это то, что в GKI означает \"общий\"; наоборот, если KMI отличается, то эти ядра несовместимы друг с другом, и прошивка образа ядра с другим KMI, чем у вашего устройства, может привести к bootloop.\n\nВ частности, для устройств GKI формат версии ядра должен быть следующим:\n\n```txt\nKernelRelease :=\nVersion.PatchLevel.SubLevel-AndroidRelease-KmiGeneration-suffix\nw      .x         .y       -zzz           -k            -something\n```\n\n`w.x-zz-k` - версия KMI. Например, если версия ядра устройства `5.10.101-android12-9-g30979850fc20`, то его KMI - `5.10-android12-9`; теоретически оно может нормально загружаться с другими ядрами KMI.\n\n::: tip\nОбратите внимание, что SubLevel в версии ядра не является частью KMI! Это означает, что `5.10.101-android12-9-g30979850fc20` имеет тот же KMI, что и `5.10.137-android12-9-g30979850fc20`!\n:::\n\n### Версия ядра и версия Android {#kernel-version-vs-android-version}\n\nОбратите внимание: **Версия ядра и версия Android - это не обязательно одно и то же!**\n\nЕсли вы обнаружили, что версия ядра `android12-5.10.101`, а версия системы Android - Android 13 или другая, не удивляйтесь, поскольку номер версии системы Android не обязательно совпадает с номером версии ядра Linux; Номер версии ядра Linux обычно соответствует версии системы Android, поставляемой с **устройством при его поставке**. При последующем обновлении системы Android версия ядра, как правило, не меняется. При необходимости прошивки **укажите версию ядра!!!**.\n\n## Введение {#installation-introduction}\n\nСуществует несколько способов установки KernelSU, каждый из которых подходит для разных сценариев, поэтому выбирайте их по своему усмотрению.\n\n1. Установка с помощью пользовательского Recovery (например, TWRP)\n2. Установка с помощью приложения для прошивки ядра, например, Franco Kernel Manager\n3. Установка с помощью fastboot с использованием boot.img, предоставленного KernelSU\n4. Восстановить boot.img вручную и установить его\n\nSince version [0.9.0](https://github.com/tiann/KernelSU/releases/tag/v0.9.0), KernelSU supports two running modes on GKI devices:\n\n1. `GKI`: Replace the original kernel of the device with the **Generic Kernel Image** (GKI) provided by KernelSU.\n2. `LKM`: Load the **Loadable Kernel Module** (LKM) into the device kernel without replacing the original kernel.\n\nThese two modes are suitable for different scenarios, and you can choose the one according to your needs.\n\n### GKI mode {#gki-mode}\n\nIn GKI mode, the original kernel of the device will be replaced with the generic kernel image provided by KernelSU. The advantages of GKI mode are:\n\n1. Strong universality, suitable for most devices. For example, Samsung has enabled KNOX devices, and LKM mode cannot work. There are also some niche modified devices that can only use GKI mode.\n2. Can be used without relying on official firmware, and there is no need to wait for official firmware updates, as long as the KMI is consistent, it can be used.\n\n### LKM mode {#lkm-mode}\n\nIn LKM mode, the original kernel of the device won't be replaced, but the loadable kernel module will be loaded into the device kernel. The advantages of LKM mode are:\n\n1. Won't replace the original kernel of the device. If you have special requirements for the original kernel of the device, or you want to use KernelSU while using a third-party kernel, you can use LKM mode.\n2. It's more convenient to upgrade and OTA. When upgrading KernelSU, you can directly install it in the manager without flashing manually. After the system OTA, you can directly install it to the second slot without manual flashing.\n3. Suitable for some special scenarios. For example, LKM can also be loaded with temporary root permissions. Since it doesn't need to replace the boot partition, it won't trigger AVB and won't cause the device to be bricked.\n4. LKM can be temporarily uninstalled. If you want to temporarily disable root access, you can uninstall LKM. This process doesn't require flashing partitions, or even rebooting the device. If you want to enable root again, just reboot the device.\n\n::: tip COEXISTENCE OF TWO MODES\nAfter opening the manager, you can see the current mode of the device on the homepage. Note that the priority of GKI mode is higher than that of LKM. For example, if you use GKI kernel to replace the original kernel, and use LKM to patch the GKI kernel, the LKM will be ignored, and the device will always run in GKI mode.\n:::\n\n### Which one to choose? {#which-one}\n\nIf your device is a mobile phone, we recommend that you prioritize LKM mode. If your device is an emulator, WSA, or Waydroid, we recommend that you prioritize GKI mode.\n\n## LKM installation\n\n### Get the official firmware\n\nTo use LKM mode, you need to get the official firmware and patch it based on the official firmware. If you use a third-party kernel, you can use the `boot.img` of the third-party kernel as the official firmware.\n\nThere are many ways to get the official firmware. If your device supports `fastboot boot`, we recommend **the most recommended and simplest** method is to use `fastboot boot` to temporarily boot the GKI kernel provided by KernelSU, then install the manager, and finally install it directly in the manager. This method doesn't require manually downloading the official firmware or manually extracting the boot.\n\nIf your device doesn't support `fastboot boot`, you may need to manually download the official firmware package and extract the boot from it.\n\nUnlike GKI mode, LKM mode modifies the `ramdisk`. Therefore, on devices with Android 13, it needs to patch the `init_boot` partition instead of the `boot` partition, while GKI mode always operates on the `boot` partition.\n\n### Use the manager\n\nOpen the manager, click the installation icon in the upper right corner, and several options will appear:\n\n1. Select a file. If your device doesn't have root privileges, you can choose this option and then select your official firmware. The manager will automatically patch it. After that, just flash this patched file to obtain root privileges permanently.\n2. Direct install. If your device is already rooted, you can choose this option. The manager will automatically get your device information, and then automatically patch the official firmware, and flash it automatically. You can consider using `fastboot boot` KernelSU's GKI kernel to get temporary root and install the manager, and then use this option. This is also the main way to upgrade KernelSU.\n3. Install to inactive slot. If your device supports A/B partition, you can choose this option. The manager will automatically patch the official firmware and install it to another partition. This method is suitable for devices after OTA, you can directly install it to the inactive slot after OTA.\n\nIf you don't want to use the manager, you can also use the command line to install LKM. The `ksud` tool provided by KernelSU can help you patch the official firmware quickly and then flash it.\n\nThe usage of `ksud` is as follows:\n\n```sh\nksud boot-patch \n```\n\n```txt\nUsage: ksud boot-patch [OPTIONS]\n\nOptions:\n  -b, --boot <BOOT>              Boot image be patched\n  -l, --lkm <LKM>                LKM module path. If not specified, the built-in module will be used\n  -m, --module <MODULE>          LKM module path to be replaced. If not specified, the built-in module will be used\n  -i, --init <INIT>              init to be replaced\n  -u, --ota                      Will use another slot if the boot image is not specified\n  -f, --flash                    Flash it to boot partition after patch\n  -o, --out <OUT>                Output path. If not specified, the current directory will be used\n      --magiskboot <MAGISKBOOT>  magiskboot path. If not specified, the built-in version will be used\n      --kmi <KMI>                KMI version. If specified, the indicated KMI will be used\n  -h, --help                     Print help\n```\n\nA few options that need to be explained:\n\n1. The `--magiskboot` option can specify the path of magiskboot. If not specified, ksud will look for it in the environment variables. If you don’t know how to get magiskboot, you can check [here](#patch-boot-image).\n2. The `--kmi` option can specify the `KMI` version. If the kernel name of your device doesn't follow the KMI specification, you can specify it using this option.\n\nThe most common usage is:\n\n```sh\nksud boot-patch -b <boot.img> --kmi android13-5.10\n```\n\n## LKM mode installation\n\nThere are several installation methods for LKM mode, each suitable for a different scenario, so please choose accordingly:\n\n1. Install with fastboot using the boot.img provided by KernelSU.\n2. Install with a kernel flash app, such as [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases).\n3. Repair the boot.img manually and install it.\n4. Install with custom Recovery (e.g., TWRP).\n\n## Установка с помощью пользовательского Recovery {#install-by-recovery}\n\nНеобходимые условия: На устройстве должен быть установлен пользовательский Recovery, например TWRP; если его нет или доступен только официальный Recovery, воспользуйтесь другим способом.\n\nШаг:\n\n1. С [Release page](https://github.com/tiann/KernelSU/releases) KernelSU загрузите zip-пакет, начинающийся с AnyKernel3, который соответствует версии вашего телефона; например, версия ядра телефона - `android12-5.10. 66`, то следует скачать файл `AnyKernel3-android12-5.10.66_yyy-MM.zip` (где `yyyy` - год, а `MM` - месяц).\n2. Перезагрузите телефон в TWRP.\n3. С помощью adb поместите AnyKernel3-*.zip в /sdcard телефона и выберите установку в графическом интерфейсе TWRP; или вы можете напрямую `adb sideload AnyKernel-*.zip` для установки.\n\nPS. Данный способ подходит для любой установки (не ограничиваясь начальной установкой или последующими обновлениями), если вы используете TWRP.\n\n## Установка с помощью Kernel Flasher {#install-by-kernel-flasher}\n\nНеобходимые условия: Ваше устройство должно быть рутованным. Например, вы установили Magisk, чтобы получить root, или установили старую версию KernelSU и должны обновить ее до другой версии; если ваше устройство не укоренено, попробуйте другие методы.\n\nШаг:\n\n1. Загрузите zip-архив AnyKernel3; инструкции по загрузке см. в разделе *Установка с помощью пользовательского Recovery*.\n2. Откройте приложение для прошивки ядра и используйте предоставленный AnyKernel3 zip для прошивки.\n\nЕсли вы раньше не использовали приложение для прошивки ядра, то наиболее популярными являются следующие.\n\n1. [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases)\n2. [Franco Kernel Manager](https://play.google.com/store/apps/details?id=com.franco.kernel)\n3. [Ex Kernel Manager](https://play.google.com/store/apps/details?id=flar2.exkernelmanager)\n\nPS. Этот способ более удобен при обновлении KernelSU и может быть выполнен без компьютера (сначала сделайте резервную копию!). .\n\n## Установка с помощью boot.img, предоставленного KernelSU {#install-by-kernelsu-boot-image}\n\nЭтот способ не требует наличия TWRP и root-прав на телефоне; он подходит для первой установки KernelSU.\n\n### Найти подходящий boot.img {#found-propery-image}\n\nKernelSU предоставляет общий boot.img для устройств GKI, и его необходимо прошить в загрузочный раздел устройства.\n\nВы можете загрузить boot.img с [GitHub Release](https://github.com/tiann/KernelSU/releases), обратите внимание, что вы должны использовать правильную версию boot.img. Например, если на устройстве установлено ядро `android12-5.10.101`, то необходимо загрузить `android-5.10.101_yyy-MM.boot-<format>.img`. , необходимо загрузить `android-5.10.101_yyy-MM.boot-<format>.img`.(Соблюдайте соответствие KMI!).\n\nГде `<format>` означает формат сжатия ядра в официальном boot.img, проверьте формат сжатия ядра в оригинальном boot.img, вы должны использовать правильный формат, например, `lz4`, `gz`; если вы используете неправильный формат сжатия, вы можете столкнуться с bootloop.\n\n::: info\n1. Вы можете использовать magiskboot для получения формата сжатия исходной загрузки; конечно, вы также можете спросить других, более опытных ребят с той же моделью, что и ваше устройство. Кроме того, формат сжатия ядра обычно не меняется, поэтому, если вы успешно загрузились с определенным форматом сжатия, вы можете попробовать этот формат позже.\n2. Устройства Xiaomi обычно используют `gz` или **без сжатия**.\n3. Для устройств Pixel следуйте приведенным ниже инструкциям.\n:::\n\n### прошить boot.img на устройство {#flash-boot-image}\n\nИспользуйте `adb` для подключения устройства, затем выполните `adb reboot bootloader` для входа в режим fastboot, после чего используйте эту команду для прошивки KernelSU:\n\n```sh\nfastboot flash boot boot.img\n```\n\n::: info\nЕсли устройство поддерживает `fastboot boot`, можно сначала использовать `fastboot boot boot.img`, чтобы попытаться использовать boot.img для загрузки системы. Если произойдет что-то непредвиденное, перезагрузите его снова для загрузки.\n:::\n\n### перезагрузка {#reboot}\n\nПосле завершения прошивки необходимо перезагрузить устройство:\n\n```sh\nfastboot reboot\n```\n\n## Исправить boot.img вручную {#patch-boot-image}\n\nДля некоторых устройств формат boot.img не так распространен, например, не `lz4`, `gz` или несжатый; наиболее типичным является Pixel, его boot.img имеет формат `lz4_legacy` со сжатием, ramdisk может быть `gz`, также может быть `lz4_legacy` со сжатием; в это время, если напрямую прошить boot.img, предоставляемый KernelSU, телефон может не загрузиться; в это время можно вручную исправить boot.img для достижения цели.\n\nКак правило, существует два способа исправления:\n\n1. [Android-Image-Kitchen](https://forum.xda-developers.com/t/tool-android-image-kitchen-unpack-repack-kernel-ramdisk-win-android-linux-mac.2073775/)\n2. [magiskboot](https://github.com/topjohnwu/Magisk/releases)\n\nСреди них Android-Image-Kitchen подходит для работы на ПК, а magiskboot нуждается в сотрудничестве мобильного телефона.\n\n### Подготовка {#patch-preparation}\n\n1. Получите стоковый boot.img вашего телефона; его можно получить у производителя устройства, возможно, вам понадобится [payload-dumper-go](https://github.com/ssut/payload-dumper-go)\n2. Загрузите zip-файл AnyKernel3, предоставленный KernelSU, который соответствует версии KMI вашего устройства (можно обратиться к разделу *Установка с помощью пользовательского Recovery*).\n3. Распакуйте пакет AnyKernel3 и получите файл `Image`, который является файлом ядра KernelSU.\n\n### Использование Android-Image-Kitchen {#using-android-image-kitchen}\n\n1. Загрузите программу Android-Image-Kitchen на свой компьютер.\n2. Поместите файл boot.img в корневую папку Android-Image-Kitchen.\n3. Выполните команду `./unpackimg.sh boot.img` в корневом каталоге Android-Image-Kitchen, в результате чего boot.img распакуется и появятся некоторые файлы.\n4. Замените `boot.img-kernel` в каталоге `split_img` тем `образом`, который вы извлекли из AnyKernel3 (обратите внимание на изменение названия на boot.img-kernel).\n5. Выполните команду `./repackimg.sh` в корневом каталоге 在 Android-Image-Kitchen; Вы получите файл с именем `image-new.img`; Прошейте этот boot.img с помощью fastboot (см. предыдущий раздел).\n\n### Использование magiskboot {#using magiskboot}\n\n1. Загрузите последнюю версию Magisk с [Release Page](https://github.com/topjohnwu/Magisk/releases).\n2. Переименуйте `Magisk-*(version).apk` в `Magisk-*.zip` и разархивируйте его.\n3. Закачайте `Magisk-*/lib/arm64-v8a/libmagiskboot.so` на устройство с помощью adb: `adb push Magisk-*/lib/arm64-v8a/libmagiskboot.so /data/local/tmp/magiskboot`.\n4. Установите на устройство стоковый boot.img и образ в AnyKernel3.\n5. Войдите в оболочку adb и перейдите в каталог `/data/local/tmp/`, затем `chmod +x magiskboot`.\n6. Войдите в adb shell и cd директории `/data/local/tmp/`, выполните команду `./magiskboot unpack boot.img` для распаковки `boot.img`, вы получите файл `kernel`, это и есть ваше стоковое ядро.\n7. Замените `kernel` на `Image`: `mv -f Image kernel`.\n8. Выполните команду `./magiskboot repack boot.img`, чтобы перепаковать boot img, и получите файл `new-boot.img`, прошейте его на устройство с помощью fastboot.\n\n## Другие методы {#other-methods}\n\nНа самом деле все эти способы установки имеют только одну основную идею - **заменить исходное ядро на ядро, предоставляемое KernelSU**; если это возможно, то установка возможна; например, возможны следующие способы.\n\n1. Сначала установить Magisk, получить права root через Magisk, а затем с помощью kernel flasher прошить AnyKernel zip из KernelSU.\n2. Использовать какой-либо инструментарий для прошивки на ПК, чтобы прошить ядро, предоставленное KernelSU.\n\nОднако, если это не работает, попробуйте подход `magiskboot`.\n\n## После установки: Поддержка модулей\n\n::: warning МЕТАМОДУЛЬ ДЛЯ МОДИФИКАЦИИ СИСТЕМНЫХ ФАЙЛОВ\nЕсли вы хотите использовать модули, которые модифицируют файлы `/system`, вам необходимо установить **метамодуль** после установки KernelSU. Модули, которые используют только скрипты, sepolicy или system.prop, работают без метамодуля.\n:::\n\n**Для поддержки модификации `/system`**, пожалуйста, см. [Руководство по метамодулям](metamodule.md), чтобы:\n- Понять, что такое метамодули и зачем они нужны\n- Установить официальный метамодуль `meta-overlayfs`\n- Узнать о других вариантах метамодулей\n"
  },
  {
    "path": "website/docs/ru_RU/guide/metamodule.md",
    "content": "# Метамодуль\n\nМетамодули — это революционная функция в KernelSU, которая передает критически важные возможности модульной системы от основного демона к подключаемым модулям. Это архитектурное изменение сохраняет стабильность и безопасность KernelSU, одновременно раскрывая больший потенциал для инноваций в экосистеме модулей.\n\n## Что такое Метамодуль?\n\nМетамодуль — это специальный тип модуля KernelSU, который предоставляет основную инфраструктурную функциональность для модульной системы. В отличие от обычных модулей, которые модифицируют системные файлы, метамодули контролируют *способ* установки и монтирования обычных модулей.\n\nМетамодули — это механизм расширения на основе плагинов, который позволяет полностью настраивать инфраструктуру управления модулями KernelSU. Делегируя логику монтирования и установки метамодулям, KernelSU избегает превращения в уязвимую точку обнаружения, одновременно поддерживая различные стратегии реализации.\n\n**Ключевые характеристики:**\n\n- **Роль инфраструктуры**: Метамодули предоставляют сервисы, от которых зависят обычные модули\n- **Единственный экземпляр**: Одновременно может быть установлен только один метамодуль\n- **Приоритетное выполнение**: Скрипты метамодулей выполняются перед скриптами обычных модулей\n- **Специальные хуки**: Предоставляет три скрипта-хука для установки, монтирования и очистки\n\n## Зачем нужны Метамодули?\n\nТрадиционные решения для получения root встраивают логику монтирования в свое ядро, что делает их более легко обнаруживаемыми и сложными для развития. Архитектура метамодулей KernelSU решает эти проблемы через разделение ответственности.\n\n**Стратегические преимущества:**\n\n- **Уменьшение поверхности обнаружения**: Сам KernelSU не выполняет монтирование, уменьшая векторы обнаружения\n- **Стабильность**: Основной демон остается стабильным, в то время как реализации монтирования могут развиваться\n- **Инновации**: Сообщество может разрабатывать альтернативные стратегии монтирования без форка KernelSU\n- **Выбор**: Пользователи могут выбрать реализацию, которая лучше всего соответствует их потребностям\n\n**Гибкость монтирования:**\n\n- **Без монтирования**: Для пользователей с модулями только без монтирования полностью избегайте накладных расходов на монтирование\n- **Монтирование OverlayFS**: Традиционный подход с поддержкой слоя чтения-записи (через `meta-overlayfs`)\n- **Magic mount**: Монтирование, совместимое с Magisk, для лучшей совместимости с приложениями\n- **Пользовательские реализации**: Наложения на основе FUSE, пользовательские монтирования VFS или совершенно новые подходы\n\n**Помимо монтирования:**\n\n- **Расширяемость**: Добавляйте функции, такие как поддержка модулей ядра, без изменения основного KernelSU\n- **Модульность**: Обновляйте реализации независимо от выпусков KernelSU\n- **Настройка**: Создавайте специализированные решения для конкретных устройств или случаев использования\n\n::: warning ВАЖНО\nБез установленного метамодуля модули **НЕ** будут смонтированы. Свежие установки KernelSU требуют установки метамодуля (например, `meta-overlayfs`) для работы модулей.\n:::\n\n## Для Пользователей\n\n### Установка Метамодуля\n\nУстановите метамодуль так же, как обычные модули:\n\n1. Загрузите ZIP-файл метамодуля (например, `meta-overlayfs.zip`)\n2. Откройте приложение KernelSU Manager\n3. Нажмите на плавающую кнопку действия (➕)\n4. Выберите ZIP-файл метамодуля\n5. Перезагрузите устройство\n\nМетамодуль `meta-overlayfs` — это официальная эталонная реализация, которая обеспечивает традиционное монтирование модулей на основе overlayfs с поддержкой образов ext4.\n\n### Проверка Активного Метамодуля\n\nВы можете проверить, какой метамодуль в настоящее время активен, на странице модулей приложения KernelSU Manager. Активный метамодуль будет отображаться в вашем списке модулей со своим специальным обозначением.\n\n### Удаление Метамодуля\n\n::: danger ПРЕДУПРЕЖДЕНИЕ\nУдаление метамодуля повлияет на **ВСЕ** модули. После удаления модули больше не будут монтироваться, пока вы не установите другой метамодуль.\n:::\n\nДля удаления:\n\n1. Откройте KernelSU Manager\n2. Найдите метамодуль в списке модулей\n3. Нажмите удалить (вы увидите специальное предупреждение)\n4. Подтвердите действие\n5. Перезагрузите устройство\n\nПосле удаления вы должны установить другой метамодуль, если хотите, чтобы модули продолжали работать.\n\n### Ограничение Единственного Метамодуля\n\nОдновременно может быть установлен только один метамодуль. Если вы попытаетесь установить второй метамодуль, KernelSU предотвратит установку, чтобы избежать конфликтов.\n\nДля переключения метамодулей:\n\n1. Удалите все обычные модули\n2. Удалите текущий метамодуль\n3. Перезагрузите\n4. Установите новый метамодуль\n5. Переустановите обычные модули\n6. Перезагрузите снова\n\n## Для Разработчиков Модулей\n\nЕсли вы разрабатываете обычные модули KernelSU, вам не нужно слишком беспокоиться о метамодулях. Ваши модули будут работать, пока у пользователей установлен совместимый метамодуль (например, `meta-overlayfs`).\n\n**Что вам нужно знать:**\n\n- **Монтирование требует метамодуль**: Директория `system` в вашем модуле будет смонтирована только если у пользователя установлен метамодуль, предоставляющий функциональность монтирования\n- **Изменения кода не требуются**: Существующие модули продолжают работать без изменений\n\n::: tip\nЕсли вы знакомы с разработкой модулей Magisk, ваши модули будут работать точно так же в KernelSU при установленном метамодуле, так как он предоставляет монтирование, совместимое с Magisk.\n:::\n\n## Для Разработчиков Метамодулей\n\nСоздание метамодуля позволяет вам настроить, как KernelSU обрабатывает установку модулей, монтирование и удаление.\n\n### Базовые Требования\n\nМетамодуль идентифицируется специальным свойством в `module.prop`:\n\n```txt\nid=my_metamodule\nname=My Custom Metamodule\nversion=1.0\nversionCode=1\nauthor=Your Name\ndescription=Custom module mounting implementation\nmetamodule=1\n```\n\nСвойство `metamodule=1` (или `metamodule=true`) помечает это как метамодуль. Без этого свойства модуль будет рассматриваться как обычный модуль.\n\n### Структура Файлов\n\nСтруктура метамодуля:\n\n```txt\nmy_metamodule/\n├── module.prop              (должен включать metamodule=1)\n│\n│      *** Специфичные хуки метамодуля ***\n├── metamount.sh             (опционально: пользовательский обработчик монтирования)\n├── metainstall.sh           (опционально: хук установки для обычных модулей)\n├── metauninstall.sh         (опционально: хук очистки для обычных модулей)\n│\n│      *** Стандартные файлы модуля (все опциональные) ***\n├── customize.sh             (настройка установки)\n├── post-fs-data.sh          (скрипт этапа post-fs-data)\n├── service.sh               (скрипт late_start service)\n├── boot-completed.sh        (скрипт завершения загрузки)\n├── uninstall.sh             (скрипт удаления самого метамодуля)\n├── system/                  (системные модификации, если необходимо)\n└── [любые дополнительные файлы]\n```\n\nМетамодули могут использовать все стандартные функции модулей (скрипты жизненного цикла и т. д.) в дополнение к своим специальным хукам метамодуля.\n\n### Скрипты-Хуки\n\nМетамодули могут предоставлять до трех специальных скриптов-хуков:\n\n#### 1. metamount.sh - Обработчик Монтирования\n\n**Назначение**: Контролирует способ монтирования модулей во время загрузки.\n\n**Когда выполняется**: Во время этапа `post-fs-data`, перед выполнением любых скриптов модулей.\n\n**Переменные окружения:**\n\n- `MODDIR`: Путь к директории метамодуля (например, `/data/adb/modules/my_metamodule`)\n- Все стандартные переменные окружения KernelSU\n\n**Обязанности:**\n\n- Монтировать все включенные модули системно\n- Проверять флаги `skip_mount`\n- Обрабатывать специфичные требования монтирования модулей\n\n::: danger КРИТИЧЕСКОЕ ТРЕБОВАНИЕ\nПри выполнении операций монтирования вы **ДОЛЖНЫ** установить имя источника/устройства в `\"KSU\"`. Это идентифицирует монтирования как принадлежащие KernelSU.\n\n**Пример (правильный):**\n\n```sh\nmount -t overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work KSU /target\n```\n\n**Для современных API монтирования** установите строку источника:\n\n```rust\nfsconfig_set_string(fs, \"source\", \"KSU\")?;\n```\n\nЭто необходимо для правильной идентификации и управления монтированиями KernelSU.\n:::\n\n**Пример скрипта:**\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\n\n# Пример: Простая реализация bind mount\nfor module in /data/adb/modules/*; do\n    if [ -f \"$module/disable\" ] || [ -f \"$module/skip_mount\" ]; then\n        continue\n    fi\n\n    if [ -d \"$module/system\" ]; then\n        # Монтирование с source=KSU (ОБЯЗАТЕЛЬНО!)\n        mount -o bind,dev=KSU \"$module/system\" /system\n    fi\ndone\n```\n\n#### 2. metainstall.sh - Хук Установки\n\n**Назначение**: Настроить способ установки обычных модулей.\n\n**Когда выполняется**: Во время установки модуля, после извлечения файлов, но до завершения установки. Этот скрипт **подключается** (не выполняется) встроенным установщиком, аналогично работе `customize.sh`.\n\n**Переменные окружения и функции:**\n\nЭтот скрипт наследует все переменные и функции из встроенного `install.sh`:\n\n- **Переменные**: `MODPATH`, `TMPDIR`, `ZIPFILE`, `ARCH`, `API`, `IS64BIT`, `KSU`, `KSU_VER`, `KSU_VER_CODE`, `BOOTMODE` и т. д.\n- **Функции**:\n  - `ui_print <msg>` - Вывести сообщение в консоль\n  - `abort <msg>` - Вывести ошибку и завершить установку\n  - `set_perm <target> <owner> <group> <permission> [context]` - Установить права доступа к файлу\n  - `set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]` - Установить права рекурсивно\n  - `install_module` - Вызвать встроенный процесс установки модуля\n\n**Случаи использования:**\n\n- Обработка файлов модуля до или после встроенной установки (вызовите `install_module`, когда готово)\n- Перемещение файлов модуля\n- Проверка совместимости модуля\n- Настройка специальных структур каталогов\n- Инициализация специфичных для модуля ресурсов\n\n**Примечание**: Этот скрипт **НЕ** вызывается при установке самого метамодуля.\n\n#### 3. metauninstall.sh - Хук Очистки\n\n**Назначение**: Очистить ресурсы при удалении обычных модулей.\n\n**Когда выполняется**: Во время удаления модуля, перед удалением каталога модуля.\n\n**Переменные окружения:**\n\n- `MODULE_ID`: ID удаляемого модуля\n\n**Случаи использования:**\n\n- Обработка файлов\n- Очистка символических ссылок\n- Освобождение выделенных ресурсов\n- Обновление внутреннего отслеживания\n\n**Пример скрипта:**\n\n```sh\n#!/system/bin/sh\n# Вызывается при удалении обычных модулей\nMODULE_ID=\"$1\"\nIMG_MNT=\"/data/adb/metamodule/mnt\"\n\n# Удалить файлы модуля из образа\nif [ -d \"$IMG_MNT/$MODULE_ID\" ]; then\n    rm -rf \"$IMG_MNT/$MODULE_ID\"\nfi\n```\n\n### Порядок Выполнения\n\nПонимание порядка выполнения загрузки критически важно для разработки метамодулей:\n\n```txt\nэтап post-fs-data:\n  1. Выполняются общие скрипты post-fs-data.d\n  2. Очистка модулей, restorecon, загрузка sepolicy.rule\n  3. Выполняется post-fs-data.sh метамодуля (если существует)\n  4. Выполняются post-fs-data.sh обычных модулей\n  5. Загружается system.prop\n  6. Выполняется metamount.sh метамодуля\n     └─> Монтирует все модули системно\n  7. Выполняется этап post-mount.d\n     - Общие скрипты post-mount.d\n     - post-mount.sh метамодуля (если существует)\n     - post-mount.sh обычных модулей\n\nэтап service:\n  1. Выполняются общие скрипты service.d\n  2. Выполняется service.sh метамодуля (если существует)\n  3. Выполняются service.sh обычных модулей\n\nэтап boot-completed:\n  1. Выполняются общие скрипты boot-completed.d\n  2. Выполняется boot-completed.sh метамодуля (если существует)\n  3. Выполняются boot-completed.sh обычных модулей\n```\n\n**Ключевые моменты:**\n\n- `metamount.sh` выполняется **ПОСЛЕ** всех скриптов post-fs-data (как метамодуля, так и обычных модулей)\n- Скрипты жизненного цикла метамодуля (`post-fs-data.sh`, `service.sh`, `boot-completed.sh`) всегда выполняются перед скриптами обычных модулей\n- Общие скрипты в каталогах `.d` выполняются перед скриптами метамодуля\n- Этап `post-mount` выполняется после завершения монтирования\n\n### Механизм Символических Ссылок\n\nПри установке метамодуля KernelSU создает символическую ссылку:\n\n```sh\n/data/adb/metamodule -> /data/adb/modules/<metamodule_id>\n```\n\nЭто обеспечивает стабильный путь для доступа к активному метамодулю независимо от его ID.\n\n**Преимущества:**\n\n- Постоянный путь доступа\n- Простое обнаружение активного метамодуля\n- Упрощение конфигурации\n\n### Реальный Пример: meta-overlayfs\n\nМетамодуль `meta-overlayfs` является официальной эталонной реализацией. Он демонстрирует лучшие практики разработки метамодулей.\n\n#### Архитектура\n\n`meta-overlayfs` использует **архитектуру с двумя каталогами**:\n\n1. **Каталог метаданных**: `/data/adb/modules/`\n   - Содержит `module.prop`, `disable`, маркеры `skip_mount`\n   - Быстро сканируется во время загрузки\n   - Малый объем хранения\n\n2. **Каталог содержимого**: `/data/adb/metamodule/mnt/`\n   - Содержит фактические файлы модулей (system, vendor, product и т. д.)\n   - Хранится в образе ext4 (`modules.img`)\n   - Оптимизировано по пространству с функциями ext4\n\n#### Реализация metamount.sh\n\nВот как `meta-overlayfs` реализует обработчик монтирования:\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\nIMG_FILE=\"$MODDIR/modules.img\"\nMNT_DIR=\"$MODDIR/mnt\"\n\n# Монтировать образ ext4, если он еще не смонтирован\nif ! mountpoint -q \"$MNT_DIR\"; then\n    mkdir -p \"$MNT_DIR\"\n    mount -t ext4 -o loop,rw,noatime \"$IMG_FILE\" \"$MNT_DIR\"\nfi\n\n# Установить переменные окружения для поддержки двух каталогов\nexport MODULE_METADATA_DIR=\"/data/adb/modules\"\nexport MODULE_CONTENT_DIR=\"$MNT_DIR\"\n\n# Выполнить бинарный файл монтирования\n# (Фактическая логика монтирования находится в бинарном файле Rust)\n\"$MODDIR/meta-overlayfs\"\n```\n\n#### Ключевые Особенности\n\n**Монтирование Overlayfs:**\n\n- Использует kernel overlayfs для настоящих системных модификаций\n- Поддерживает несколько разделов (system, vendor, product, system_ext, odm, oem)\n- Поддержка слоя чтения-записи через `/data/adb/modules/.rw/`\n\n**Идентификация источника:**\n\n```rust\n// Из meta-overlayfs/src/mount.rs\nfsconfig_set_string(fs, \"source\", \"KSU\")?;  // ОБЯЗАТЕЛЬНО!\n```\n\nЭто устанавливает `dev=KSU` для всех overlay монтирований, обеспечивая правильную идентификацию.\n\n### Лучшие Практики\n\nПри разработке метамодулей:\n\n1. **Всегда устанавливайте источник в \"KSU\"** для операций монтирования - размонтированию ядра и zygisksu нужно это для правильного размонтирования\n2. **Обрабатывайте ошибки корректно** - процессы загрузки чувствительны ко времени\n3. **Уважайте стандартные флаги** - поддерживайте `skip_mount` и `disable`\n4. **Логируйте операции** - используйте `echo` или логирование для отладки\n5. **Тщательно тестируйте** - ошибки монтирования могут вызвать циклы загрузки\n6. **Документируйте поведение** - ясно объясняйте, что делает ваш метамодуль\n7. **Предоставляйте пути миграции** - помогайте пользователям переключаться с других решений\n\n### Тестирование Вашего Метамодуля\n\nПеред выпуском:\n\n1. **Тестируйте установку** на чистой настройке KernelSU\n2. **Проверяйте монтирование** с различными типами модулей\n3. **Проверяйте совместимость** с распространенными модулями\n4. **Тестируйте удаление** и очистку\n5. **Проверяйте производительность загрузки** (metamount.sh блокирует!)\n6. **Обеспечьте правильную обработку ошибок** для избежания циклов загрузки\n\n## Часто Задаваемые Вопросы\n\n### Нужен ли мне метамодуль?\n\n**Для пользователей**: Только если вы хотите использовать модули, требующие монтирования. Если вы используете только модули, которые запускают скрипты без изменения системных файлов, вам не нужен метамодуль.\n\n**Для разработчиков модулей**: Нет, вы разрабатываете модули как обычно. Пользователям нужен метамодуль только если ваш модуль требует монтирования.\n\n**Для продвинутых пользователей**: Только если вы хотите настроить поведение монтирования или создать альтернативные реализации монтирования.\n\n### Могу ли я иметь несколько метамодулей?\n\nНет. Одновременно может быть установлен только один метамодуль. Это предотвращает конфликты и обеспечивает предсказуемое поведение.\n\n### Что произойдет, если я удалю свой единственный метамодуль?\n\nМодули больше не будут монтироваться. Ваше устройство будет загружаться нормально, но модификации модулей не будут применяться, пока вы не установите другой метамодуль.\n\n### Обязателен ли meta-overlayfs?\n\nНет. Он обеспечивает стандартное монтирование overlayfs, совместимое с большинством модулей. Вы можете создать свой собственный метамодуль, если вам нужно другое поведение.\n\n## См. Также\n\n- [Руководство по Модулям](module.md) - Общая разработка модулей\n- [Разница с Magisk](difference-with-magisk.md) - Сравнение KernelSU и Magisk\n- [Как Собрать](how-to-build.md) - Сборка KernelSU из исходного кода\n"
  },
  {
    "path": "website/docs/ru_RU/guide/module-config.md",
    "content": "# Конфигурация модулей\n\nKernelSU предоставляет встроенную систему конфигурации, которая позволяет модулям хранить постоянные или временные настройки в формате ключ-значение. Конфигурации хранятся в бинарном формате по пути `/data/adb/ksu/module_configs/<module_id>/` и имеют следующие характеристики:\n\n## Типы конфигурации\n\n- **Постоянная конфигурация** (`persist.config`): сохраняется после перезагрузки до явного удаления или деинсталляции модуля\n- **Временная конфигурация** (`tmp.config`): автоматически очищается на этапе post-fs-data при каждой загрузке\n\nПри чтении конфигурации временные значения имеют приоритет над постоянными для одного и того же ключа.\n\n## Использование конфигурации в скриптах модуля\n\nВсе скрипты модуля (`post-fs-data.sh`, `service.sh`, `boot-completed.sh` и др.) выполняются с установленной переменной окружения `KSU_MODULE`, содержащей ID модуля. Вы можете использовать команды `ksud module config` для управления конфигурацией модуля:\n\n```bash\n# Получить значение конфигурации\nvalue=$(ksud module config get my_setting)\n\n# Установить постоянное значение конфигурации\nksud module config set my_setting \"some value\"\n\n# Установить временное значение конфигурации (очищается после перезагрузки)\nksud module config set --temp runtime_state \"active\"\n\n# Установить значение из stdin (полезно для многострочного или сложного текста)\nksud module config set my_key <<EOF\nмногострочное\nтекстовое значение\nEOF\n\n# Или передать через pipe из команды\necho \"значение\" | ksud module config set my_key\n\n# Явный флаг stdin\ncat file.json | ksud module config set json_data --stdin\n\n# Вывести все записи конфигурации (объединенные постоянные и временные)\nksud module config list\n\n# Удалить запись конфигурации\nksud module config delete my_setting\n\n# Удалить временную запись конфигурации\nksud module config delete --temp runtime_state\n\n# Очистить все постоянные конфигурации\nksud module config clear\n\n# Очистить все временные конфигурации\nksud module config clear --temp\n```\n\n## Ограничения валидации\n\nСистема конфигурации применяет следующие ограничения:\n\n- **Максимальная длина ключа**: 256 байт\n- **Максимальная длина значения**: 1МБ (1048576 байт)\n- **Максимальное количество записей конфигурации**: 32 на модуль\n- **Формат ключа**: Должен соответствовать `^[a-zA-Z][a-zA-Z0-9._-]+$` (как ID модуля)\n  - Должен начинаться с буквы\n  - Может содержать буквы, цифры, точки, подчеркивания или дефисы\n  - Минимальная длина: 2 символа\n- **Формат значения**: Без ограничений - может содержать любые UTF-8 символы, включая переносы строк и управляющие символы\n  - Хранится в бинарном формате с префиксом длины для безопасной обработки всех данных\n\n## Жизненный цикл\n\n- **При загрузке**: все временные конфигурации очищаются на этапе post-fs-data\n- **При деинсталляции модуля**: все конфигурации (постоянные и временные) автоматически удаляются\n- Конфигурации хранятся в бинарном формате с магическим числом `0x4b53554d` (\"KSUM\") и проверкой версии\n\n## Сценарии использования\n\nСистема конфигурации идеальна для:\n\n- **Пользовательские настройки**: хранение настроек модуля, которые пользователи настраивают через WebUI или action-скрипты\n- **Флаги функций**: включение/отключение функций модуля без переустановки\n- **Состояние во время выполнения**: отслеживание временного состояния, которое должно сбрасываться при перезагрузке (используйте временную конфигурацию)\n- **Настройки установки**: запоминание выбора, сделанного при установке модуля\n- **Сложные данные**: Хранение JSON, многострочного текста, данных в кодировке Base64 или любого структурированного содержимого (до 1МБ)\n\n::: tip ЛУЧШИЕ ПРАКТИКИ\n- Используйте постоянную конфигурацию для пользовательских настроек, которые должны сохраняться после перезагрузки\n- Используйте временную конфигурацию для состояния во время выполнения или флагов функций, которые должны сбрасываться при загрузке\n- Проверяйте значения конфигурации в скриптах перед их использованием\n- Используйте команду `ksud module config list` для отладки проблем с конфигурацией\n:::\n\n## Расширенные возможности\n\nСистема конфигурации модулей предоставляет специальные ключи конфигурации для расширенных сценариев использования:\n\n### Переопределение описания модуля {#overriding-module-description}\n\nВы можете динамически переопределить поле `description` из `module.prop`, установив ключ конфигурации `override.description`:\n\n```bash\n# Переопределить описание модуля\nksud module config set override.description \"Пользовательское описание, отображаемое в менеджере\"\n```\n\nПри получении списка модулей, если существует конфигурация `override.description`, она заменит исходное описание из `module.prop`. Это полезно для:\n- Отображения динамической информации о состоянии в описании модуля\n- Показа пользователям деталей конфигурации во время выполнения\n- Обновления описания на основе состояния модуля без переустановки\n\n### Объявление управляемых функций\n\nМодули могут объявлять, какими функциями KernelSU они управляют, используя шаблон конфигурации `manage.<feature>`. Поддерживаемые функции соответствуют внутреннему перечислению `FeatureId` в KernelSU:\n\n**Поддерживаемые функции:**\n- `su_compat` - Режим совместимости SU\n- `kernel_umount` - Автоматическое размонтирование ядра\n\n```bash\n# Объявить, что этот модуль управляет совместимостью SU и включает её\nksud module config set manage.su_compat true\n\n# Объявить, что этот модуль управляет размонтированием ядра и отключает его\nksud module config set manage.kernel_umount false\n\n# Удалить управление функцией (модуль больше не контролирует эту функцию)\nksud module config delete manage.su_compat\n```\n\n**Как это работает:**\n- Наличие ключа `manage.<feature>` указывает, что модуль управляет этой функцией\n- Значение указывает желаемое состояние: `true`/`1` для включения, `false`/`0` (или любое другое значение) для отключения\n- Чтобы прекратить управление функцией, полностью удалите ключ конфигурации\n\nУправляемые функции доступны через API списка модулей как поле `managedFeatures` (строка, разделённая запятыми). Это позволяет:\n- Менеджеру KernelSU определять, какие модули управляют какими функциями KernelSU\n- Предотвращать конфликты, когда несколько модулей пытаются управлять одной и той же функцией\n- Улучшить координацию между модулями и основным функционалом KernelSU\n\n::: warning ТОЛЬКО ПОДДЕРЖИВАЕМЫЕ ФУНКЦИИ\nИспользуйте только предопределённые имена функций, перечисленные выше (`su_compat`, `kernel_umount`). Они соответствуют реальным внутренним функциям KernelSU. Использование других имён функций не вызовет ошибок, но не будет иметь никакого функционального назначения.\n:::\n"
  },
  {
    "path": "website/docs/ru_RU/guide/module-webui.md",
    "content": "# Веб-интерфейс модуля\n\nПомимо выполнения загрузочных скриптов и правки системных файлов, модули KernelSU могут показывать пользовательский интерфейс и напрямую взаимодействовать с пользователем.\n\nМодуль может описать страницу на HTML + CSS + JavaScript, используя любые веб‑технологии. Менеджер KernelSU отображает такие страницы через WebView и предоставляет API для взаимодействия с системой, например для запуска shell-команд.\n\n## Каталог `webroot`\n\nФайлы веб-ресурсов должны располагаться в подкаталоге `webroot` в корне модуля, и там **ОБЯЗАТЕЛЬНО** должен находиться файл `index.html`, выступающий точкой входа. Самая простая структура модуля с веб-интерфейсом выглядит так:\n\n```txt\n❯ tree .\n.\n|-- module.prop\n`-- webroot\n    `-- index.html\n```\n\n::: warning\nПри установке модуля KernelSU автоматически задаёт права доступа и SELinux-контекст для этого каталога. Если вы не уверены, что делаете, не меняйте разрешения вручную!\n:::\n\nЕсли страница содержит CSS или JavaScript, их тоже нужно положить в этот каталог.\n\n## JavaScript API\n\nЕсли это только страница отображения, она работает как обычный веб-сайт. Но самое главное — KernelSU предоставляет набор системных API, позволяющих реализовать модуль‑специфичные функции.\n\nKernelSU предлагает JavaScript-библиотеку, опубликованную в [npm](https://www.npmjs.com/package/kernelsu), которую можно использовать в коде страницы.\n\nНапример, можно выполнить shell-команду, чтобы получить конфигурацию или изменить свойство:\n\n```JavaScript\nimport { exec } from 'kernelsu';\n\nconst { errno, stdout } = exec(\"getprop ro.product.model\");\n```\n\nТакже можно переключить страницу в полноэкранный режим или показать toast.\n\n[Документация по API](https://www.npmjs.com/package/kernelsu)\n\nЕсли существующего API недостаточно или им неудобно пользоваться, оставьте нам предложение [здесь](https://github.com/tiann/KernelSU/issues)!\n\n## Несколько советов\n\n1. `localStorage` можно использовать как обычно для хранения данных, но помните, что всё будет удалено при удалении приложения‑менеджера. Для постоянного хранения сохраните данные вручную в отдельном каталоге.\n2. Для простых страниц мы рекомендуем использовать [parceljs](https://parceljs.org/) для сборки. Он не требует начальной настройки и очень прост. Но если вы опытный фронтенд‑разработчик или у вас есть собственные предпочтения, смело используйте любой другой инструмент!\n"
  },
  {
    "path": "website/docs/ru_RU/guide/module.md",
    "content": "# Руководство по разработке модулей {#introduction}\n\nKernelSU предоставляет механизм модулей, позволяющий добиться эффекта модификации системного каталога при сохранении целостности системного раздела. Этот механизм принято называть \"бессистемным\".\n\nМодульный механизм KernelSU практически аналогичен механизму Magisk. Если вы знакомы с разработкой модулей Magisk, то разработка модулей KernelSU очень похожа. Представление модулей ниже можно пропустить, достаточно прочитать [различия-с-magisk] (difference-with-magisk.md).\n\n::: warning МЕТАМОДУЛЬ ТРЕБУЕТСЯ ТОЛЬКО ДЛЯ МОДИФИКАЦИИ СИСТЕМНЫХ ФАЙЛОВ\nKernelSU использует архитектуру [метамодулей](metamodule.md) для монтирования директории `system`. **Только если вашему модулю нужно модифицировать файлы `/system`** (через директорию `system`), вам необходимо установить метамодуль (например, [meta-overlayfs](https://github.com/tiann/KernelSU/releases)). Другие функции модулей, такие как скрипты, правила sepolicy и system.prop, работают без метамодуля.\n:::\n\n## WebUI\n\nKernelSU modules support displaying interfaces and interacting with users. See the [WebUI documentation](module-webui.md) for additional details.\n\n## Конфигурация модулей\n\nKernelSU предоставляет встроенную систему конфигурации, которая позволяет модулям хранить постоянные или временные настройки в формате ключ-значение. Подробности смотрите в [документации по конфигурации модулей](module-config.md).\n\n## Busybox\n\nВ комплект поставки KernelSU входит полнофункциональный бинарный файл BusyBox (включая полную поддержку SELinux). Исполняемый файл находится по адресу `/data/adb/ksu/bin/busybox`. BusyBox от KernelSU поддерживает переключаемый во время работы \"ASH Standalone Shell Mode\". Этот автономный режим означает, что при запуске в оболочке `ash` BusyBox каждая команда будет напрямую использовать апплет внутри BusyBox, независимо от того, что задано в качестве `PATH`. Например, такие команды, как `ls`, `rm`, `chmod` будут **НЕ** использовать то, что находится в `PATH` (в случае Android по умолчанию это будут `/system/bin/ls`, `/system/bin/rm` и `/system/bin/chmod` соответственно), а вместо этого будут напрямую вызывать внутренние апплеты BusyBox. Это гарантирует, что скрипты всегда будут выполняться в предсказуемом окружении и всегда будут иметь полный набор команд, независимо от того, на какой версии Android они выполняются. Чтобы заставить команду _не_ использовать BusyBox, необходимо вызвать исполняемый файл с полными путями.\n\nКаждый сценарий оболочки, запущенный в контексте KernelSU, будет выполняться в оболочке BusyBox `ash` с включенным автономным режимом. Для сторонних разработчиков это касается всех загрузочных скриптов и скриптов установки модулей.\n\nДля тех, кто хочет использовать эту возможность \"Автономного режима\" вне KernelSU, есть два способа включить ее:\n\n1. Установите переменной окружения `ASH_STANDALONE` значение `1`<br>Пример: `ASH_STANDALONE=1 /data/adb/ksu/bin/busybox sh <script>`\n2. Переключитесь с помощью параметров командной строки:<br>`/data/adb/ksu/bin/busybox sh -o standalone <script>`\n\nЧтобы убедиться, что все последующие запуски оболочки `sh` также выполняются в автономном режиме, предпочтительным методом является вариант 1 (и это то, что KernelSU и менеджер KernelSU используют внутри), поскольку переменные окружения наследуются вплоть до дочерних процессов.\n\n::: tip отличие от Magisk\n\nBusyBox в KernelSU теперь использует бинарный файл, скомпилированный непосредственно из проекта Magisk. **Поэтому вам не нужно беспокоиться о проблемах совместимости между скриптами BusyBox в Magisk и KernelSU, поскольку они абсолютно одинаковы!\n:::\n\n## Модули KernelSU {#kernelsu-modules}\n\nМодуль KernelSU - это папка, размещенная в каталоге `/data/adb/modules` и имеющая следующую структуру:\n\n```txt\n/data/adb/modules\n├── .\n├── .\n|\n├── $MODID                  <--- Папка имеет имя с идентификатором модуля\n│   │\n│   │      *** Идентификация модуля ***\n│   │\n│   ├── module.prop         <--- В этом файле хранятся метаданные модуля\n│   │\n│   │      *** Основное содержимое ***\n│   │\n│   ├── system              <--- Эта папка будет смонтирована, если skip_mount не существует\n│   │   ├── ...\n│   │   ├── ...\n│   │   └── ...\n│   │\n│   │      *** Флаги состояния ***\n│   │\n│   ├── skip_mount          <--- Если он существует, то KernelSU НЕ будет монтировать вашу системную папку\n│   ├── disable             <--- Если модуль существует, то он будет отключен\n│   ├── remove              <--- Если модуль существует, то при следующей перезагрузке он будет удален\n│   │\n│   │      *** Необязательные файлы ***\n│   │\n│   ├── post-fs-data.sh     <--- Этот скрипт будет выполняться в post-fs-data\n│   ├── service.sh          <--- Этот скрипт будет выполняться в сервисе late_start\n|   ├── uninstall.sh        <--- Этот скрипт будет выполнен, когда KernelSU удалит ваш модуль\n│   ├── system.prop         <--- Свойства из этого файла будут загружены в качестве системных свойств программой resetprop\n│   ├── sepolicy.rule       <--- Дополнительные пользовательские правила sepolicy\n│   │\n│   │      *** Автоматически генерируется, НЕЛЬЗЯ создавать или изменять вручную ***\n│   │\n│   ├── vendor              <--- Символьная ссылка на $MODID/system/vendor\n│   ├── product             <--- Символьная ссылка на $MODID/system/product\n│   ├── system_ext          <--- Симлинк на $MODID/system/system_ext\n│   │\n│   │      *** Допускается использование любых дополнительных файлов/папок ***\n│   │\n│   ├── ...\n│   └── ...\n|\n├── another_module\n│   ├── .\n│   └── .\n├── .\n├── .\n```\n\n::: tip различия с Magisk\nKernelSU не имеет встроенной поддержки Zygisk, поэтому в модуле нет содержимого, связанного с Zygisk. Однако для поддержки модулей Zygisk можно использовать [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext). В этом случае содержимое модуля Zygisk идентично содержимому, поддерживаемому Magisk.\n:::\n\n### module.prop\n\nmodule.prop - это конфигурационный файл модуля. В KernelSU, если модуль не содержит этого файла, он не будет распознан как модуль. Формат этого файла следующий:\n\n```txt\nid=<string>\nname=<string>\nversion=<string>\nversionCode=<int>\nauthor=<string>\ndescription=<string>\nupdateJson=<url> (optional)\nactionIcon=<path> (optional)\nwebuiIcon=<path> (optional)\n```\n\n- `id` должно соответствовать данному регулярному выражению: `^[a-zA-Z][a-zA-Z0-9._-]+$`<br>\n  экс: ✓ `a_module`, ✓ `a.module`, ✓ `module-101`, ✗ `a module`, ✗ `1_module`, ✗ `-a-module`<br>\n  Это **уникальный идентификатор** вашего модуля. Не следует изменять его после публикации.\n- `versionCode` должен быть **целым**. Это используется для сравнения версий.\n- Другими, не упомянутыми выше, могут быть любые **однострочные** строки.\n- Обязательно используйте тип перевода строки `UNIX (LF)`, а не `Windows (CR+LF)` или `Macintosh (CR)`.\n- `actionIcon` и `webuiIcon` — необязательные пути к изображениям, которые\n  используются как значки по умолчанию для ярлыков действия и WebUI модуля в\n  менеджере. Эти пути должны быть относительными к корневому каталогу модуля. Например,\n  `actionIcon=icon/icon.png` будет интерпретирован как `<MODDIR>/icon/icon.png`.\n\n::: tip ДИНАМИЧЕСКОЕ ОПИСАНИЕ\nПоле `description` может быть динамически переопределено во время выполнения с помощью системы конфигурации модулей. Подробности см. в разделе [Переопределение описания модуля](module-config.md#overriding-module-description).\n:::\n\n### Сценарии командной оболочки {#shell-scripts}\n\nЧтобы понять разницу между `post-fs-data.sh` и `Service.sh`, прочитайте раздел [Boot Scripts](#boot-scripts). Для большинства разработчиков модулей `service.sh` должно быть достаточно, если вам нужно просто запустить загрузочный скрипт.\n\nВо всех скриптах вашего модуля используйте `MODDIR=${0%/*}` для получения пути к базовому каталогу вашего модуля; **НЕ** кодируйте жестко путь к вашему модулю в скриптах.\n\n::: tip различия с Magisk\nС помощью переменной окружения KSU можно определить, выполняется ли сценарий в KernelSU или в Magisk. Если скрипт выполняется в KernelSU, то это значение будет равно true.\n:::\n\n### каталог `system` {#system-directories}\n\nПосле загрузки системы содержимое этого каталога будет наложено поверх раздела /system. Это означает, что:\n\n::: tip ТРЕБОВАНИЕ МЕТАМОДУЛЯ\nДиректория `system` монтируется только если у вас установлен метамодуль, предоставляющий функциональность монтирования (например, `meta-overlayfs`). Метамодуль обрабатывает способ монтирования модулей. См. [Руководство по метамодулям](metamodule.md) для получения дополнительной информации.\n:::\n\n1. Файлы с теми же именами, что и в соответствующем каталоге в системе, будут перезаписаны файлами в этом каталоге.\n2. Папки с теми же именами, что и в соответствующем каталоге в системе, будут объединены с папками в этом каталоге.\n\nЕсли вы хотите удалить файл или папку в исходном каталоге системы, необходимо создать файл с тем же именем, что и файл/папка, в каталоге модуля с помощью команды `mknod filename c 0 0`. Таким образом, система overlayfs автоматически \"забелит\" этот файл, как если бы он был удален (раздел /system при этом фактически не изменится).\n\nВы также можете объявить в `customize.sh` переменную с именем `REMOVE`, содержащую список каталогов для выполнения операций удаления, и KernelSU автоматически выполнит команду `mknod <TARGET> c 0 0` в соответствующих каталогах модуля. Например:\n\n```sh\nREMOVE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n```\n\nВ приведенном выше списке будут выполнены команды `mknod $MODPATH/system/app/YouTuBe c 0 0` и `mknod $MODPATH/system/app/Bloatware c 0 0`; при этом `/system/app/YouTube` и `/system/app/Bloatware` будут удалены после вступления модуля в силу.\n\nЕсли вы хотите заменить каталог в системе, то необходимо создать каталог с тем же путем в каталоге модуля, а затем установить для этого каталога атрибут `setfattr -n trusted.overlay.opaque -v y <TARGET>`. Таким образом, система overlayfs автоматически заменит соответствующий каталог в системе (без изменения раздела /system).\n\nВы можете объявить в файле `customize.sh` переменную с именем `REPLACE`, содержащую список заменяемых каталогов, и KernelSU автоматически выполнит соответствующие операции в каталоге вашего модуля. Например:\n\nREPLACE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n\nВ этом списке будут автоматически созданы каталоги `$MODPATH/system/app/YouTube` и `$MODPATH/system/app/Bloatware`, а затем выполнены команды `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/YouTube` и `setfattr -n trusted.overlay.opaque -v y $MODPATH/system/app/Bloatware`. После вступления модуля в силу каталоги `/system/app/YouTube` и `/system/app/Bloatware` будут заменены на пустые.\n\n::: tip различия с Magisk\n\nKernelSU использует архитектуру [метамодулей](metamodule.md), где монтирование делегируется подключаемым метамодулям. Официальный метамодуль `meta-overlayfs` использует OverlayFS ядра для бессистемных модификаций, в то время как Magisk использует magic mount (bind mount), встроенный непосредственно в его ядро. Оба достигают одной цели: модификация файлов `/system` без физического изменения раздела `/system`. Подход KernelSU обеспечивает большую гибкость и уменьшает поверхность обнаружения.\n:::\n\nЕсли вы заинтересованы в использовании overlayfs, рекомендуется прочитать [документацию по overlayfs](https://docs.kernel.org/filesystems/overlayfs.html) ядра Linux.\n\n### system.prop\n\nЭтот файл имеет тот же формат, что и `build.prop`. Каждая строка состоит из `[key]=[value]`.\n\n### sepolicy.rule\n\nЕсли для вашего модуля требуются дополнительные патчи sepolicy, добавьте эти правила в данный файл. Каждая строка в этом файле будет рассматриваться как утверждение политики.\n\n## Установщик модулей {#module-installer}\n\nИнсталлятор модуля KernelSU - это модуль KernelSU, упакованный в zip-файл, который может быть прошит в APP-менеджере KernelSU. Простейший установщик модуля KernelSU - это просто модуль KernelSU, упакованный в zip-файл.\n\n```txt\nmodule.zip\n│\n├── customize.sh                       <--- (Необязательно, более подробно позже)\n│                                           Этот скрипт будет использоваться в update-binary\n├── ...\n├── ...  /* Остальные файлы модуля */\n│\n```\n\n:::warning\nМодуль KernelSU НЕ поддерживается для установки в пользовательское Recovery!!!\n:::\n\n### Персонализация {#customizing-installation}\n\nЕсли вам необходимо настроить процесс установки модуля, то в качестве опции вы можете создать в программе установки скрипт с именем `customize.sh`. Этот скрипт будет _источником_ (не исполняться!) сценария установщика модуля после извлечения всех файлов и применения стандартных разрешений и secontext. Это очень удобно, если ваш модуль требует дополнительной настройки в зависимости от ABI устройства, или вам необходимо установить специальные разрешения/секонтекст для некоторых файлов модуля.\n\nЕсли вы хотите полностью контролировать и настраивать процесс установки, объявите `SKIPUNZIP=1` в файле `customize.sh`, чтобы пропустить все шаги установки по умолчанию. При этом ваш `customize.sh` будет сам отвечать за установку.\n\nСценарий `customize.sh` запускается в оболочке BusyBox `ash` KernelSU с включенным \"Автономным режимом\". Доступны следующие переменные и функции:\n\n#### Переменные\n\n- `KSU` (bool): переменная, отмечающая, что скрипт выполняется в окружении KernelSU, причем значение этой переменной всегда будет true. Ее можно использовать для различения KernelSU и Magisk.\n- `KSU_VER` (string): строка версии текущего установленного KernelSU (например, `v0.4.0`)\n- `KSU_VER_CODE` (int): код версии текущего установленного KernelSU в пользовательском пространстве (например, `10672`)\n- `KSU_KERNEL_VER_CODE` (int): код версии текущей установленной KernelSU в пространстве ядра (например, `10672`)\n- `BOOTMODE` (bool): в KernelSU всегда должно быть `true`.\n- `MODPATH` (path): путь, по которому должны быть установлены файлы ваших модулей\n- `TMPDIR` (path): место, где вы можете временно хранить файлы\n- `ZIPFILE` (path): установочный zip-архив вашего модуля\n- `ARCH` (string): архитектура процессора устройства. Значение: `arm`, `arm64`, `x86` или `x64`.\n- `IS64BIT` (bool): `true`, если `$ARCH` имеет значение `arm64` или `x64`.\n- `API` (int): уровень API (версия Android) устройства (например, `23` для Android 6.0)\n\n::: warning\nВ KernelSU MAGISK_VER_CODE всегда равен 25200, а MAGISK_VER всегда равен v25.2. Пожалуйста, не используйте эти две переменные для определения того, запущен ли он на KernelSU или нет.\n:::\n\n#### Функции {#functions}\n\n```txt\nui_print <msg>\n    вывести <msg> на консоль\n    Избегайте использования 'echo', так как он не будет отображаться в консоли пользовательского recovery\n\nabort <msg>\n    вывести сообщение об ошибке <msg> на консоль и завершить установку\n    Избегайте использования команды 'exit', так как в этом случае будут пропущены шаги очистки завершения установки\n\nset_perm <target> <owner> <group> <permission> [context]\n    если [context] не задан, то по умолчанию используется \"u:object_r:system_file:s0\".\n    Эта функция является сокращением для следующих команд:\n       chown owner.group target\n       chmod permission target\n       chcon context target\n\nset_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]\n    если [context] не задан, то по умолчанию используется \"u:object_r:system_file:s0\".\n    для всех файлов в <directory> будет вызвана команда:\n       set_perm file owner group filepermission context\n    для всех каталогов в <directory> (включая себя самого), он вызовет:\n       set_perm dir owner group dirpermission context\n```\n\n## Загрузочные сценарии {#boot-scripts}\n\nВ KernelSU скрипты делятся на два типа в зависимости от режима их работы: режим post-fs-data и режим late_start service:\n\n- режим post-fs-data\n  - Эта стадия является БЛОКИРУЮЩЕЙ. Процесс загрузки приостанавливается до завершения выполнения или по истечении 10 секунд.\n  - Сценарии запускаются до того, как будут смонтированы какие-либо модули. Это позволяет разработчику модулей динамически настраивать свои модули до того, как они будут смонтированы.\n  - Этот этап происходит до запуска Zygote, что практически означает, что все в Android\n  - **ПРЕДУПРЕЖДЕНИЕ:** использование `setprop` приведет к блокировке процесса загрузки! Вместо этого используйте `resetprop -n <prop_name> <prop_value>`.\n  - Запускайте скрипты в этом режиме только в случае необходимости.\n- режим обслуживания late_start\n  - Эта стадия является НЕБЛОКИРУЮЩЕЙ. Ваш скрипт выполняется параллельно с остальным процессом загрузки.\n  - **Это рекомендуемый этап для запуска большинства скриптов.**\n\nВ KernelSU скрипты запуска делятся на два типа по месту их хранения: общие скрипты и скрипты модулей:\n\n- Общие скрипты\n  - Размещаются в файлах `/data/adb/post-fs-data.d` или `/data/adb/service.d`.\n  - Выполняется только в том случае, если скрипт установлен как исполняемый (`chmod +x script.sh`)\n  - Скрипты в `post-fs-data.d` выполняются в режиме post-fs-data, а скрипты в `service.d` - в режиме late_start service.\n  - Модули не должны **НЕ** добавлять общие скрипты при установке\n- Скрипты модуля\n  - Размещаются в отдельной папке модуля\n  - Выполняются только в том случае, если модуль включен\n  - `post-fs-data.sh` запускается в режиме post-fs-data, а `service.sh` - в режиме late_start service.\n\nВсе загрузочные скрипты будут выполняться в оболочке BusyBox `ash` от KernelSU с включенным \"Автономным режимом\".\n\n## Режим late-load {#late-load-mode}\n\nПомимо стандартного процесса загрузки, описанного выше, KernelSU поддерживает **режим late-load** для сценариев LKM (загружаемый модуль ядра). В этом режиме модуль ядра KernelSU загружается **после полной загрузки системы**, а не во время процесса init.\n\n### Когда происходит late-load?\n\nLate-load запускается командой `ksud late-load`. Эта команда:\n\n1. Определяет текущую версию KMI и загружает соответствующий `kernelsu.ko` из встроенных ресурсов.\n2. Выполняет инициализацию модуля (правила SELinux, список разрешений, функции и т.д.), которая обычно происходит во время загрузки.\n\nПоскольку система уже полностью запущена, некоторые механизмы времени загрузки недоступны или не нужны.\n\n### Отличия от стандартной загрузки\n\n| Поведение | Стандартная загрузка | Режим late-load |\n|-----------|:---:|:---:|\n| Модуль ядра загружен init (PID 1) | Да | Нет (загружен после загрузки) |\n| Хуки kprobe ksud (execve/read/fstat/input) | Да | Пропущены |\n| Обнаружение безопасного режима (клавиша громкости) | Да | Всегда отключено |\n| Захват журнала загрузки (logcat/dmesg) | Да | Пропущен |\n| Проверка сосуществования с Magisk | Да | Пропущена |\n| Событие `post-fs-data` отправлено ядру | Да | Пропущено |\n| Событие `boot-completed` отправлено ядру | Да | Установлено напрямую при инициализации |\n| Скрипты `post-fs-data.sh` / `post-fs-data.d/` | Да | Заменены этапом `late-load` |\n| Загрузка `system.prop` | Да | Да |\n| Монтирование OverlayFS (metamodule) | Да | Да |\n| Скрипты `post-mount.sh` / `post-mount.d/` | Да | Да |\n| Скрипты `service.sh` / `service.d/` | Да | Да |\n| Скрипты `boot-completed.sh` / `boot-completed.d/` | Да | Да |\n| Переменная окружения `KSU_LATE_LOAD` | Не установлена | Установлена в `1` |\n| Флаг info ядра `0x4` | Не установлен | Установлен |\n\n### Порядок выполнения скриптов\n\nВ режиме late-load порядок выполнения скриптов следующий:\n\n```txt\nksud late-load:\n  1. Загрузить kernelsu.ko (если ещё не загружен)\n  2. Извлечь бинарные файлы, обработать обновления модулей, загрузить правила SELinux, инициализировать функции\n  3. Выполнить скрипты late-load.d/ и скрипты late-load модулей (блокирующе)\n  4. Загрузить system.prop (resetprop -n)\n  5. Выполнить скрипт монтирования metamodule (OverlayFS)\n  6. Выполнить скрипты post-mount.d/ и post-mount.sh модулей (блокирующе)\n  7. Выполнить скрипты service.d/ и service.sh модулей (неблокирующе)\n  8. Выполнить скрипты boot-completed.d/ и boot-completed.sh модулей (неблокирующе)\n```\n\n### Скрипты, специфичные для late-load\n\nМодули могут предоставить скрипт `late-load.sh`, который выполняется **только** в режиме late-load, как замена `post-fs-data.sh`. Этот скрипт выполняется до монтирования OverlayFS, аналогично `post-fs-data.sh` в стандартном потоке.\n\nКроме того, общие скрипты можно размещать в `/data/adb/late-load.d/` для выполнения на этом этапе.\n\n### Обнаружение режима late-load в скриптах\n\nМодули могут определить режим late-load, проверив переменную окружения `KSU_LATE_LOAD`:\n\n```sh\nif [ \"$KSU_LATE_LOAD\" = \"1\" ]; then\n    # Работа в режиме late-load\n    echo \"Late-load mode detected\"\nfi\n```\n\nЭто позволяет модулям соответствующим образом корректировать своё поведение, например, пропуская операции, необходимые только при ранней загрузке.\n"
  },
  {
    "path": "website/docs/ru_RU/guide/rescue-from-bootloop.md",
    "content": "# Выход из циклической загрузки {#intruduction}\n\nПри прошивке устройства могут возникать ситуации, когда устройство становится \"окирпиченным\". Теоретически, если использовать fastboot только для прошивки загрузочного раздела или установить неподходящие модули, из-за которых устройство не загружается, то это можно восстановить соответствующими операциями. В данном документе описаны некоторые экстренные методы восстановления работоспособности \"окирпиченного\" устройства.\n\n## Кирпич путем перепрошивки загрузочного раздела\n\nВ KernelSU при прошивке загрузочного раздела могут возникнуть следующие ситуации:\n\n1. Загрузочный образ прошивается в неправильном формате. Например, если формат загрузки телефона - `gz`, а вы прошили образ в формате `lz4`, то телефон не сможет загрузиться.\n2. Для корректной загрузки телефона необходимо отключить проверку AVB (обычно для этого требуется стереть все данные на телефоне).\n3. Ядро содержит ошибки или не подходит для прошивки телефона.\n\nНезависимо от ситуации, восстановить работоспособность можно путем **прошивки стокового загрузочного образа**. Поэтому в начале руководства по установке мы настоятельно рекомендуем создать резервную копию стокового загрузочного образа перед прошивкой. Если у вас нет резервной копии, вы можете получить оригинальную заводскую загрузку от других пользователей с таким же устройством, как у вас, или из официальной прошивки.\n\n## Окирпичивание из-за модулей\n\nУстановка модулей может быть более распространенной причиной окирпичивания устройства, но мы должны серьезно предупредить вас: **Не устанавливайте модули из неизвестных источников**! Поскольку модули обладают правами root, они могут нанести непоправимый ущерб вашему устройству!\n\n### Нормальные модули\n\nЕсли вы прошили модуль, безопасность которого доказана, но он приводит к невозможности загрузки устройства, то такая ситуация легко восстанавливается в KernelSU без каких-либо проблем. KernelSU имеет встроенные механизмы для спасения устройства, в том числе следующие:\n\n1. Обновление AB\n2. Восстановление при нажатии клавиши уменьшения громкости\n\n#### AB-обновление {#ab-update}\n\nМеханизм обновления модулей в KernelSU основан на механизме AB-обновления, используемом в OTA-обновлениях системы Android. При установке нового модуля или обновлении существующего он не будет напрямую изменять текущий файл модуля. Вместо этого все модули будут встроены в другой образ обновления. После перезагрузки системы она попытается начать использовать этот образ обновления. Если система Android успешно загрузится, то модули будут действительно обновлены.\n\nПоэтому самым простым и наиболее часто используемым методом спасения устройства является **принудительная перезагрузка**. Если после прошивки модуля не удается запустить систему, можно нажать и удерживать кнопку питания более 10 секунд, после чего система автоматически перезагрузится; после перезагрузки произойдет откат к состоянию до обновления модуля, а ранее обновленные модули будут автоматически отключены.\n\n#### Спасение, с зажатой клавишей уменьшения громкости {#volume-down}\n\nЕсли обновление AB не помогло решить проблему, можно попробовать использовать **Безопасный режим**. В безопасном режиме все модули отключены.\n\nВойти в безопасный режим можно двумя способами:\n\n1. Встроенный безопасный режим некоторых систем; некоторые системы имеют встроенный безопасный режим, доступ к которому осуществляется долгим нажатием кнопки уменьшения громкости, в то время как другие (например, MIUI) могут включить безопасный режим в Recovery. При входе в безопасный режим системы KernelSU также переходит в безопасный режим и автоматически отключает модули.\n2. Встроенный безопасный режим KernelSU; метод работы заключается в том, что после первого экрана загрузки необходимо **непрерывно нажать клавишу уменьшения громкости более трех раз**. Обратите внимание, что именно нажать-отпустить, нажать-отпустить, нажать-отпустить, а не нажать и удерживать.\n\nПосле входа в безопасный режим все модули на странице модулей менеджера KernelSU Manager отключаются, но можно выполнить операцию \"деинсталляция\" для удаления модулей, которые могут вызывать проблемы.\n\nВстроенный безопасный режим реализован в ядре, поэтому вероятность пропуска ключевых событий из-за перехвата исключена. Однако для ядер, отличных от ГКИ, может потребоваться ручная интеграция кода, и за рекомендациями можно обратиться к официальной документации.\n\n### Вредоносные модули\n\nЕсли описанные выше способы не помогли спасти устройство, то высока вероятность того, что установленный модуль имеет вредоносные операции или повредил устройство иным способом. В этом случае есть только два варианта:\n\n1. Стереть данные и прошить официальную систему.\n2. Обратиться в сервисную службу.\n"
  },
  {
    "path": "website/docs/ru_RU/guide/unofficially-support-devices.md",
    "content": "# Неофициально поддерживаемые устройства\n\n::: warning\nЭтот документ предназначен только для архивных ссылок и больше не обновляется.\nНачиная с KernelSU v1.0, мы отказались от официальной поддержки устройств не-GKI.\n:::\n\n::: warning\nНа этой странице представлены ядра для устройств, не поддерживающих ГКИ и поддерживающих KernelSU, которые поддерживаются другими разработчиками.\n:::\n\n::: warning\nЭта страница предназначена только для поиска исходного кода, соответствующего вашему устройству, и **НЕ** означает, что исходный код был проверен _разработчиками KernelSU_. Вы должны использовать его на свой страх и риск.\n:::\n\n<script setup>\nimport data from '../../repos.json'\n</script>\n\n<table>\n   <thead>\n      <tr>\n         <th>Сопровождающий</th>\n         <th>Репозиторий</th>\n         <th>Поддерживаемое устройство</th>\n      </tr>\n   </thead>\n   <tbody>\n    <tr v-for=\"repo in data\" :key=\"repo.devices\">\n        <td><a :href=\"repo.maintainer_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.maintainer }}</a></td>\n        <td><a :href=\"repo.kernel_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.kernel_name }}</a></td>\n        <td>{{ repo.devices }}</td>\n    </tr>\n   </tbody>\n</table>\n"
  },
  {
    "path": "website/docs/ru_RU/guide/what-is-kernelsu.md",
    "content": "# Что такое KernelSU? {#introduction}\n\nKernelSU - это root-решение для устройств Android GKI, работающее в режиме ядра и предоставляющее root-права пользовательским приложениям непосредственно в пространстве ядра.\n\n## Особенности {#features}\n\nОсновной особенностью KernelSU является то, что он **основан на ядре**. KernelSU работает в режиме ядра, поэтому он может предоставить интерфейс ядра, которого раньше не было. Например, мы можем добавить аппаратную точку останова любому процессу в режиме ядра; мы можем получить доступ к физической памяти любого процесса без чьего-либо ведома; мы можем перехватить любой syscall в пространстве ядра; и т.д.\n\nКроме того, KernelSU предоставляет [систему metamodule](metamodule.md), которая является подключаемой архитектурой для управления модулями. В отличие от традиционных root-решений, которые встраивают логику монтирования в свое ядро, KernelSU делегирует это metamodules. Это позволяет устанавливать metamodules (например [meta-overlayfs](https://github.com/tiann/KernelSU/tree/main/userspace/meta-overlayfs)) для обеспечения бессистемных модификаций раздела `/system` и других разделов.\n\n## Как использовать {#how-to-use}\n\nПожалуйста, обратитесь к: [Установка](installation)\n\n## Как собрать {#how-to-build}\n\n[Как собрать](how-to-build)\n\n## Обсуждение {#discussion}\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n"
  },
  {
    "path": "website/docs/ru_RU/index.md",
    "content": "---\nlayout: home\ntitle: Основанное на ядре root-решение для Android\n\nhero:\n  name: KernelSU\n  text: Основанное на ядре root-решение для Android\n  tagline: \"\"\n  image:\n    src: /logo.png\n    alt: KernelSU\n  actions:\n    - theme: brand\n      text: Начало работы\n      link: /ru_RU/guide/what-is-kernelsu\n    - theme: alt\n      text: Посмотр на GitHub\n      link: https://github.com/tiann/KernelSU\n\nfeatures:\n  - title: Основанный на ядре\n    details: KernelSU работает в режиме ядра Linux, он имеет больше контроля над пользовательскими приложениями.\n  - title: Контроль доступа по белому списку\n    details: Только приложение, которому предоставлено разрешение root, может получить доступ к `su`, другие приложения не могут воспринимать su.\n  - title: Ограниченные root-права\n    details: KernelSU позволяет вам настраивать uid, gid, группы, возможности и правила SELinux для su. Заприте root-власть в клетке.\n  - title: Система Metamodule\n    details: Подключаемая модульная инфраструктура позволяет модифицировать /system без изменения системы. Установите metamodule (например meta-overlayfs) для включения монтирования модулей.\n\n"
  },
  {
    "path": "website/docs/vi_VN/guide/app-profile.md",
    "content": "# App Profile\n\nApp Profile là một cơ chế do KernelSU cung cấp để tùy chỉnh cấu hình của các ứng dụng khác nhau.\n\nĐối với các ứng dụng được cấp quyền root (tức là có thể sử dụng `su`), App Profile cũng có thể được gọi là Root Profile. Nó cho phép tùy chỉnh các quy tắc `uid`, `gid`, `groups`, `capabilities` và `SELinux` của lệnh `su`, do đó hạn chế các đặc quyền của người dùng root. Ví dụ: nó có thể chỉ cấp quyền mạng cho các ứng dụng tường lửa trong khi từ chối quyền truy cập tệp hoặc có thể cấp quyền shell thay vì quyền truy cập root đầy đủ cho các ứng dụng đóng băng: **giữ nguồn điện theo nguyên tắc đặc quyền tối thiểu.**\n\nĐối với các ứng dụng thông thường không có quyền root, App Profile có thể kiểm soát hành vi của hệ thống kernel và mô-đun đối với các ứng dụng này. Ví dụ, nó có thể xác định liệu các sửa đổi do mô-đun tạo ra có nên được giải quyết hay không. Hệ thống kernel và mô-đun có thể đưa ra quyết định dựa trên cấu hình này, chẳng hạn như thực hiện các hoạt động tương tự như \"hiding\"\n\n## Root Profile\n\n### UID, GID, và Groups\n\nHệ thống Linux có hai khái niệm: người dùng (user) và nhóm (group). Mỗi người dùng có một user ID (UID) và một người dùng có thể thuộc nhiều nhóm, mỗi nhóm có group ID (GID) riêng. Những ID này được sử dụng để xác định người dùng trong hệ thống và xác định tài nguyên hệ thống nào họ có thể truy cập.\n\nNgười dùng có UID bằng 0 được gọi là người dùng root và các nhóm có GID bằng 0 được gọi là nhóm root. Nhóm người dùng root thường giữ các đặc quyền hệ thống cao nhất.\n\nTrong trường hợp hệ thống Android, mỗi ứng dụng là một người dùng riêng biệt (không bao gồm các trường hợp UID dùng chung) với một UID duy nhất. Ví dụ: `0` đại diện cho người dùng root, `1000` đại diện cho `system`, `2000` đại diện cho ADB shell và các UID từ 10000 đến 19999 đại diện cho các ứng dụng thông thường.\n\n:::info\nỞ đây, UID được đề cập không giống với khái niệm nhiều người dùng hoặc hồ sơ công việc (Work profile) trong hệ thống Android. Hồ sơ công việc thực sự được triển khai bằng cách phân vùng phạm vi UID. Ví dụ: 10000-19999 đại diện cho người dùng chính, trong khi 110000-119999 đại diện cho hồ sơ công việc. Mỗi ứng dụng thông thường trong số đó đều có UID riêng.\n:::\n\nMỗi ứng dụng có thể có nhiều nhóm, với GID đại diện cho nhóm chính, thường khớp với UID. Các nhóm khác được gọi là nhóm bổ sung. Một số quyền nhất định được kiểm soát thông qua các nhóm, chẳng hạn như quyền truy cập mạng hoặc truy cập Bluetooth.\n\nVí dụ: nếu chúng ta thực thi lệnh `id` trong shell ADB, kết quả đầu ra có thể trông như thế này:\n\n```sh\noriole:/ $ id\nuid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),1078(ext_data_rw),1079(ext_obb_rw),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid),3012(readtracefs) context=u:r:shell:s0\n```\n\nỞ đây, UID là `2000` và GID (ID nhóm chính) cũng là `2000`. Ngoài ra, nó thuộc một số nhóm bổ sung, chẳng hạn như `inet` (biểu thị khả năng tạo ổ cắm `AF_INET` và `AF_INET6`) và `sdcard_rw` (biểu thị quyền đọc/ghi đối với thẻ SD).\n\nRoot Profile của KernelSU cho phép tùy chỉnh UID, GID và các nhóm cho quy trình gốc sau khi thực thi `su`. Ví dụ: Cấu hình gốc của ứng dụng gốc có thể đặt UID của nó thành `2000`, có nghĩa là khi sử dụng `su`, các quyền thực tế của ứng dụng sẽ ở cấp shell ADB. Nhóm `inet` có thể bị xóa, ngăn lệnh `su` truy cập mạng.\n\n:::tip Ghi chú\nHồ sơ ứng dụng chỉ kiểm soát các quyền của tiến trình gốc sau khi sử dụng `su`; nó không kiểm soát các quyền của ứng dụng. Nếu một ứng dụng đã yêu cầu quyền truy cập mạng, ứng dụng đó vẫn có thể truy cập mạng ngay cả khi không sử dụng `su`. Việc xóa nhóm `inet` khỏi `su` chỉ ngăn `su` truy cập mạng.\n:::\n\nRoot Profile được thực thi trong kernel và không dựa vào hành vi tự nguyện của các ứng dụng root, không giống như việc chuyển đổi người dùng hoặc nhóm thông qua `su`, việc cấp quyền `su` hoàn toàn phụ thuộc vào người dùng chứ không phải nhà phát triển.\n\n### Capabilities\n\nCapabilities (khả năng) là một cơ chế phân tách đặc quyền trong Linux.\n\nVới mục đích thực hiện kiểm tra quyền, việc triển khai UNIX truyền thống phân biệt hai loại quy trình: quy trình đặc quyền (có ID người dùng hiệu quả là 0, được gọi là siêu người dùng hoặc root) và quy trình không có đặc quyền (có UID hiệu dụng khác 0). Các quy trình đặc quyền bỏ qua tất cả các bước kiểm tra quyền của kernel, trong khi các quy trình không có đặc quyền phải chịu sự kiểm tra quyền đầy đủ dựa trên thông tin xác thực của quy trình (thường là: UID hiệu quả, GID hiệu quả và danh sách nhóm bổ sung).\n\nBắt đầu với Linux 2.2, Linux chia các đặc quyền truyền thống được liên kết với siêu người dùng thành các đơn vị riêng biệt, được gọi là các khả năng, có thể được bật và tắt một cách độc lập.\n\nMỗi Khả năng đại diện cho một hoặc nhiều đặc quyền. Ví dụ: `CAP_DAC_READ_SEARCH` thể hiện khả năng bỏ qua việc kiểm tra quyền để đọc tệp cũng như quyền đọc và thực thi thư mục. Nếu người dùng có UID hiệu dụng là `0` (người dùng root) thiếu khả năng `CAP_DAC_READ_SEARCH` hoặc cao hơn, điều này có nghĩa là ngay cả khi họ là root, họ không thể tùy ý đọc tệp.\n\nCấu hình gốc của KernelSU cho phép tùy chỉnh các Khả năng của tiến trình gốc sau khi thực thi `su`, nhờ đó đạt được việc cấp một phần \"quyền root\". Không giống như UID và GID đã nói ở trên, một số ứng dụng gốc nhất định yêu cầu UID là `0` sau khi sử dụng `su`. Trong những trường hợp như vậy, việc giới hạn Khả năng của người dùng root này bằng UID `0` có thể hạn chế các hoạt động được phép của họ.\n\n:::tip Rất Khuyến Nghị\nCapabilities của Linux [tài liệu chính thức](https://man7.org/linux/man-pages/man7/capabilities.7.html) cung cấp giải thích chi tiết về các khả năng mà mỗi Capabilities thể hiện. Nếu bạn có ý định tùy chỉnh Capabilities, bạn nên đọc tài liệu này trước.\n:::\n\n### SELinux\n\nSELinux là một cơ chế Kiểm Soát Truy Cập Bắt Buộc (Mandatory Access Control: MAC) mạnh mẽ. Nó hoạt động theo nguyên tắc **từ chối mặc định**: bất kỳ hành động nào không được cho phép rõ ràng đều bị từ chối.\n\nSELinux có thể chạy ở hai chế độ chung:\n\n1. Chế độ cho phép (Permissive mode): Các sự kiện từ chối được ghi lại nhưng không được thực thi.\n2. Chế độ thực thi (Enforcing mode): Các sự kiện từ chối được ghi lại và thực thi.\n\n:::warning Cảnh báo\nCác hệ thống Android hiện đại phụ thuộc rất nhiều vào SELinux để đảm bảo an ninh hệ thống tổng thể. Chúng tôi khuyên bạn không nên sử dụng bất kỳ hệ thống tùy chỉnh nào chạy ở \"chế độ cho phép\" vì nó không mang lại lợi thế đáng kể nào so với hệ thống mở hoàn toàn.\n:::\n\nViệc giải thích khái niệm đầy đủ về SELinux rất phức tạp và nằm ngoài phạm vi của tài liệu này. Trước tiên nên hiểu hoạt động của nó thông qua các tài nguyên sau:\n\n1. [Wikipedia](https://en.wikipedia.org/wiki/Security-Enhanced_Linux)\n2. [Red Hat: What Is SELinux?](https://www.redhat.com/en/topics/linux/what-is-selinux)\n3. [ArchLinux: SELinux](https://wiki.archlinux.org/title/SELinux)\n\nRoot Profile của KernelSU cho phép tùy chỉnh ngữ cảnh SELinux của tiến trình gốc sau khi thực thi `su`. Các quy tắc kiểm soát truy cập cụ thể có thể được đặt cho bối cảnh này để cho phép kiểm soát chi tiết hơn các quyền .\n\nTrong các trường hợp điển hình, khi một ứng dụng thực thi `su`, nó sẽ chuyển quy trình sang miền SELinux với **quyền truy cập không hạn chế**, chẳng hạn như `u:r:su:s0`. Thông qua Root Profile, miền này có thể được chuyển sang miền tùy chỉnh, chẳng hạn như `u:r:app1:s0` và một loạt quy tắc có thể được xác định cho miền này:\n\n```sh\ntype app1\nenforce app1\ntypeattribute app1 mlstrustedsubject\nallow app1 * * *\n```\n\nLưu ý rằng quy tắc `allow app1 * * *` chỉ được sử dụng cho mục đích minh họa. Trong thực tế, quy tắc này không nên được sử dụng rộng rãi vì nó không khác nhiều so với chế độ cho phép.\n\n### Escalation\n\nNếu cấu hình của Root Profile không được đặt đúng cách, một tình huống escalation (leo thang) có thể xảy ra: các hạn chế do Root Profile áp đặt có thể vô tình bị lỗi.\n\nVí dụ: nếu bạn cấp quyền root cho người dùng shell ADB (đây là trường hợp phổ biến), sau đó bạn cấp quyền root cho một ứng dụng thông thường nhưng định cấu hình cấu hình gốc của nó bằng UID 2000 (là UID của người dùng shell ADB) , ứng dụng có thể có được quyền truy cập root đầy đủ bằng cách thực hiện lệnh `su` hai lần:\n\n1. Lần thực thi `su` đầu tiên phải tuân theo sự thực thi của App Profile và sẽ chuyển sang UID `2000` (adb shell) thay vì `0` (root).\n2. Lần thực thi `su` thứ hai, vì UID là `2000` và bạn đã cấp quyền truy cập root cho UID `2000` (adb shell) trong cấu hình, ứng dụng sẽ có toàn quyền root.\n\n:::warning Ghi chú\nHành vi này hoàn toàn được mong đợi và không phải là lỗi. Vì vậy, chúng tôi khuyến nghị như sau:\n\nNếu bạn thực sự cần cấp quyền root cho ADB (ví dụ: với tư cách là nhà phát triển), bạn không nên thay đổi UID thành `2000` khi định cấu hình Root Profile. Sử dụng `1000` (hệ thống) sẽ là lựa chọn tốt hơn.\n:::\n\n## Non-Root Profile\n\n### Umount Modules\n\nKernelSU cung cấp một cơ chế systemless để sửa đổi các phân vùng hệ thống, đạt được thông qua việc gắn overlayfs. Tuy nhiên, một số ứng dụng có thể nhạy cảm với hành vi đó. Do đó, chúng ta có thể dỡ bỏ các mô-đun được gắn trên các ứng dụng này bằng cách đặt tùy chọn \"umount modules\".\n\nNgoài ra, giao diện cài đặt của trình quản lý KernelSU cung cấp một công tắc cho \"umount modules by default\". Theo mặc định, công tắc này được **bật**, có nghĩa là KernelSU hoặc một số mô-đun sẽ hủy tải các mô-đun cho ứng dụng này trừ khi áp dụng cài đặt bổ sung. Nếu bạn không thích cài đặt này hoặc nếu nó ảnh hưởng đến một số ứng dụng nhất định, bạn có các tùy chọn sau:\n\n1. Giữ nút chuyển cho \"umount modules by default\" và tắt riêng tùy chọn \"umount modules\" trong App Profile đối với các ứng dụng yêu cầu tải mô-đun (hoạt động như \"whitelist\").\n2. Tắt khóa chuyển cho \"umount modules by default\" và bật riêng tùy chọn \"umount modules\" trong App Profile cho các ứng dụng yêu cầu dỡ bỏ mô-đun (hoạt động như \"blacklist\").\n\n:::info\nTrong các thiết bị sử dụng kernel phiên bản 5.10 trở lên, kernel thực hiện việc dỡ tải các mô-đun. Tuy nhiên, đối với các thiết bị chạy phiên bản kernel dưới 5.10, công tắc này chỉ đơn thuần là một tùy chọn cấu hình và bản thân KernelSU không thực hiện bất kỳ hành động nào. Một số mô-đun, chẳng hạn như Zygisksu, có thể sử dụng công tắc này để xác định xem có cần thiết phải dỡ bỏ mô-đun hay không.\n:::\n"
  },
  {
    "path": "website/docs/vi_VN/guide/difference-with-magisk.md",
    "content": "# Sự khác biệt với Magisk\n\nMặc dù có nhiều điểm tương đồng giữa mô-đun KernelSU và mô-đun Magisk nhưng chắc chắn vẫn có một số khác biệt do cơ chế triển khai hoàn toàn khác nhau của chúng. Nếu muốn mô-đun của mình chạy trên cả Magisk và KernelSU, bạn phải hiểu những khác biệt này.\n\n## Điểm tương đồng\n\n- Định dạng file mô-đun: đều sử dụng định dạng zip để sắp xếp các mô-đun và định dạng của các mô-đun gần như giống nhau\n- Thư mục cài đặt mô-đun: cả hai đều nằm trong `/data/adb/modules`\n- systemless: cả hai đều hỗ trợ sửa đổi /system theo cách không có hệ thống thông qua các mô-đun\n- post-fs-data.sh: thời gian thực hiện và ngữ nghĩa hoàn toàn giống nhau\n- service.sh: thời gian thực hiện và ngữ nghĩa hoàn toàn giống nhau\n- system.prop: hoàn toàn giống nhau\n- sepolicy.rule: hoàn toàn giống nhau\n- BusyBox: các tập lệnh được chạy trong BusyBox với \"standalone mode\" được bật trong cả hai trường hợp\n\n## Điểm khác biệt\n\nTrước khi hiểu sự khác biệt, bạn cần biết cách phân biệt mô-đun của bạn đang chạy trong KernelSU hay Magisk. Bạn có thể sử dụng biến môi trường `KSU` để phân biệt nó ở tất cả những nơi bạn có thể chạy tập lệnh mô-đun (`customize.sh`, `post-fs-data.sh`, `service.sh`). Trong KernelSU, biến môi trường này sẽ được đặt thành `true`.\n\nDưới đây là một số khác biệt:\n\n- Không thể cài đặt các mô-đun KernelSU ở chế độ Recovery.\n- Các mô-đun KernelSU không có hỗ trợ tích hợp cho Zygisk (nhưng bạn có thể sử dụng các mô-đun Zygisk thông qua [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext).\n- Phương pháp thay thế hoặc xóa file trong module KernelSU hoàn toàn khác với Magisk. KernelSU không hỗ trợ phương thức `.replace`. Thay vào đó, bạn cần tạo một file cùng tên với `mknod filename c 0 0` để xóa file tương ứng.\n- Các thư mục của BusyBox khác nhau. BusyBox tích hợp trong KernelSU nằm ở `/data/adb/ksu/bin/busybox`, trong khi ở Magisk nó nằm ở `/data/adb/magisk/busybox`. **Lưu ý rằng đây là hoạt động nội bộ của KernelSU và có thể thay đổi trong tương lai!**\n- KernelSU không hỗ trợ file `.replace`; tuy nhiên, KernelSU hỗ trợ biến `REMOVE` và `REPLACE` để xóa hoặc thay thế các tệp và thư mục.\n- KernelSU thêm giai đoạn `boot-completed` để chạy một số script khi khởi động xong.\n- KernelSU thêm giai đoạn `post-mount` để chạy một số tập lệnh sau khi gắn overlayfs\n"
  },
  {
    "path": "website/docs/vi_VN/guide/faq.md",
    "content": "# FAQ\n\n## KernelSU có hỗ trợ thiết bị của tôi không?\n\nKernelSU hỗ trợ các thiết bị chạy Android với bootloader đã mở khóa. Tuy nhiên, hỗ trợ chính thức chỉ dành cho GKI Linux Kernel 5.10+ (trong thực tế, điều này có nghĩa là thiết bị của bạn cần có Android 12 out-of-the-box để được hỗ trợ).\n\nBạn có thể dễ dàng kiểm tra hỗ trợ cho thiết bị của mình thông qua ứng dụng quản lý KernelSU, có sẵn [tại đây](https://github.com/tiann/KernelSU/releases).\n\nNếu ứng dụng hiển thị `Not installed`, điều đó có nghĩa là thiết bị của bạn được KernelSU hỗ trợ chính thức.\n\nNếu ứng dụng hiển thị `Unsupported`, điều đó có nghĩa là thiết bị của bạn hiện không được hỗ trợ chính thức. Tuy nhiên, bạn có thể build mã nguồn kernel và tích hợp KernelSU để làm cho nó hoạt động, hoặc sử dụng [Thiết bị được hỗ trợ không chính thức](unofficially-support-devices).\n\n## KernelSU có cần mở khóa bootloader không?\n\nChắc chắn rồi.\n\n## KernelSU có hỗ trợ module không?\n\nCó, hầu hết các module Magisk hoạt động trên KernelSU. Tuy nhiên, nếu module của bạn cần sửa đổi các tệp `/system`, bạn cần cài đặt [metamodule](metamodule.md) (chẳng hạn như `meta-overlayfs`). Các tính năng module khác hoạt động mà không cần metamodule. Kiểm tra [Hướng dẫn Module](module.md) để biết thêm thông tin.\n\n## KernelSU có hỗ trợ Xposed không?\n\nCó, bạn có thể sử dụng LSPosed (hoặc các phái sinh Xposed hiện đại khác) với [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext).\n\n## KernelSU có hỗ trợ Zygisk không?\n\nKernelSU không có hỗ trợ Zygisk tích hợp sẵn, nhưng bạn có thể sử dụng module như [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) để hỗ trợ nó.\n\n## KernelSU có tương thích với Magisk không?\n\nHệ thống module của KernelSU xung đột với magic mount của Magisk. Nếu có bất kỳ module nào được kích hoạt trong KernelSU, toàn bộ Magisk sẽ ngừng hoạt động.\n\nTuy nhiên, nếu bạn chỉ sử dụng `su` của KernelSU, nó sẽ hoạt động tốt với Magisk. KernelSU sửa đổi `kernel`, trong khi Magisk sửa đổi `ramdisk`, cho phép cả hai hoạt động cùng nhau.\n\n## KernelSU sẽ thay thế Magisk?\n\nChúng tôi tin rằng không, và đó không phải là mục tiêu của chúng tôi. Magisk đã đủ tốt cho giải pháp root userspace và sẽ tồn tại lâu dài. Mục tiêu của KernelSU là cung cấp giao diện kernel cho người dùng, không phải để thay thế Magisk.\n\n## KernelSU có thể hỗ trợ các thiết bị không phải GKI không?\n\nCó thể. Nhưng bạn cần tải xuống mã nguồn kernel và tích hợp KernelSU vào source tree, sau đó tự biên dịch kernel.\n\n## KernelSU có thể hỗ trợ các thiết bị dưới Android 12 không?\n\nChính kernel thiết bị ảnh hưởng đến khả năng tương thích của KernelSU, và nó không liên quan gì đến phiên bản Android. Hạn chế duy nhất là các thiết bị được ra mắt với Android 12 phải có phiên bản kernel 5.10+ (thiết bị GKI). Vì vậy:\n\n1. Các thiết bị được ra mắt với Android 12 phải được hỗ trợ.\n2. Các thiết bị có kernel cũ (một số thiết bị với Android 12 cũng có kernel cũ) tương thích (bạn cần tự build kernel).\n\n## KernelSU có thể hỗ trợ kernel cũ không?\n\nCó thể. KernelSU hiện đã được backport cho kernel 4.14. Đối với các kernel cũ hơn, bạn cần tự backport, và PR luôn được chào đón!\n\n## Làm cách nào để tích hợp KernelSU cho kernel cũ?\n\nVui lòng kiểm tra hướng dẫn [Tích hợp cho thiết bị không phải GKI](how-to-integrate-for-non-gki).\n\n## Tại sao phiên bản Android của tôi là 13, nhưng kernel hiển thị \"android12-5.10\"?\n\nPhiên bản kernel không liên quan gì đến phiên bản Android. Nếu bạn cần flash kernel, luôn sử dụng phiên bản kernel; phiên bản Android không quan trọng bằng.\n\n## Tôi là GKI 1.0, tôi có thể sử dụng điều này không?\n\nGKI 1.0 hoàn toàn khác với GKI 2.0, bạn phải tự biên dịch kernel.\n\n## Làm cách nào để làm cho `/system` RW?\n\nChúng tôi không khuyến nghị bạn sửa đổi trực tiếp phân vùng hệ thống. Vui lòng kiểm tra [Hướng dẫn Module](module.md) để sửa đổi nó một cách systemless. Nếu bạn khăng khăng làm điều này, hãy kiểm tra [magisk_overlayfs](https://github.com/HuskyDG/magic_overlayfs).\n\n## KernelSU có thể sửa đổi hosts không? Làm cách nào để sử dụng AdAway?\n\nTất nhiên. Nhưng KernelSU không có hỗ trợ hosts tích hợp sẵn, bạn có thể cài đặt module như [systemless-hosts](https://github.com/symbuzzer/systemless-hosts-KernelSU-module) để thực hiện.\n\n## Tại sao các module của tôi không hoạt động sau khi cài đặt mới?\n\nNếu các module của bạn cần sửa đổi các tệp `/system`, bạn cần cài đặt [metamodule](metamodule.md) để mount thư mục `system`. Các tính năng module khác (scripts, sepolicy, system.prop) hoạt động mà không cần metamodule.\n\n**Giải pháp**: Xem [Hướng dẫn Metamodule](metamodule.md) để biết hướng dẫn cài đặt.\n\n## Metamodule là gì và tại sao tôi cần nó?\n\nMetamodule là một module đặc biệt cung cấp cơ sở hạ tầng để mount các module thông thường. Xem [Hướng dẫn Metamodule](metamodule.md) để biết giải thích đầy đủ.\n"
  },
  {
    "path": "website/docs/vi_VN/guide/hidden-features.md",
    "content": "# Tính Năng Ẩn\n\n## .ksurc\n\nTheo mặc định, `/system/bin/sh` tải `/system/etc/mkshrc`.\n\nBạn có thể tạo su tải tệp rc tùy chỉnh bằng cách tạo tệp `/data/adb/ksu/.ksurc`.\n"
  },
  {
    "path": "website/docs/vi_VN/guide/how-to-build.md",
    "content": "# Làm thế nào để xây dựng KernelSU?\n\n::: warning\nTài liệu này chỉ để tham khảo lưu trữ và không còn được duy trì.\nKể từ KernelSU v3.0, chúng tôi đã ngừng hỗ trợ chính thức cho chế độ ảnh GKI để có tốc độ lặp và build nhanh hơn. Khuyến nghị sử dụng `Ylarod/ddk` để build LKM.\n:::\n\nTrước tiên, bạn nên đọc tài liệu chính thức của Android để xây dựng kernel:\n\n1. [Building Kernels](https://source.android.com/docs/setup/build/building-kernels?hl=vi)\n2. [GKI Release Builds](https://source.android.com/docs/core/architecture/kernel/gki-release-builds?hl=vi)\n\n::: warning\nTrang này dành cho thiết bị GKI, nếu bạn sử dụng kernel cũ, vui lòng tham khảo [cách tích hợp KernelSU cho kernel cũ](how-to-integrate-for-non-gki)\n:::\n\n## Build Kernel\n\n### Đồng bộ hóa mã nguồn kernel\n\n```sh\nrepo init -u https://android.googlesource.com/kernel/manifest\nmv <kernel_manifest.xml> .repo/manifests\nrepo init -m manifest.xml\nrepo sync\n```\n\n`<kernel_manifest.xml>` là một tệp kê khai có thể xác định duy nhất một bản dựng, bạn có thể sử dụng tệp kê khai đó để thực hiện một bản dựng có thể dự đoán lại. Bạn nên tải xuống tệp kê khai từ [Google GKI release builds](https://source.android.com/docs/core/architecture/kernel/gki-release-builds?hl=vi)\n\n### Build\n\nTrước tiên, vui lòng kiểm tra [tài liệu chính thức](https://source.android.com/docs/setup/build/building-kernels?hl=vi).\n\nVí dụ: chúng ta cần xây dựng kernel image aarch64:\n\n```sh\nLTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh\n```\n\nĐừng quên thêm cờ `LTO=thin`, nếu không quá trình xây dựng có thể thất bại nếu bộ nhớ máy tính của bạn nhỏ hơn 24Gb.\n\nBắt đầu từ Android 13, kernel được xây dựng bởi `bazel`:\n\n```sh\ntools/bazel build --config=fast //common:kernel_aarch64_dist\n```\n\n## Build Kernel với KernelSU\n\nNếu bạn có thể build kernel thành công thì việc xây dựng KernelSU thật dễ dàng, Chọn bất kỳ một lần chạy trong thư mục gốc nguồn Kernel:\n\n- Thẻ mới nhất (ổn định)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\n- nhánh chính (dev)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\n```\n\n- Chọn thẻ (chẳng hạn như v0.5.2)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\n```\n\nVà sau đó build lại kernel và bạn sẽ có được image kernel với KernelSU!\n"
  },
  {
    "path": "website/docs/vi_VN/guide/how-to-integrate-for-non-gki.md",
    "content": "# Làm thế nào để tích hợp KernelSU vào thiết bị không sử dụng GKI ?\n\n::: warning\nTài liệu này chỉ để tham khảo lưu trữ và không còn được duy trì.\nKể từ KernelSU v1.0, chúng tôi đã ngừng hỗ trợ chính thức cho các thiết bị không phải GKI.\n:::\n\nKernelSU có thể được tích hợp vào kernel không phải GKI và hiện tại nó đã được backport xuống 4.14, thậm chí cũng có thể chạy trên kernel thấp hơn 4.14.\n\nDo các kernel không phải GKI bị phân mảnh nên chúng tôi không có cách build thống nhất, vì vậy chúng tôi không thể cung cấp các boot image không phải GKI. Nhưng bạn có thể tự build kernel với KernelSU được tích hợp vào.\n\nĐầu tiên, bạn phải build được kernel từ nguồn có khả năng boot được , nếu kernel không có mã nguồn mở thì rất khó để chạy KernelSU cho thiết bị của bạn.\n\nNếu bạn có thể build kernel khởi động được, có hai cách để tích hợp KernelSU vào mã nguồn kernel:\n\n1. Tự động với `kprobe`\n2. Thủ công\n\n\n## Tích hợp vào kprobe\n\nKernelSU sử dụng kprobe để thực hiện hook kernel, nếu *kprobe* chạy tốt trong kernel của bạn thì nên sử dụng cách này.\n\nĐầu tiên, thêm KernelSU vào mã nguồn kernel của bạn:\n\n- Thẻ mới nhất (ổn định)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\n- Nhánh chính (dev)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\n```\n\n- Chọn thẻ (chẳng hạn như v0.5.2)\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\n```\n\nSau đó, bạn nên kiểm tra xem *kprobe* có được bật trong config của bạn hay không, nếu không, vui lòng thêm các cấu hình sau vào:\n\n```\nCONFIG_KPROBES=y\nCONFIG_HAVE_KPROBES=y\nCONFIG_KPROBE_EVENTS=y\n```\n\nRồi build lại kernel của bạn, KernelSU sẽ hoạt động ok.\n\nTrong trường hợp kprobe chưa được bật, bạn có thể thêm `CONFIG_MODULES=y` vào kernel config. (Nếu vẫn không có hiệu lực thì hãy sử dụng `make menuconfig` rồi tìm các thành phần khác mà kprobe phụ thuộc).\n\nNhưng nếu bạn gặp bootloop khi tích hợp KernelSU thì có khả năng ***kprobe bị hỏng trong kernel***, bạn nên fix lỗi kprobe trong mã nguồn hoặc dùng cách 2.\n\n## Chỉnh sửa mã nguồn kernel thủ công\n\nNếu kprobe không thể hoạt động trong kernel của bạn (có thể là lỗi do upstream hoặc kernel dưới bản 4.8), thì bạn có thể thử cách này:\n\nĐầu tiên, thêm KernelSU vào mã nguồn kernel của bạn:\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\nSau đó, thêm lệnh gọi KernelSU vào mã nguồn kernel, đây là một patch bạn có thể tham khảo:\n\n```diff\ndiff --git a/fs/exec.c b/fs/exec.c\nindex ac59664eaecf..bdd585e1d2cc 100644\n--- a/fs/exec.c\n+++ b/fs/exec.c\n@@ -1890,11 +1890,14 @@ static int __do_execve_file(int fd, struct filename *filename,\n \treturn retval;\n }\n \n+extern bool ksu_execveat_hook __read_mostly;\n+extern int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv,\n+\t\t\tvoid *envp, int *flags);\n+extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,\n+\t\t\t\t void *argv, void *envp, int *flags);\n static int do_execveat_common(int fd, struct filename *filename,\n \t\t\t      struct user_arg_ptr argv,\n \t\t\t      struct user_arg_ptr envp,\n \t\t\t      int flags)\n {\n+\tif (unlikely(ksu_execveat_hook))\n+\t\tksu_handle_execveat(&fd, &filename, &argv, &envp, &flags);\n+\telse\n+\t\tksu_handle_execveat_sucompat(&fd, &filename, &argv, &envp, &flags);\n \treturn __do_execve_file(fd, filename, argv, envp, flags, NULL);\n }\n```\n```diff\ndiff --git a/fs/open.c b/fs/open.c\nindex 05036d819197..965b84d486b8 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -348,6 +348,8 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn ksys_fallocate(fd, mode, offset, len);\n }\n \n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t int *flags);\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -355,6 +357,7 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n  */\n long do_faccessat(int dfd, const char __user *filename, int mode)\n {\n \tconst struct cred *old_cred;\n \tstruct cred *override_cred;\n \tstruct path path;\n \tstruct inode *inode;\n \tstruct vfsmount *mnt;\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n \n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n \n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n```diff\ndiff --git a/fs/read_write.c b/fs/read_write.c\nindex 650fc7e0f3a6..55be193913b6 100644\n--- a/fs/read_write.c\n+++ b/fs/read_write.c\n@@ -434,10 +434,14 @@ ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)\n }\n EXPORT_SYMBOL(kernel_read);\n \n+extern bool ksu_vfs_read_hook __read_mostly;\n+extern int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,\n+\t\t\tsize_t *count_ptr, loff_t **pos);\n ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)\n {\n \tssize_t ret;\n \n+\tif (unlikely(ksu_vfs_read_hook))\n+\t\tksu_handle_vfs_read(&file, &buf, &count, &pos);\n+\n \tif (!(file->f_mode & FMODE_READ))\n \t\treturn -EBADF;\n \tif (!(file->f_mode & FMODE_CAN_READ))\n```\n```diff\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 376543199b5a..82adcef03ecc 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -148,6 +148,8 @@ int vfs_statx_fd(unsigned int fd, struct kstat *stat,\n }\n EXPORT_SYMBOL(vfs_statx_fd);\n \n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+\n /**\n  * vfs_statx - Get basic and extra attributes by filename\n  * @dfd: A file descriptor representing the base dir for a relative filename\n@@ -170,6 +172,7 @@ int vfs_statx(int dfd, const char __user *filename, int flags,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_AUTOMOUNT;\n \n+\tksu_handle_stat(&dfd, &filename, &flags);\n \tif ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t       AT_EMPTY_PATH | KSTAT_QUERY_FLAGS)) != 0)\n \t\treturn -EINVAL;\n```\n\nBạn sẽ tìm thấy bốn chức năng trong mã nguồn kernel:\n\n1. do_faccessat, thường là trong `fs/open.c`\n2. do_execveat_common, thường nằm trong `fs/exec.c`\n3. vfs_read, thường nằm trong `fs/read_write.c`\n4. vfs_statx, thường có trong `fs/stat.c`\n\nCuối cùng, chỉnh sửa `KernelSU/kernel/ksu.c` và bỏ `enable_sucompat()` sau đó xây dựng lại kernel của bạn, KernelSU sẽ hoạt động tốt.\n\n### How to backport path_umount\n\nYou can make the \"Umount modules\" feature work on pre-GKI kernels by manually backporting `path_umount` from 5.9. You can use this patch as reference:\n\n```diff\n--- a/fs/namespace.c\n+++ b/fs/namespace.c\n@@ -1739,6 +1739,39 @@ static inline bool may_mandlock(void)\n }\n #endif\n\n+static int can_umount(const struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\n+\tif (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW))\n+\t\treturn -EINVAL;\n+\tif (!may_mount())\n+\t\treturn -EPERM;\n+\tif (path->dentry != path->mnt->mnt_root)\n+\t\treturn -EINVAL;\n+\tif (!check_mnt(mnt))\n+\t\treturn -EINVAL;\n+\tif (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */\n+\t\treturn -EINVAL;\n+\tif (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN))\n+\t\treturn -EPERM;\n+\treturn 0;\n+}\n+\n+int path_umount(struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\tint ret;\n+\n+\tret = can_umount(path, flags);\n+\tif (!ret)\n+\t\tret = do_umount(mnt, flags);\n+\n+\t/* we mustn't call path_put() as that would clear mnt_expiry_mark */\n+\tdput(path->dentry);\n+\tmntput_no_expire(mnt);\n+\treturn ret;\n+}\n /*\n  * Now umount can handle mount points as well as block devices.\n  * This is important for filesystems which use unnamed block devices.\n```\n\nFinally, build your kernel again, and KernelSU should work correctly.\n"
  },
  {
    "path": "website/docs/vi_VN/guide/installation.md",
    "content": "# Cách cài đặt\n\n## Kiểm tra xem thiết bị của bạn có được hỗ trợ không\n\nTải xuống APP KernelSU manager từ [GitHub Releases](https://github.com/tiann/KernelSU/releases) và cài đặt nó vào thiết bị của bạn:\n\n- Nếu ứng dụng hiển thị `Unsupported`, nghĩa là **Bạn nên tự biên dịch kernel**, KernelSU sẽ không và không bao giờ cung cấp boot image để bạn flash.\n- Nếu ứng dụng hiển thị `Not installed` thì thiết bị của bạn đã được KernelSU hỗ trợ chính thức.\n\n:::info\nĐối với các thiết bị hiển thị `Unsupported`, đây là [Thiết-bị-hỗ-trợ-không-chính-thức](unofficially-support-devices.md), bạn có thể tự biên dịch kernel.\n:::\n\n## Sao lưu stock boot.img\n\nTrước khi flash, trước tiên bạn phải sao lưu stock boot.img. Nếu bạn gặp phải bootloop (vòng lặp khởi động), bạn luôn có thể khôi phục hệ thống bằng cách quay lại trạng thái khởi động ban đầu bằng fastboot.\n\n::: warning\nViệc flash có thể gây mất dữ liệu, hãy đảm bảo thực hiện tốt bước này trước khi chuyển sang bước tiếp theo!! Bạn cũng có thể sao lưu tất cả dữ liệu trên điện thoại nếu cần.\n:::\n\n## Kiến thức cần thiết\n\n### ADB và fastboot\n\nTheo mặc định, bạn sẽ sử dụng các công cụ ADB và fastboot trong hướng dẫn này, vì vậy nếu bạn không biết về chúng, chúng tôi khuyên bạn nên sử dụng công cụ tìm kiếm để tìm hiểu về chúng trước tiên.\n\n### KMI\n\nKernel Module Interface (KMI), các phiên bản kernel có cùng KMI đều **tương thích** Đây là ý nghĩa của \"general\" trong GKI; ngược lại, nếu KMI khác thì các kernel này không tương thích với nhau và việc flash kernel image có KMI khác với thiết bị của bạn có thể gây ra bootloop.\n\nCụ thể, đối với thiết bị GKI, định dạng phiên bản kernel phải như sau:\n\n```txt\nKernelRelease :=\nVersion.PatchLevel.SubLevel-AndroidRelease-KmiGeneration-suffix\nw      .x         .y       -zzz           -k            -something\n```\n\n`w.x-zzz-k` là phiên bản KMI. Ví dụ: nếu phiên bản kernel của thiết bị là `5.10.101-android12-9-g30979850fc20`, thì KMI của nó là `5.10-android12-9`; về mặt lý thuyết, nó có thể khởi động bình thường với các kernel KMI khác.\n\n::: tip\nLưu ý rằng SubLevel trong phiên bản kernel không phải là một phần của KMI! Điều đó có nghĩa là `5.10.101-android12-9-g30979850fc20` có cùng KMI với `5.10.137-android12-9-g30979850fc20`!\n:::\n\n### Phiên bản kernel vs Phiên bản Android\n\nXin lưu ý: **Phiên bản kernel và phiên bản Android không nhất thiết phải giống nhau!**\n\nNếu bạn nhận thấy phiên bản kernel của mình là `android12-5.10.101` nhưng phiên bản hệ thống Android của bạn là Android 13 hoặc phiên bản khác; xin đừng ngạc nhiên, vì số phiên bản của hệ thống Android không nhất thiết phải giống với số phiên bản của kernel Linux; Số phiên bản của kernel Linux nhìn chung nhất quán với phiên bản của hệ thống Android đi kèm với **thiết bị khi nó được xuất xưởng**. Nếu hệ thống Android được nâng cấp sau này, phiên bản kernel thường sẽ không thay đổi. Nếu bạn cần flash, **vui lòng tham khảo phiên bản kernel!!**\n\n## Giới thiệu\n\nCó một số phương pháp cài đặt KernelSU, mỗi phương pháp phù hợp với một kịch bản khác nhau, vì vậy vui lòng chọn khi cần.\n\n1. Cài đặt với Recovery tùy chỉnh (ví dụ TWRP)\n2. Cài đặt bằng ứng dụng flash kernel, chẳng hạn như Franco Kernel Manager\n3. Cài đặt thông qua fastboot bằng boot.img do KernelSU cung cấp\n4. Sửa boot.img theo cách thủ công và cài đặt nó\n\nSince version [0.9.0](https://github.com/tiann/KernelSU/releases/tag/v0.9.0), KernelSU supports two running modes on GKI devices:\n\n1. `GKI`: Replace the original kernel of the device with the **Generic Kernel Image** (GKI) provided by KernelSU.\n2. `LKM`: Load the **Loadable Kernel Module** (LKM) into the device kernel without replacing the original kernel.\n\nThese two modes are suitable for different scenarios, and you can choose the one according to your needs.\n\n### GKI mode {#gki-mode}\n\nIn GKI mode, the original kernel of the device will be replaced with the generic kernel image provided by KernelSU. The advantages of GKI mode are:\n\n1. Strong universality, suitable for most devices. For example, Samsung has enabled KNOX devices, and LKM mode cannot work. There are also some niche modified devices that can only use GKI mode.\n2. Can be used without relying on official firmware, and there is no need to wait for official firmware updates, as long as the KMI is consistent, it can be used.\n\n### LKM mode {#lkm-mode}\n\nIn LKM mode, the original kernel of the device won't be replaced, but the loadable kernel module will be loaded into the device kernel. The advantages of LKM mode are:\n\n1. Won't replace the original kernel of the device. If you have special requirements for the original kernel of the device, or you want to use KernelSU while using a third-party kernel, you can use LKM mode.\n2. It's more convenient to upgrade and OTA. When upgrading KernelSU, you can directly install it in the manager without flashing manually. After the system OTA, you can directly install it to the second slot without manual flashing.\n3. Suitable for some special scenarios. For example, LKM can also be loaded with temporary root permissions. Since it doesn't need to replace the boot partition, it won't trigger AVB and won't cause the device to be bricked.\n4. LKM can be temporarily uninstalled. If you want to temporarily disable root access, you can uninstall LKM. This process doesn't require flashing partitions, or even rebooting the device. If you want to enable root again, just reboot the device.\n\n::: tip COEXISTENCE OF TWO MODES\nAfter opening the manager, you can see the current mode of the device on the homepage. Note that the priority of GKI mode is higher than that of LKM. For example, if you use GKI kernel to replace the original kernel, and use LKM to patch the GKI kernel, the LKM will be ignored, and the device will always run in GKI mode.\n:::\n\n### Which one to choose? {#which-one}\n\nIf your device is a mobile phone, we recommend that you prioritize LKM mode. If your device is an emulator, WSA, or Waydroid, we recommend that you prioritize GKI mode.\n\n## LKM installation\n\n### Get the official firmware\n\nTo use LKM mode, you need to get the official firmware and patch it based on the official firmware. If you use a third-party kernel, you can use the `boot.img` of the third-party kernel as the official firmware.\n\nThere are many ways to get the official firmware. If your device supports `fastboot boot`, we recommend **the most recommended and simplest** method is to use `fastboot boot` to temporarily boot the GKI kernel provided by KernelSU, then install the manager, and finally install it directly in the manager. This method doesn't require manually downloading the official firmware or manually extracting the boot.\n\nIf your device doesn't support `fastboot boot`, you may need to manually download the official firmware package and extract the boot from it.\n\nUnlike GKI mode, LKM mode modifies the `ramdisk`. Therefore, on devices with Android 13, it needs to patch the `init_boot` partition instead of the `boot` partition, while GKI mode always operates on the `boot` partition.\n\n### Use the manager\n\nOpen the manager, click the installation icon in the upper right corner, and several options will appear:\n\n1. Select a file. If your device doesn't have root privileges, you can choose this option and then select your official firmware. The manager will automatically patch it. After that, just flash this patched file to obtain root privileges permanently.\n2. Direct install. If your device is already rooted, you can choose this option. The manager will automatically get your device information, and then automatically patch the official firmware, and flash it automatically. You can consider using `fastboot boot` KernelSU's GKI kernel to get temporary root and install the manager, and then use this option. This is also the main way to upgrade KernelSU.\n3. Install to inactive slot. If your device supports A/B partition, you can choose this option. The manager will automatically patch the official firmware and install it to another partition. This method is suitable for devices after OTA, you can directly install it to the inactive slot after OTA.\n\nIf you don't want to use the manager, you can also use the command line to install LKM. The `ksud` tool provided by KernelSU can help you patch the official firmware quickly and then flash it.\n\nThe usage of `ksud` is as follows:\n\n```sh\nksud boot-patch \n```\n\n```txt\nUsage: ksud boot-patch [OPTIONS]\n\nOptions:\n  -b, --boot <BOOT>              Boot image be patched\n  -l, --lkm <LKM>                LKM module path. If not specified, the built-in module will be used\n  -m, --module <MODULE>          LKM module path to be replaced. If not specified, the built-in module will be used\n  -i, --init <INIT>              init to be replaced\n  -u, --ota                      Will use another slot if the boot image is not specified\n  -f, --flash                    Flash it to boot partition after patch\n  -o, --out <OUT>                Output path. If not specified, the current directory will be used\n      --magiskboot <MAGISKBOOT>  magiskboot path. If not specified, the built-in version will be used\n      --kmi <KMI>                KMI version. If specified, the indicated KMI will be used\n  -h, --help                     Print help\n```\n\nA few options that need to be explained:\n\n1. The `--magiskboot` option can specify the path of magiskboot. If not specified, ksud will look for it in the environment variables. If you don’t know how to get magiskboot, you can check [here](#patch-boot-image).\n2. The `--kmi` option can specify the `KMI` version. If the kernel name of your device doesn't follow the KMI specification, you can specify it using this option.\n\nThe most common usage is:\n\n```sh\nksud boot-patch -b <boot.img> --kmi android13-5.10\n```\n\n## LKM mode installation\n\nThere are several installation methods for LKM mode, each suitable for a different scenario, so please choose accordingly:\n\n1. Install with fastboot using the boot.img provided by KernelSU.\n2. Install with a kernel flash app, such as [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases).\n3. Repair the boot.img manually and install it.\n4. Install with custom Recovery (e.g., TWRP).\n\n## Cài đặt với Recovery tùy chỉnh\n\nĐiều kiện chắc chắn: Thiết bị của bạn phải có Recovery tùy chỉnh, chẳng hạn như TWRP; nếu không hoặc chỉ có Recovery chính thức, hãy sử dụng phương pháp khác.\n\nCác bước:\n\n1. Từ [Release page](https://github.com/tiann/KernelSU/releases) của KernelSU, tải xuống gói zip bắt đầu bằng AnyKernel3 phù hợp với phiên bản điện thoại của bạn; ví dụ: phiên bản kernel của điện thoại là `android12-5.10. 66`, thì bạn nên tải xuống tệp `AnyKernel3-android12-5.10.66_yyyy-MM.zip` (trong đó `yyyy` là năm và `MM` là tháng).\n2. Khởi động lại điện thoại vào TWRP.\n3. Sử dụng adb để đặt AnyKernel3-*.zip vào điện thoại /sdcard và chọn cài đặt nó trong GUI TWRP; hoặc bạn có thể trực tiếp `adb sideload AnyKernel-*.zip` để cài đặt.\n\nPS. Phương pháp này phù hợp với mọi cài đặt (không giới hạn cài đặt ban đầu hoặc các nâng cấp tiếp theo), miễn là bạn sử dụng TWRP.\n\n## Cài đặt bằng Kernel Flasher\n\nĐiều kiện chắc chắn: Thiết bị của bạn phải được root. Ví dụ: bạn đã cài đặt Magisk để root hoặc bạn đã cài đặt phiên bản KernelSU cũ và cần nâng cấp lên phiên bản KernelSU khác; nếu thiết bị của bạn chưa được root, vui lòng thử các phương pháp khác.\n\nCác bước:\n\n1. Tải xuống zip AnyKernel3; hãy tham khảo phần *Cài đặt bằng Custom Recovery* để biết hướng dẫn tải xuống.\n2. Mở Ứng dụng Kernel Flash và sử dụng zip AnyKernel3 được cung cấp để flash.\n\nNếu trước đây bạn chưa từng sử dụng Ứng dụng Kernel flash thì sau đây là những ứng dụng phổ biến hơn.\n\n1. [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases)\n2. [Franco Kernel Manager](https://play.google.com/store/apps/details?id=com.franco.kernel)\n3. [Ex Kernel Manager](https://play.google.com/store/apps/details?id=flar2.exkernelmanager)\n\nPS. Phương pháp này thuận tiện hơn khi nâng cấp KernelSU và có thể thực hiện mà không cần máy tính (sao lưu trước!). .\n\nCác bước:\n\n## Cài đặt bằng boot.img do KernelSU cung cấp\n\nPhương pháp này không yêu cầu bạn phải có TWRP, cũng như không yêu cầu điện thoại của bạn phải có quyền root; nó phù hợp cho lần cài đặt KernelSU đầu tiên của bạn.\n\n### Tìm boot.img thích hợp\n\nKernelSU cung cấp boot.img chung cho các thiết bị GKI và bạn nên chuyển boot.img vào phân vùng boot của thiết bị.\n\nBạn có thể tải xuống boot.img từ [GitHub Release](https://github.com/tiann/KernelSU/releases), xin lưu ý rằng bạn nên sử dụng đúng phiên bản boot.img. Ví dụ: nếu thiết bị của bạn hiển thị kernel `android12-5.10.101` , bạn cần tải xuống `android-5.10.101_yyyy-MM.boot-<format>.img`. (Giữ KMI nhất quán!)\n\nTrong đó `<format>` đề cập đến định dạng nén kernel của boot.img chính thức của bạn, vui lòng kiểm tra định dạng nén kernel của boot.img ban đầu của bạn, bạn nên sử dụng đúng định dạng, ví dụ: `lz4`, `gz`; nếu bạn sử dụng định dạng nén không chính xác, bạn có thể gặp phải bootloop.\n\n::: info\n1. Bạn có thể sử dụng magiskboot để lấy định dạng nén của boot ban đầu; Tất nhiên, bạn cũng có thể hỏi những người khác, có kinh nghiệm hơn có cùng kiểu máy với thiết bị của bạn. Ngoài ra, định dạng nén của kernel thường không thay đổi nên nếu bạn khởi động thành công với một định dạng nén nào đó thì bạn có thể thử định dạng đó sau.\n2. Các thiết bị Xiaomi thường sử dụng `gz` hoặc **uncompressed** (không nén).\n3. Đối với thiết bị Pixel, hãy làm theo hướng dẫn bên dưới.\n:::\n\n### flash boot.img vào thiết bị\n\nSử dụng `adb` để kết nối thiết bị của bạn, sau đó thực thi `adb restart bootloader` để vào chế độ fastboot, sau đó sử dụng lệnh này để flash KernelSU:\n\n```sh\nfastboot flash boot boot.img\n```\n\n::: info\nNếu thiết bị của bạn hỗ trợ `fastboot boot`, trước tiên bạn có thể sử dụng `fastboot boot boot.img` để thử sử dụng boot.img để khởi động hệ thống trước. Nếu có điều gì bất ngờ xảy ra, hãy khởi động lại để boot.\n:::\n\n### khởi động lại\n\nSau khi flash xong bạn nên khởi động lại máy:\n\n```sh\nfastboot reboot\n```\n\n## Vá boot.img theo cách thủ công\n\nĐối với một số thiết bị, định dạng boot.img không quá phổ biến, chẳng hạn như không `lz4`, `gz` và không nén; điển hình nhất là Pixel, định dạng boot.img của nó là nén `lz4_legacy`, ramdisk có thể là `gz` cũng có thể là nén `lz4_legacy`; tại thời điểm này, nếu bạn trực tiếp flash boot.img do KernelSU cung cấp, điện thoại có thể không khởi động được; Tại thời điểm này, bạn có thể vá boot.img theo cách thủ công để dùng được.\n\nNhìn chung có hai phương pháp vá:\n\n1. [Android-Image-Kitchen](https://forum.xda-developers.com/t/tool-android-image-kitchen-unpack-repack-kernel-ramdisk-win-android-linux-mac.2073775/)\n2. [magiskboot](https://github.com/topjohnwu/Magisk/releases)\n\nTrong số đó, Android-Image-Kitchen phù hợp để hoạt động trên PC và magiskboot cần sự kết nối của điện thoại di động.\n\n### Chuẩn bị\n\n1. Lấy stock boot.img của điện thoại; bạn có thể lấy nó từ nhà sản xuất thiết bị của mình, bạn có thể cần [payload-dumper-go](https://github.com/ssut/payload-dumper-go)\n2. Tải xuống tệp zip AnyKernel3 do KernelSU cung cấp phù hợp với phiên bản KMI của thiết bị của bạn (bạn có thể tham khảo *Cài đặt với Khôi phục tùy chỉnh*).\n3. Giải nén gói AnyKernel3 và lấy tệp `Image`, đây là tệp kernel của KernelSU.\n\n### Sử dụng Android-Image-Kitchen\n\n1. Tải Android-Image-Kitchen về máy tính.\n2. Đặt stock boot.img vào thư mục gốc của Android-Image-Kitchen.\n3. Thực thi `./unpackimg.sh boot.img` tại thư mục gốc của Android-Image-Kitchen, lệnh này sẽ giải nén boot.img và bạn sẽ nhận được một số tệp.\n4. Thay thế `boot.img-kernel` trong thư mục `split_img` bằng `Image` bạn đã trích xuất từ AnyKernel3 (lưu ý đổi tên thành boot.img-kernel).\n5. Thực thi `./repackimg.sh` tại thư mục gốc của 在 Android-Image-Kitchen; Và bạn sẽ nhận được một file có tên `image-new.img`; Flash boot.img này bằng fastboot(Tham khảo phần trước).\n\n### Sử dụng magiskboot\n\n1. Tải xuống Magisk mới nhất từ [Trang phát hành](https://github.com/topjohnwu/Magisk/releases)\n2. Đổi tên `Magisk-*(version).apk` thành `Magisk-*.zip` và giải nén nó.\n3. Đẩy `Magisk-*/lib/arm64-v8a/libmagiskboot.so` vào thiết bị của bạn bằng adb: `adb push Magisk-*/lib/arm64-v8a/libmagiskboot.so /data/local/tmp /magiskboot`\n4. Đẩy stock boot.img và Image trong AnyKernel3 vào thiết bị của bạn.\n5. Nhập thư mục adb shell và cd `/data/local/tmp/`, sau đó `chmod +x magiskboot`\n6. Nhập adb shell và cd `/data/local/tmp/`, thực thi `./magiskboot unpack boot.img` để giải nén `boot.img`, bạn sẽ nhận được file `kernel`, đây là kernel gốc của bạn.\n7. Thay thế `kernel` bằng `Image`: `mv -f Image kernel`\n8. Thực thi `./magiskboot repack boot.img` để đóng gói lại boot img và bạn sẽ nhận được một tệp `new-boot.img`, flash tệp này vào thiết bị bằng fastboot.\n\n## Các phương pháp khác\n\nTrên thực tế, tất cả các phương pháp cài đặt này chỉ có một ý tưởng chính, đó là **thay thế kernel gốc bằng kernel do KernelSU cung cấp**; chỉ cần đạt được điều này là có thể cài đặt được; ví dụ, sau đây là các phương pháp có thể khác.\n\n1. Trước tiên hãy cài đặt Magisk, nhận quyền root thông qua Magisk, sau đó sử dụng flasher kernel để flash trong zip AnyKernel từ KernelSU.\n2. Sử dụng một số bộ công cụ flash trên PC để flash trong kernel do KernelSU cung cấp.\n\nTuy nhiên, nếu nó không hoạt động, vui lòng thử phương pháp `magiskboot`.\n\n## Sau khi cài đặt: Hỗ trợ Module\n\n::: warning METAMODULE CHO SỬA ĐỔI TỆP HỆ THỐNG\nNếu bạn muốn sử dụng các module sửa đổi tệp `/system`, bạn cần cài đặt **metamodule** sau khi cài đặt KernelSU. Các module chỉ sử dụng scripts, sepolicy hoặc system.prop hoạt động mà không cần metamodule.\n:::\n\n**Để hỗ trợ sửa đổi `/system`**, vui lòng xem [Hướng dẫn Metamodule](metamodule.md) để:\n- Hiểu metamodule là gì và tại sao cần chúng\n- Cài đặt metamodule `meta-overlayfs` chính thức\n- Tìm hiểu về các tùy chọn metamodule khác\n"
  },
  {
    "path": "website/docs/vi_VN/guide/metamodule.md",
    "content": "# Metamodule\n\nMetamodule là một tính năng đột phá trong KernelSU cho phép chuyển các khả năng quan trọng của hệ thống module từ lõi sang các module có thể cắm thêm. Sự thay đổi kiến trúc này duy trì tính ổn định và bảo mật của KernelSU đồng thời giải phóng tiềm năng đổi mới lớn hơn cho hệ sinh thái module.\n\n## Metamodule là gì?\n\nMetamodule là một loại module đặc biệt của KernelSU cung cấp chức năng cơ sở hạ tầng cốt lõi cho hệ thống module. Không giống như các module thông thường sửa đổi tệp hệ thống, metamodule kiểm soát *cách thức* các module thông thường được cài đặt và mount.\n\nMetamodule là cơ chế mở rộng dựa trên plugin cho phép tùy chỉnh hoàn toàn cơ sở hạ tầng quản lý module của KernelSU. Bằng cách ủy thác logic mount và cài đặt cho metamodule, KernelSU tránh trở thành điểm phát hiện dễ vỡ trong khi cho phép các chiến lược triển khai đa dạng.\n\n**Đặc điểm chính:**\n\n- **Vai trò cơ sở hạ tầng**: Metamodule cung cấp các dịch vụ mà các module thông thường phụ thuộc vào\n- **Chỉ một instance**: Chỉ có thể cài đặt một metamodule tại một thời điểm\n- **Thực thi ưu tiên**: Các script của metamodule chạy trước các script của module thông thường\n- **Hook đặc biệt**: Cung cấp ba script hook cho cài đặt, mount và dọn dẹp\n\n## Tại sao cần Metamodule?\n\nCác giải pháp root truyền thống tích hợp logic mount vào lõi của chúng, khiến chúng dễ bị phát hiện và khó phát triển hơn. Kiến trúc metamodule của KernelSU giải quyết những vấn đề này thông qua việc tách biệt các mối quan tâm.\n\n**Lợi thế chiến lược:**\n\n- **Giảm bề mặt phát hiện**: Bản thân KernelSU không thực hiện mount, giảm các vector phát hiện\n- **Tính ổn định**: Lõi vẫn ổn định trong khi các triển khai mount có thể phát triển\n- **Đổi mới**: Cộng đồng có thể phát triển các chiến lược mount thay thế mà không cần fork KernelSU\n- **Lựa chọn**: Người dùng có thể chọn triển khai phù hợp nhất với nhu cầu của họ\n\n**Tính linh hoạt của mount:**\n\n- **Không mount**: Đối với người dùng chỉ sử dụng module không cần mount, tránh hoàn toàn chi phí mount\n- **Mount OverlayFS**: Cách tiếp cận truyền thống với hỗ trợ lớp đọc-ghi (thông qua `meta-overlayfs`)\n- **Magic mount**: Mount tương thích với Magisk để có khả năng tương thích ứng dụng tốt hơn\n- **Triển khai tùy chỉnh**: Overlay dựa trên FUSE, mount VFS tùy chỉnh hoặc các phương pháp hoàn toàn mới\n\n**Vượt xa mount:**\n\n- **Khả năng mở rộng**: Thêm các tính năng như hỗ trợ kernel module mà không cần sửa đổi lõi KernelSU\n- **Tính module hóa**: Cập nhật các triển khai độc lập với các bản phát hành KernelSU\n- **Tùy chỉnh**: Tạo các giải pháp chuyên biệt cho các thiết bị hoặc trường hợp sử dụng cụ thể\n\n::: warning QUAN TRỌNG\nNếu không cài đặt metamodule, các module sẽ **KHÔNG** được mount. Các cài đặt KernelSU mới yêu cầu cài đặt một metamodule (như `meta-overlayfs`) để các module hoạt động.\n:::\n\n## Dành cho Người dùng\n\n### Cài đặt Metamodule\n\nCài đặt metamodule giống như cài đặt các module thông thường:\n\n1. Tải xuống tệp ZIP metamodule (ví dụ: `meta-overlayfs.zip`)\n2. Mở ứng dụng KernelSU Manager\n3. Nhấn nút hành động nổi (➕)\n4. Chọn tệp ZIP metamodule\n5. Khởi động lại thiết bị của bạn\n\nMetamodule `meta-overlayfs` là triển khai tham chiếu chính thức cung cấp mount module dựa trên overlayfs truyền thống với hỗ trợ ext4 image.\n\n### Kiểm tra Metamodule đang hoạt động\n\nBạn có thể kiểm tra metamodule nào đang hoạt động trong trang Module của ứng dụng KernelSU Manager. Metamodule đang hoạt động sẽ được hiển thị trong danh sách module của bạn với chỉ định đặc biệt.\n\n### Gỡ cài đặt Metamodule\n\n::: danger CẢNH BÁO\nGỡ cài đặt metamodule sẽ ảnh hưởng đến **TẤT CẢ** các module. Sau khi gỡ bỏ, các module sẽ không còn được mount cho đến khi bạn cài đặt một metamodule khác.\n:::\n\nĐể gỡ cài đặt:\n\n1. Mở KernelSU Manager\n2. Tìm metamodule trong danh sách module của bạn\n3. Nhấn gỡ cài đặt (bạn sẽ thấy cảnh báo đặc biệt)\n4. Xác nhận hành động\n5. Khởi động lại thiết bị của bạn\n\nSau khi gỡ cài đặt, bạn nên cài đặt một metamodule khác nếu muốn các module tiếp tục hoạt động.\n\n### Ràng buộc chỉ một Metamodule\n\nChỉ có thể cài đặt một metamodule tại một thời điểm. Nếu bạn cố gắng cài đặt metamodule thứ hai, KernelSU sẽ ngăn chặn việc cài đặt để tránh xung đột.\n\nĐể chuyển đổi metamodule:\n\n1. Gỡ cài đặt tất cả các module thông thường\n2. Gỡ cài đặt metamodule hiện tại\n3. Khởi động lại\n4. Cài đặt metamodule mới\n5. Cài đặt lại các module thông thường của bạn\n6. Khởi động lại một lần nữa\n\n## Dành cho Nhà phát triển Module\n\nNếu bạn đang phát triển các module KernelSU thông thường, bạn không cần lo lắng nhiều về metamodule. Các module của bạn sẽ hoạt động miễn là người dùng có cài đặt một metamodule tương thích (như `meta-overlayfs`).\n\n**Những điều bạn cần biết:**\n\n- **Mount yêu cầu metamodule**: Thư mục `system` trong module của bạn sẽ chỉ được mount nếu người dùng có cài đặt metamodule cung cấp chức năng mount\n- **Không cần thay đổi code**: Các module hiện có tiếp tục hoạt động mà không cần sửa đổi\n\n::: tip\nNếu bạn quen thuộc với phát triển module Magisk, các module của bạn sẽ hoạt động tương tự trong KernelSU khi cài đặt metamodule, vì nó cung cấp mount tương thích với Magisk.\n:::\n\n## Dành cho Nhà phát triển Metamodule\n\nTạo một metamodule cho phép bạn tùy chỉnh cách KernelSU xử lý cài đặt, mount và gỡ cài đặt module.\n\n### Yêu cầu Cơ bản\n\nMột metamodule được xác định bởi một thuộc tính đặc biệt trong `module.prop`:\n\n```txt\nid=my_metamodule\nname=My Custom Metamodule\nversion=1.0\nversionCode=1\nauthor=Your Name\ndescription=Custom module mounting implementation\nmetamodule=1\n```\n\nThuộc tính `metamodule=1` (hoặc `metamodule=true`) đánh dấu đây là một metamodule. Nếu không có thuộc tính này, module sẽ được coi là module thông thường.\n\n### Cấu trúc Tệp\n\nCấu trúc một metamodule:\n\n```txt\nmy_metamodule/\n├── module.prop              (phải bao gồm metamodule=1)\n│\n│      *** Hook đặc biệt của metamodule ***\n├── metamount.sh             (tùy chọn: xử lý mount tùy chỉnh)\n├── metainstall.sh           (tùy chọn: hook cài đặt cho module thông thường)\n├── metauninstall.sh         (tùy chọn: hook dọn dẹp cho module thông thường)\n│\n│      *** Tệp module tiêu chuẩn (tất cả đều tùy chọn) ***\n├── customize.sh             (tùy chỉnh cài đặt)\n├── post-fs-data.sh          (script giai đoạn post-fs-data)\n├── service.sh               (script dịch vụ late_start)\n├── boot-completed.sh        (script hoàn thành khởi động)\n├── uninstall.sh             (script gỡ cài đặt của chính metamodule)\n├── system/                  (sửa đổi systemless, nếu cần)\n└── [bất kỳ tệp bổ sung nào]\n```\n\nMetamodule có thể sử dụng tất cả các tính năng module tiêu chuẩn (script vòng đời, v.v.) ngoài các hook metamodule đặc biệt của chúng.\n\n### Script Hook\n\nMetamodule có thể cung cấp tối đa ba script hook đặc biệt:\n\n#### 1. metamount.sh - Xử lý Mount\n\n**Mục đích**: Kiểm soát cách các module được mount trong quá trình khởi động.\n\n**Khi thực thi**: Trong giai đoạn `post-fs-data`, trước khi bất kỳ script module nào chạy.\n\n**Biến môi trường:**\n\n- `MODDIR`: Đường dẫn thư mục của metamodule (ví dụ: `/data/adb/modules/my_metamodule`)\n- Tất cả các biến môi trường KernelSU tiêu chuẩn\n\n**Trách nhiệm:**\n\n- Mount tất cả các module đã kích hoạt một cách systemless\n- Kiểm tra cờ `skip_mount`\n- Xử lý các yêu cầu mount cụ thể của module\n\n::: danger YÊU CẦU QUAN TRỌNG\nKhi thực hiện các thao tác mount, bạn **PHẢI** đặt tên nguồn/thiết bị thành `\"KSU\"`. Điều này xác định các mount thuộc về KernelSU.\n\n**Ví dụ (đúng):**\n\n```sh\nmount -t overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work KSU /target\n```\n\n**Đối với API mount hiện đại**, đặt chuỗi nguồn:\n\n```rust\nfsconfig_set_string(fs, \"source\", \"KSU\")?;\n```\n\nĐiều này rất cần thiết để KernelSU xác định và quản lý các mount của nó đúng cách.\n:::\n\n**Script ví dụ:**\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\n\n# Ví dụ: Triển khai bind mount đơn giản\nfor module in /data/adb/modules/*; do\n    if [ -f \"$module/disable\" ] || [ -f \"$module/skip_mount\" ]; then\n        continue\n    fi\n\n    if [ -d \"$module/system\" ]; then\n        # Mount với source=KSU (BẮT BUỘC!)\n        mount -o bind,dev=KSU \"$module/system\" /system\n    fi\ndone\n```\n\n#### 2. metainstall.sh - Hook Cài đặt\n\n**Mục đích**: Tùy chỉnh cách các module thông thường được cài đặt.\n\n**Khi thực thi**: Trong quá trình cài đặt module, sau khi các tệp được giải nén nhưng trước khi cài đặt hoàn tất. Script này được **sourced** (không thực thi) bởi trình cài đặt tích hợp, tương tự như cách `customize.sh` hoạt động.\n\n**Biến môi trường và hàm:**\n\nScript này kế thừa tất cả các biến và hàm từ `install.sh` tích hợp:\n\n- **Biến**: `MODPATH`, `TMPDIR`, `ZIPFILE`, `ARCH`, `API`, `IS64BIT`, `KSU`, `KSU_VER`, `KSU_VER_CODE`, `BOOTMODE`, v.v.\n- **Hàm**:\n  - `ui_print <msg>` - In thông báo ra console\n  - `abort <msg>` - In lỗi và kết thúc cài đặt\n  - `set_perm <target> <owner> <group> <permission> [context]` - Đặt quyền tệp\n  - `set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]` - Đặt quyền đệ quy\n  - `install_module` - Gọi quy trình cài đặt module tích hợp\n\n**Trường hợp sử dụng:**\n\n- Xử lý tệp module trước hoặc sau khi cài đặt tích hợp (gọi `install_module` khi sẵn sàng)\n- Di chuyển tệp module\n- Xác thực khả năng tương thích của module\n- Thiết lập cấu trúc thư mục đặc biệt\n- Khởi tạo tài nguyên cụ thể của module\n\n**Lưu ý**: Script này **KHÔNG** được gọi khi cài đặt chính metamodule.\n\n#### 3. metauninstall.sh - Hook Dọn dẹp\n\n**Mục đích**: Dọn dẹp tài nguyên khi các module thông thường được gỡ cài đặt.\n\n**Khi thực thi**: Trong quá trình gỡ cài đặt module, trước khi thư mục module bị xóa.\n\n**Biến môi trường:**\n\n- `MODULE_ID`: ID của module đang được gỡ cài đặt\n\n**Trường hợp sử dụng:**\n\n- Xử lý tệp\n- Dọn dẹp symlink\n- Giải phóng tài nguyên đã phân bổ\n- Cập nhật theo dõi nội bộ\n\n**Script ví dụ:**\n\n```sh\n#!/system/bin/sh\n# Được gọi khi gỡ cài đặt module thông thường\nMODULE_ID=\"$1\"\nIMG_MNT=\"/data/adb/metamodule/mnt\"\n\n# Xóa tệp module khỏi image\nif [ -d \"$IMG_MNT/$MODULE_ID\" ]; then\n    rm -rf \"$IMG_MNT/$MODULE_ID\"\nfi\n```\n\n### Thứ tự Thực thi\n\nHiểu thứ tự thực thi khởi động rất quan trọng cho phát triển metamodule:\n\n```txt\nGiai đoạn post-fs-data:\n  1. Các script post-fs-data.d chung thực thi\n  2. Prune module, restorecon, tải sepolicy.rule\n  3. post-fs-data.sh của metamodule thực thi (nếu có)\n  4. post-fs-data.sh của các module thông thường thực thi\n  5. Tải system.prop\n  6. metamount.sh của metamodule thực thi\n     └─> Mount tất cả các module một cách systemless\n  7. Giai đoạn post-mount.d chạy\n     - Các script post-mount.d chung\n     - post-mount.sh của metamodule (nếu có)\n     - post-mount.sh của các module thông thường\n\nGiai đoạn service:\n  1. Các script service.d chung thực thi\n  2. service.sh của metamodule thực thi (nếu có)\n  3. service.sh của các module thông thường thực thi\n\nGiai đoạn boot-completed:\n  1. Các script boot-completed.d chung thực thi\n  2. boot-completed.sh của metamodule thực thi (nếu có)\n  3. boot-completed.sh của các module thông thường thực thi\n```\n\n**Điểm chính:**\n\n- `metamount.sh` chạy **SAU** tất cả các script post-fs-data (cả metamodule và module thông thường)\n- Các script vòng đời của metamodule (`post-fs-data.sh`, `service.sh`, `boot-completed.sh`) luôn chạy trước các script module thông thường\n- Các script chung trong thư mục `.d` chạy trước các script metamodule\n- Giai đoạn `post-mount` chạy sau khi mount hoàn tất\n\n### Cơ chế Symlink\n\nKhi một metamodule được cài đặt, KernelSU tạo một symlink:\n\n```sh\n/data/adb/metamodule -> /data/adb/modules/<metamodule_id>\n```\n\nĐiều này cung cấp một đường dẫn ổn định để truy cập metamodule đang hoạt động, bất kể ID của nó.\n\n**Lợi ích:**\n\n- Đường dẫn truy cập nhất quán\n- Dễ dàng phát hiện metamodule đang hoạt động\n- Đơn giản hóa cấu hình\n\n### Ví dụ Thực tế: meta-overlayfs\n\nMetamodule `meta-overlayfs` là triển khai tham chiếu chính thức. Nó thể hiện các thực hành tốt nhất cho phát triển metamodule.\n\n#### Kiến trúc\n\n`meta-overlayfs` sử dụng **kiến trúc thư mục kép**:\n\n1. **Thư mục metadata**: `/data/adb/modules/`\n   - Chứa `module.prop`, các marker `disable`, `skip_mount`\n   - Nhanh để quét trong quá trình khởi động\n   - Dung lượng lưu trữ nhỏ\n\n2. **Thư mục nội dung**: `/data/adb/metamodule/mnt/`\n   - Chứa các tệp module thực tế (system, vendor, product, v.v.)\n   - Được lưu trữ trong một ext4 image (`modules.img`)\n   - Tối ưu hóa không gian với các tính năng ext4\n\n#### Triển khai metamount.sh\n\nĐây là cách `meta-overlayfs` triển khai xử lý mount:\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\nIMG_FILE=\"$MODDIR/modules.img\"\nMNT_DIR=\"$MODDIR/mnt\"\n\n# Mount ext4 image nếu chưa được mount\nif ! mountpoint -q \"$MNT_DIR\"; then\n    mkdir -p \"$MNT_DIR\"\n    mount -t ext4 -o loop,rw,noatime \"$IMG_FILE\" \"$MNT_DIR\"\nfi\n\n# Đặt biến môi trường cho hỗ trợ thư mục kép\nexport MODULE_METADATA_DIR=\"/data/adb/modules\"\nexport MODULE_CONTENT_DIR=\"$MNT_DIR\"\n\n# Thực thi binary mount\n# (Logic mount thực tế nằm trong binary Rust)\n\"$MODDIR/meta-overlayfs\"\n```\n\n#### Tính năng Chính\n\n**Mount Overlayfs:**\n\n- Sử dụng kernel overlayfs cho các sửa đổi systemless thực sự\n- Hỗ trợ nhiều phân vùng (system, vendor, product, system_ext, odm, oem)\n- Hỗ trợ lớp đọc-ghi thông qua `/data/adb/modules/.rw/`\n\n**Xác định nguồn:**\n\n```rust\n// Từ meta-overlayfs/src/mount.rs\nfsconfig_set_string(fs, \"source\", \"KSU\")?;  // BẮT BUỘC!\n```\n\nĐiều này đặt `dev=KSU` cho tất cả các overlay mount, cho phép xác định đúng.\n\n### Thực hành Tốt nhất\n\nKhi phát triển metamodule:\n\n1. **Luôn đặt source thành \"KSU\"** cho các thao tác mount - kernel umount và zygisksu umount cần điều này để umount đúng cách\n2. **Xử lý lỗi một cách khéo léo** - các quy trình khởi động nhạy cảm về thời gian\n3. **Tôn trọng các cờ tiêu chuẩn** - hỗ trợ `skip_mount` và `disable`\n4. **Log các thao tác** - sử dụng `echo` hoặc logging để debug\n5. **Kiểm tra kỹ lưỡng** - lỗi mount có thể gây ra vòng lặp khởi động\n6. **Ghi chú hành vi** - giải thích rõ ràng metamodule của bạn làm gì\n7. **Cung cấp đường dẫn di chuyển** - giúp người dùng chuyển đổi từ các giải pháp khác\n\n### Kiểm tra Metamodule của bạn\n\nTrước khi phát hành:\n\n1. **Kiểm tra cài đặt** trên thiết lập KernelSU sạch\n2. **Xác minh mount** với nhiều loại module khác nhau\n3. **Kiểm tra khả năng tương thích** với các module phổ biến\n4. **Kiểm tra gỡ cài đặt** và dọn dẹp\n5. **Xác thực hiệu suất khởi động** (metamount.sh đang chặn!)\n6. **Đảm bảo xử lý lỗi đúng cách** để tránh vòng lặp khởi động\n\n## Câu hỏi Thường gặp\n\n### Tôi có cần metamodule không?\n\n**Đối với người dùng**: Chỉ cần nếu bạn muốn sử dụng các module yêu cầu mount. Nếu bạn chỉ sử dụng các module chạy script mà không sửa đổi tệp hệ thống, bạn không cần metamodule.\n\n**Đối với nhà phát triển module**: Không, bạn phát triển module bình thường. Người dùng chỉ cần metamodule nếu module của bạn yêu cầu mount.\n\n**Đối với người dùng nâng cao**: Chỉ cần nếu bạn muốn tùy chỉnh hành vi mount hoặc tạo các triển khai mount thay thế.\n\n### Tôi có thể có nhiều metamodule không?\n\nKhông. Chỉ có thể cài đặt một metamodule tại một thời điểm. Điều này ngăn chặn xung đột và đảm bảo hành vi có thể dự đoán được.\n\n### Điều gì xảy ra nếu tôi gỡ cài đặt metamodule duy nhất của mình?\n\nCác module sẽ không còn được mount. Thiết bị của bạn sẽ khởi động bình thường, nhưng các sửa đổi của module sẽ không áp dụng cho đến khi bạn cài đặt một metamodule khác.\n\n### meta-overlayfs có bắt buộc không?\n\nKhông. Nó cung cấp mount overlayfs tiêu chuẩn tương thích với hầu hết các module. Bạn có thể tạo metamodule của riêng mình nếu cần hành vi khác.\n\n## Xem thêm\n\n- [Module Guide](module.md) - Phát triển module chung\n- [Difference with Magisk](difference-with-magisk.md) - So sánh KernelSU và Magisk\n- [How to Build](how-to-build.md) - Xây dựng KernelSU từ nguồn\n"
  },
  {
    "path": "website/docs/vi_VN/guide/module-config.md",
    "content": "# Cấu hình module\n\nKernelSU cung cấp hệ thống cấu hình tích hợp cho phép các module lưu trữ các cài đặt key-value liên tục hoặc tạm thời. Cấu hình được lưu trữ ở định dạng nhị phân tại `/data/adb/ksu/module_configs/<module_id>/` với các đặc điểm sau:\n\n## Các loại cấu hình\n\n- **Cấu hình liên tục** (`persist.config`): tồn tại sau khi khởi động lại cho đến khi bị xóa rõ ràng hoặc gỡ cài đặt module\n- **Cấu hình tạm thời** (`tmp.config`): tự động bị xóa trong giai đoạn post-fs-data mỗi khi khởi động\n\nKhi đọc cấu hình, giá trị tạm thời được ưu tiên hơn giá trị liên tục cho cùng một key.\n\n## Sử dụng cấu hình trong script module\n\nTất cả các script module (`post-fs-data.sh`, `service.sh`, `boot-completed.sh`, v.v.) chạy với biến môi trường `KSU_MODULE` được đặt thành ID module. Bạn có thể sử dụng các lệnh `ksud module config` để quản lý cấu hình module của mình:\n\n```bash\n# Lấy giá trị cấu hình\nvalue=$(ksud module config get my_setting)\n\n# Đặt giá trị cấu hình liên tục\nksud module config set my_setting \"some value\"\n\n# Đặt giá trị cấu hình tạm thời (xóa sau khi khởi động lại)\nksud module config set --temp runtime_state \"active\"\n\n# Đặt giá trị từ stdin (hữu ích cho văn bản nhiều dòng hoặc dữ liệu phức tạp)\nksud module config set my_key <<EOF\nvăn bản nhiều dòng\ngiá trị\nEOF\n\n# Hoặc truyền từ lệnh\necho \"value\" | ksud module config set my_key\n\n# Cờ stdin rõ ràng\ncat file.json | ksud module config set json_data --stdin\n\n# Liệt kê tất cả các mục cấu hình (hợp nhất liên tục và tạm thời)\nksud module config list\n\n# Xóa một mục cấu hình\nksud module config delete my_setting\n\n# Xóa một mục cấu hình tạm thời\nksud module config delete --temp runtime_state\n\n# Xóa tất cả cấu hình liên tục\nksud module config clear\n\n# Xóa tất cả cấu hình tạm thời\nksud module config clear --temp\n```\n\n## Giới hạn xác thực\n\nHệ thống cấu hình áp dụng các giới hạn sau:\n\n- **Độ dài key tối đa**: 256 byte\n- **Độ dài giá trị tối đa**: 1MB (1048576 byte)\n- **Số lượng mục cấu hình tối đa**: 32 mỗi module\n- **Định dạng key**: Phải khớp với `^[a-zA-Z][a-zA-Z0-9._-]+$` (như ID module)\n  - Phải bắt đầu bằng một chữ cái\n  - Có thể chứa chữ cái, số, dấu chấm, dấu gạch dưới hoặc dấu gạch ngang\n  - Độ dài tối thiểu: 2 ký tự\n- **Định dạng giá trị**: Không có hạn chế - có thể chứa bất kỳ ký tự UTF-8 nào, bao gồm ngắt dòng và ký tự điều khiển\n  - Được lưu trữ ở định dạng nhị phân có tiền tố độ dài để xử lý dữ liệu an toàn\n\n## Vòng đời\n\n- **Khi khởi động**: Tất cả cấu hình tạm thời được xóa trong giai đoạn post-fs-data\n- **Khi gỡ cài đặt module**: Tất cả cấu hình (liên tục và tạm thời) tự động bị xóa\n- Cấu hình được lưu trữ ở định dạng nhị phân với số ma thuật `0x4b53554d` (\"KSUM\") và xác thực phiên bản\n\n## Trường hợp sử dụng\n\nHệ thống cấu hình lý tưởng cho:\n\n- **Tùy chọn người dùng**: Lưu trữ cài đặt module mà người dùng cấu hình thông qua WebUI hoặc action script\n- **Cờ tính năng**: Bật/tắt tính năng module mà không cần cài đặt lại\n- **Trạng thái runtime**: Theo dõi trạng thái tạm thời nên được đặt lại khi khởi động lại (sử dụng cấu hình tạm thời)\n- **Cài đặt cài đặt**: Ghi nhớ các lựa chọn được thực hiện trong quá trình cài đặt module\n- **Dữ liệu phức tạp**: Lưu trữ JSON, văn bản nhiều dòng, dữ liệu được mã hóa Base64 hoặc bất kỳ nội dung có cấu trúc nào (lên đến 1MB)\n\n::: tip THỰC HÀNH TỐT NHẤT\n- Sử dụng cấu hình liên tục cho tùy chọn người dùng nên tồn tại sau khi khởi động lại\n- Sử dụng cấu hình tạm thời cho trạng thái runtime hoặc cờ tính năng nên được đặt lại khi khởi động\n- Xác thực giá trị cấu hình trong script của bạn trước khi sử dụng chúng\n- Sử dụng lệnh `ksud module config list` để gỡ lỗi các vấn đề cấu hình\n:::\n\n## Tính năng Nâng cao\n\nHệ thống cấu hình mô-đun cung cấp các khóa cấu hình đặc biệt cho các trường hợp sử dụng nâng cao:\n\n### Ghi đè Mô tả Mô-đun {#overriding-module-description}\n\nBạn có thể ghi đè động trường `description` từ `module.prop` bằng cách đặt khóa cấu hình `override.description`:\n\n```bash\n# Ghi đè mô tả mô-đun\nksud module config set override.description \"Mô tả tùy chỉnh được hiển thị trong trình quản lý\"\n```\n\nKhi lấy danh sách mô-đun, nếu cấu hình `override.description` tồn tại, nó sẽ thay thế mô tả gốc từ `module.prop`. Điều này hữu ích cho:\n- Hiển thị thông tin trạng thái động trong mô tả mô-đun\n- Hiển thị chi tiết cấu hình runtime cho người dùng\n- Cập nhật mô tả dựa trên trạng thái mô-đun mà không cần cài đặt lại\n\n### Khai báo Tính năng được Quản lý\n\nCác mô-đun có thể khai báo tính năng KernelSU nào mà chúng quản lý bằng cách sử dụng mẫu cấu hình `manage.<feature>`. Các tính năng được hỗ trợ tương ứng với enum nội bộ `FeatureId` của KernelSU:\n\n**Tính năng được Hỗ trợ:**\n- `su_compat` - Chế độ tương thích SU\n- `kernel_umount` - Tự động unmount kernel\n\n```bash\n# Khai báo rằng mô-đun này quản lý khả năng tương thích SU và bật nó\nksud module config set manage.su_compat true\n\n# Khai báo rằng mô-đun này quản lý unmount kernel và tắt nó\nksud module config set manage.kernel_umount false\n\n# Xóa quản lý tính năng (mô-đun không còn kiểm soát tính năng này)\nksud module config delete manage.su_compat\n```\n\n**Cách hoạt động:**\n- Sự hiện diện của khóa `manage.<feature>` cho biết mô-đun đang quản lý tính năng đó\n- Giá trị cho biết trạng thái mong muốn: `true`/`1` để bật, `false`/`0` (hoặc bất kỳ giá trị nào khác) để tắt\n- Để ngừng quản lý một tính năng, xóa hoàn toàn khóa cấu hình\n\nCác tính năng được quản lý được hiển thị thông qua API danh sách mô-đun dưới dạng trường `managedFeatures` (chuỗi phân tách bằng dấu phẩy). Điều này cho phép:\n- Trình quản lý KernelSU phát hiện mô-đun nào quản lý tính năng KernelSU nào\n- Ngăn chặn xung đột khi nhiều mô-đun cố gắng quản lý cùng một tính năng\n- Phối hợp tốt hơn giữa các mô-đun và chức năng cốt lõi của KernelSU\n\n::: warning CHỈ CÁC TÍNH NĂNG ĐƯỢC HỖ TRỢ\nChỉ sử dụng các tên tính năng được xác định trước được liệt kê ở trên (`su_compat`, `kernel_umount`). Chúng tương ứng với các tính năng nội bộ thực tế của KernelSU. Sử dụng các tên tính năng khác sẽ không gây lỗi nhưng không có mục đích chức năng nào.\n:::\n"
  },
  {
    "path": "website/docs/vi_VN/guide/module-webui.md",
    "content": "# Module WebUI\n\nNgoài việc chạy các script khởi động và chỉnh sửa tệp hệ thống, module KernelSU còn có thể hiển thị giao diện người dùng và tương tác trực tiếp với người dùng.\n\nModule có thể định nghĩa các trang HTML + CSS + JavaScript bằng bất kỳ công nghệ web nào. Trình quản lý của KernelSU hiển thị những trang này thông qua WebView và cung cấp API để tương tác với hệ thống, chẳng hạn như thực thi lệnh shell.\n\n## Thư mục `webroot`\n\nCác tệp tài nguyên web cần được đặt trong thư mục con `webroot` ở thư mục gốc của module và **PHẢI** có tệp `index.html`, đây là điểm vào của trang module. Cấu trúc đơn giản nhất của một module có giao diện web trông như sau:\n\n```txt\n❯ tree .\n.\n|-- module.prop\n`-- webroot\n    `-- index.html\n```\n\n::: warning\nKhi cài đặt module, KernelSU sẽ tự động đặt quyền và ngữ cảnh SELinux cho thư mục này. Nếu bạn không chắc mình đang làm gì, đừng tự ý thay đổi quyền của thư mục!\n:::\n\nNếu trang của bạn có CSS hoặc JavaScript thì cũng cần đặt chúng trong thư mục này.\n\n## JavaScript API\n\nNếu chỉ là trang hiển thị, nó sẽ hoạt động giống một trang web bình thường. Tuy nhiên điều quan trọng nhất là KernelSU cung cấp một loạt API hệ thống cho phép bạn triển khai các chức năng riêng của module.\n\nKernelSU có một thư viện JavaScript được phát hành trên [npm](https://www.npmjs.com/package/kernelsu) để bạn dùng trong mã JavaScript của trang.\n\nVí dụ, bạn có thể thực thi một lệnh shell để lấy cấu hình hoặc thay đổi một thuộc tính:\n\n```JavaScript\nimport { exec } from 'kernelsu';\n\nconst { errno, stdout } = exec(\"getprop ro.product.model\");\n```\n\nBạn cũng có thể chuyển trang sang chế độ toàn màn hình hoặc hiển thị thông báo toast.\n\n[Tài liệu API](https://www.npmjs.com/package/kernelsu)\n\nNếu API hiện tại chưa đáp ứng nhu cầu hoặc khó sử dụng, hãy gửi đề xuất cho chúng tôi [tại đây](https://github.com/tiann/KernelSU/issues)!\n\n## Một vài lưu ý\n\n1. Bạn có thể sử dụng `localStorage` như bình thường để lưu dữ liệu, nhưng hãy nhớ rằng dữ liệu sẽ biến mất khi ứng dụng quản lý bị gỡ cài đặt. Nếu cần lưu trữ lâu dài, hãy tự lưu dữ liệu vào một thư mục cụ thể.\n2. Với các trang đơn giản, chúng tôi gợi ý dùng [parceljs](https://parceljs.org/) để đóng gói. Công cụ này không cần cấu hình ban đầu và rất dễ dùng. Nếu bạn là chuyên gia front-end hoặc có sở thích khác, cứ thoải mái dùng công cụ bạn muốn!\n"
  },
  {
    "path": "website/docs/vi_VN/guide/module.md",
    "content": "# Hướng dẫn mô-đun\n\nKernelSU cung cấp một cơ chế mô-đun giúp đạt được hiệu quả sửa đổi thư mục hệ thống trong khi vẫn duy trì tính toàn vẹn của phân vùng system. Cơ chế này thường được gọi là \"systemless\".\n\nCơ chế mô-đun của KernelSU gần giống với Magisk. Nếu bạn đã quen với việc phát triển mô-đun Magisk thì việc phát triển các mô-đun KernelSU cũng rất tương tự. Bạn có thể bỏ qua phần giới thiệu các mô-đun bên dưới và chỉ cần đọc [difference-with-magisk](difference-with-magisk.md).\n\n::: warning METAMODULE CHỈ CẦN THIẾT ĐỂ SỬA ĐỔI TỆP HỆ THỐNG\nKernelSU sử dụng kiến trúc [metamodule](metamodule.md) để mount thư mục `system`. **Chỉ khi module của bạn cần sửa đổi tệp `/system`** (thông qua thư mục `system`), bạn mới cần cài đặt metamodule (như [meta-overlayfs](https://github.com/tiann/KernelSU/releases)). Các tính năng module khác như scripts, quy tắc sepolicy và system.prop hoạt động mà không cần metamodule.\n:::\n\n## WebUI\n\nKernelSU modules support displaying interfaces and interacting with users. Xem [tài liệu WebUI](module-webui.md) để biết thêm chi tiết.\n\n## Cấu hình module\n\nKernelSU cung cấp hệ thống cấu hình tích hợp cho phép các module lưu trữ các cài đặt key-value liên tục hoặc tạm thời. Để biết thêm chi tiết, xem [tài liệu Cấu hình module](module-config.md).\n\n## Busybox\n\nKernelSU cung cấp tính năng nhị phân BusyBox hoàn chỉnh (bao gồm hỗ trợ SELinux đầy đủ). Tệp thực thi được đặt tại `/data/adb/ksu/bin/busybox`. BusyBox của KernelSU hỗ trợ \"ASH Standalone Shell Mode\" có thể chuyển đổi thời gian chạy. Standalone mode này có nghĩa là khi chạy trong shell `ash` của BusyBox, mọi lệnh sẽ trực tiếp sử dụng applet trong BusyBox, bất kể cái gì được đặt là `PATH`. Ví dụ: các lệnh như `ls`, `rm`, `chmod` sẽ **KHÔNG** sử dụng những gì có trong `PATH` (trong trường hợp Android theo mặc định, nó sẽ là `/system/bin/ls`, ` /system/bin/rm` và `/system/bin/chmod` tương ứng), nhưng thay vào đó sẽ gọi trực tiếp các ứng dụng BusyBox nội bộ. Điều này đảm bảo rằng các tập lệnh luôn chạy trong môi trường có thể dự đoán được và luôn có bộ lệnh đầy đủ cho dù nó đang chạy trên phiên bản Android nào. Để buộc lệnh _not_ sử dụng BusyBox, bạn phải gọi tệp thực thi có đường dẫn đầy đủ.\n\nMỗi tập lệnh shell đơn lẻ chạy trong ngữ cảnh của KernelSU sẽ được thực thi trong shell `ash` của BusyBox với standalone mode được bật. Đối với những gì liên quan đến nhà phát triển bên thứ 3, điều này bao gồm tất cả các tập lệnh khởi động và tập lệnh cài đặt mô-đun.\n\nĐối với những người muốn sử dụng tính năng \"Standalone mode\" này bên ngoài KernelSU, có 2 cách để kích hoạt tính năng này:\n\n1. Đặt biến môi trường `ASH_STANDALONE` thành `1`<br>Ví dụ: `ASH_STANDALONE=1 /data/adb/ksu/bin/busybox sh <script>`\n2. Chuyển đổi bằng các tùy chọn dòng lệnh:<br>`/data/adb/ksu/bin/busybox sh -o độc lập <script>`\n\nĐể đảm bảo tất cả shell `sh` tiếp theo được thực thi cũng chạy ở standalone mode, tùy chọn 1 là phương thức ưu tiên (và đây là những gì KernelSU và KernelSU manager sử dụng nội bộ) vì các biến môi trường được kế thừa xuống các tiến trình con.\n\n::: tip sự khác biệt với Magisk\n\nBusyBox của KernelSU hiện đang sử dụng tệp nhị phân được biên dịch trực tiếp từ dự án Magisk. **Cảm ơn Magisk!** Vì vậy, bạn không phải lo lắng về vấn đề tương thích giữa các tập lệnh BusyBox trong Magisk và KernelSU vì chúng hoàn toàn giống nhau!\n:::\n\n## Mô-đun hạt nhânSU\n\nMô-đun KernelSU là một thư mục được đặt trong `/data/adb/modules` với cấu trúc bên dưới:\n\n```txt\n/data/adb/modules\n├── .\n├── .\n|\n├── $MODID                  <--- Thư mục được đặt tên bằng ID của mô-đun\n│   │\n│   │      *** Nhận Dạng Mô-đun ***\n│   │\n│   ├── module.prop         <--- Tệp này lưu trữ metadata của mô-đun\n│   │\n│   │      *** Nội Dung Chính ***\n│   │\n│   ├── system              <--- Thư mục này sẽ được gắn kết nếu skip_mount không tồn tại\n│   │   ├── ...\n│   │   ├── ...\n│   │   └── ...\n│   │\n│   │      *** Cờ Trạng Thái ***\n│   │\n│   ├── skip_mount          <--- Nếu tồn tại, KernelSU sẽ KHÔNG gắn kết thư mục hệ thống của bạn\n│   ├── disable             <--- Nếu tồn tại, mô-đun sẽ bị vô hiệu hóa\n│   ├── remove              <--- Nếu tồn tại, mô-đun sẽ bị xóa trong lần khởi động lại tiếp theo\n│   │\n│   │      *** Tệp Tùy Chọn ***\n│   │\n│   ├── post-fs-data.sh     <--- Tập lệnh này sẽ được thực thi trong post-fs-data\n│   ├── post-mount.sh       <--- Tập lệnh này sẽ được thực thi trong post-mount\n│   ├── service.sh          <--- Tập lệnh này sẽ được thực thi trong dịch vụ late_start\n│   ├── boot-completed.sh   <--- Tập lệnh này sẽ được thực thi khi khởi động xong\n|   ├── uninstall.sh        <--- Tập lệnh này sẽ được thực thi khi KernelSU xóa mô-đun của bạn\n│   ├── system.prop         <--- Các thuộc tính trong tệp này sẽ được tải dưới dạng thuộc tính hệ thống bằng resetprop\n│   ├── sepolicy.rule       <--- Quy tắc riêng biệt tùy chỉnh bổ sung\n│   │\n│   │      *** Được Tạo Tự Động, KHÔNG TẠO HOẶC SỬA ĐỔI THỦ CÔNG ***\n│   │\n│   ├── vendor              <--- Một liên kết tượng trưng đến $MODID/system/vendor\n│   ├── product             <--- Một liên kết tượng trưng đến $MODID/system/product\n│   ├── system_ext          <--- Một liên kết tượng trưng đến $MODID/system/system_ext\n│   │\n│   │      *** Mọi tập tin / thư mục bổ sung đều được phép ***\n│   │\n│   ├── ...\n│   └── ...\n|\n├── another_module\n│   ├── .\n│   └── .\n├── .\n├── .\n```\n\n::: tip sự khác biệt với Magisk\nKernelSU không có hỗ trợ tích hợp cho Zygisk nên không có nội dung liên quan đến Zygisk trong mô-đun. Tuy nhiên, bạn có thể sử dụng [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) để hỗ trợ các mô-đun Zygisk. Trong trường hợp này, nội dung của mô-đun Zygisk giống hệt với nội dung được Magisk hỗ trợ.\n:::\n\n### module.prop\n\nmodule.prop là tệp cấu hình cho mô-đun. Trong KernelSU, nếu một mô-đun không chứa tệp này, nó sẽ không được nhận dạng là mô-đun. Định dạng của tập tin này như sau:\n\n```txt\nid=<string>\nname=<string>\nversion=<string>\nversionCode=<int>\nauthor=<string>\ndescription=<string>\nupdateJson=<url> (optional)\nactionIcon=<path> (optional)\nwebuiIcon=<path> (optional)\n```\n\n- `id` phải khớp với biểu thức chính quy này: `^[a-zA-Z][a-zA-Z0-9._-]+$`<br>\n   ví dụ: ✓ `a_module`, ✓ `a.module`, ✓ `module-101`, ✗ `a module`, ✗ `1_module`, ✗ `-a-module`<br>\n   Đây là **mã định danh duy nhất** của mô-đun của bạn. Bạn không nên thay đổi nó sau khi được xuất bản.\n- `versionCode` phải là **số nguyên**. Điều này được sử dụng để so sánh các phiên bản.\n- Các chuỗi khác không được đề cập ở trên có thể là chuỗi **một dòng** bất kỳ.\n- Đảm bảo sử dụng kiểu ngắt dòng `UNIX (LF)` chứ không phải `Windows (CR+LF)` hoặc `Macintosh (CR)`.\n- `actionIcon` và `webuiIcon` là các đường dẫn hình ảnh tùy chọn, được dùng làm\n  biểu tượng mặc định cho phím tắt hành động và phím tắt WebUI của mô-đun trong\n  ứng dụng quản lý. Các đường dẫn này phải là đường dẫn tương đối so với thư mục gốc\n  của mô-đun. Ví dụ, `actionIcon=icon/icon.png` sẽ được ánh xạ thành `<MODDIR>/icon/icon.png`.\n\n::: tip MÔ TẢ ĐỘNG\nTrường `description` có thể được ghi đè động khi chạy bằng hệ thống cấu hình mô-đun. Xem [Ghi đè Mô tả Mô-đun](module-config.md#overriding-module-description) để biết chi tiết.\n:::\n\n### Tập lệnh Shell\n\nVui lòng đọc phần [Boot Scripts](#boot-scripts) để hiểu sự khác biệt giữa `post-fs-data.sh` và `service.sh`. Đối với hầu hết các nhà phát triển mô-đun, `service.sh` sẽ đủ tốt nếu bạn chỉ cần chạy tập lệnh khởi động, nếu bạn cần chạy tập lệnh sau khi khởi động xong, vui lòng sử dụng `boot-completed.sh`. Nếu bạn muốn làm gì đó sau khi gắn các lớp phủ, vui lòng sử dụng `post-mount.sh`.\n\nTrong tất cả các tập lệnh của mô-đun của bạn, vui lòng sử dụng `MODDIR=${0%/*}` để lấy đường dẫn thư mục cơ sở của mô-đun của bạn; **KHÔNG** mã hóa cứng đường dẫn mô-đun của bạn trong tập lệnh.\n\n::: tip sự khác biệt với Magisk\nBạn có thể sử dụng biến môi trường KSU để xác định xem tập lệnh đang chạy trong KernelSU hay Magisk. Nếu chạy trong KernelSU, giá trị này sẽ được đặt thành true.\n:::\n\n### thư mục `system`\n\nNội dung của thư mục này sẽ được phủ lên trên phân vùng /system của hệ thống sau khi hệ thống được khởi động. Điều này có nghĩa rằng:\n\n::: tip YÊU CẦU METAMODULE\nThư mục `system` chỉ được mount nếu bạn đã cài đặt metamodule cung cấp chức năng mounting (như `meta-overlayfs`). Metamodule xử lý cách các module được mount. Xem [Hướng dẫn Metamodule](metamodule.md) để biết thêm thông tin.\n:::\n\n1. Các file có cùng tên với các file trong thư mục tương ứng trong hệ thống sẽ bị ghi đè bởi các file trong thư mục này.\n2. Các thư mục có cùng tên với thư mục tương ứng trong hệ thống sẽ được gộp với các thư mục trong thư mục này.\n\nNếu bạn muốn xóa một tập tin hoặc thư mục trong thư mục hệ thống gốc, bạn cần tạo một tập tin có cùng tên với tập tin/thư mục trong thư mục mô-đun bằng cách sử dụng `mknod filename c 0 0`. Bằng cách này, hệ thống lớp phủ sẽ tự động \"whiteout\" (Xóa trắng) tệp này như thể nó đã bị xóa (phân vùng /system không thực sự bị thay đổi).\n\nBạn cũng có thể khai báo một biến có tên `REMOVE` chứa danh sách các thư mục trong `customize.sh` để thực hiện các thao tác xóa và KernelSU sẽ tự động thực thi `mknod <TARGET> c 0 0` trong các thư mục tương ứng của mô-đun. Ví dụ:\n\n```sh\nREMOVE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n```\n\nDanh sách trên sẽ thực thi `mknod $MODPATH/system/app/YouTuBe c 0 0` và `mknod $MODPATH/system/app/Bloatware c 0 0`; và `/system/app/YouTube` và `/system/app/Bloatware` sẽ bị xóa sau khi mô-đun này có hiệu lực.\n\nNếu bạn muốn thay thế một thư mục trong hệ thống, bạn cần tạo một thư mục có cùng đường dẫn trong thư mục mô-đun của mình, sau đó đặt thuộc tính `setfattr -ntrust.overlay.opaque -v y <TARGET>` cho thư mục này. Bằng cách này, hệ thống Overlayfs sẽ tự động thay thế thư mục tương ứng trong hệ thống (mà không thay đổi phân vùng /system).\n\nBạn có thể khai báo một biến có tên `REPLACE` trong tệp `customize.sh` của mình, bao gồm danh sách các thư mục sẽ được thay thế và KernelSU sẽ tự động thực hiện các thao tác tương ứng trong thư mục mô-đun của bạn. Ví dụ:\n\nREPLACE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n\nDanh sách này sẽ tự động tạo các thư mục `$MODPATH/system/app/YouTube` và `$MODPATH/system/app/Bloatware`, sau đó thực thi `setfattr -ntrusted.overlay.opaque -v y $MODPATH/system/app/ YouTube` và `setfattr -n Trust.overlay.opaque -v y $MODPATH/system/app/Bloatware`. Sau khi mô-đun có hiệu lực, `/system/app/YouTube` và `/system/app/Bloatware` sẽ được thay thế bằng các thư mục trống.\n\n::: tip sự khác biệt với Magisk\n\nKernelSU sử dụng kiến trúc [metamodule](metamodule.md) trong đó việc mounting được ủy thác cho các metamodule có thể cắm được. Metamodule `meta-overlayfs` chính thức sử dụng OverlayFS của kernel cho các sửa đổi systemless, trong khi Magisk sử dụng magic mount (bind mount) được tích hợp trực tiếp vào lõi của nó. Cả hai đều đạt được cùng một mục tiêu: sửa đổi tệp `/system` mà không sửa đổi vật lý phân vùng `/system`. Cách tiếp cận của KernelSU mang lại tính linh hoạt cao hơn và giảm bề mặt phát hiện.\n:::\n\nNếu bạn quan tâm đến overlayfs, bạn nên đọc [documentation on overlayfs](https://docs.kernel.org/filesystems/overlayfs.html) của Kernel Linux.\n\n### system.prop\n\nTệp này có cùng định dạng với `build.prop`. Mỗi dòng bao gồm `[key]=[value]`.\n\n### sepolicy.rule\n\nNếu mô-đun của bạn yêu cầu một số bản vá lỗi chính sách bổ sung, vui lòng thêm các quy tắc đó vào tệp này. Mỗi dòng trong tệp này sẽ được coi là một tuyên bố chính sách.\n\n## Trình cài đặt mô-đun\n\nTrình cài đặt mô-đun KernelSU là mô-đun KernelSU được đóng gói trong tệp zip có thể được flash trong APP KernelSU manager. Trình cài đặt mô-đun KernelSU đơn giản chỉ là mô-đun KernelSU được đóng gói dưới dạng tệp zip.\n\n```txt\nmodule.zip\n│\n├── customize.sh                       <--- (Tùy chọn, biết thêm chi tiết sau)\n│                                           Tập lệnh này sẽ có nguồn gốc từ update-binary\n├── ...\n├── ...  /* Các tập tin còn lại của mô-đun */\n│\n```\n\n:::warning\nMô-đun KernelSU KHÔNG được hỗ trợ để cài đặt trong khôi phục tùy chỉnh!!\n:::\n\n### Tùy chỉnh\n\nNếu bạn cần tùy chỉnh quá trình cài đặt mô-đun, bạn có thể tùy ý tạo một tập lệnh trong trình cài đặt có tên `customize.sh`. Tập lệnh này sẽ được _sourced_ (không được thực thi!) bởi tập lệnh cài đặt mô-đun sau khi tất cả các tệp được trích xuất và các quyền mặc định cũng như văn bản thứ hai được áp dụng. Điều này rất hữu ích nếu mô-đun của bạn yêu cầu thiết lập bổ sung dựa trên ABI của thiết bị hoặc bạn cần đặt các quyền/văn bản thứ hai đặc biệt cho một số tệp mô-đun của mình.\n\nNếu bạn muốn kiểm soát và tùy chỉnh hoàn toàn quá trình cài đặt, hãy khai báo `SKIPUNZIP=1` trong `customize.sh` để bỏ qua tất cả các bước cài đặt mặc định. Bằng cách đó, `customize.sh` của bạn sẽ chịu trách nhiệm cài đặt mọi thứ.\n\nTập lệnh `customize.sh` chạy trong shell `ash` BusyBox của KernelSU với \"Chế độ độc lập\" được bật. Có sẵn các biến và hàm sau:\n\n#### Biến\n\n- `KSU` (bool): biến để đánh dấu script đang chạy trong môi trường KernelSU, và giá trị của biến này sẽ luôn đúng. Bạn có thể sử dụng nó để phân biệt giữa KernelSU và Magisk.\n- `KSU_VER` (chuỗi): chuỗi phiên bản của KernelSU được cài đặt hiện tại (ví dụ: `v0.4.0`)\n- `KSU_VER_CODE` (int): mã phiên bản của KernelSU được cài đặt hiện tại trong không gian người dùng (ví dụ: `10672`)\n- `KSU_KERNEL_VER_CODE` (int): mã phiên bản của KernelSU được cài đặt hiện tại trong không gian kernel (ví dụ: `10672`)\n- `BOOTMODE` (bool): luôn là `true` trong KernelSU\n- `MODPATH` (đường dẫn): đường dẫn nơi các tập tin mô-đun của bạn sẽ được cài đặt\n- `TMPDIR` (đường dẫn): nơi bạn có thể lưu trữ tạm thời các tập tin\n- `ZIPFILE` (đường dẫn): zip cài đặt mô-đun của bạn\n- `ARCH` (chuỗi): kiến trúc CPU của thiết bị. Giá trị là `arm`, `arm64`, `x86` hoặc `x64`\n- `IS64BIT` (bool): `true` nếu `$ARCH` là `arm64` hoặc `x64`\n- `API` (int): cấp độ API (phiên bản Android) của thiết bị (ví dụ: `23` cho Android 6.0)\n\n::: warning\nTrong KernelSU, MAGISK_VER_CODE luôn là 25200 và MAGISK_VER luôn là v25.2. Vui lòng không sử dụng hai biến này để xác định xem nó có chạy trên KernelSU hay không.\n:::\n\n#### Hàm\n\n```txt\nui_print <msg>\n    in <msg> ra console\n    Tránh sử dụng 'echo' vì nó sẽ không hiển thị trong console của recovery tùy chỉnh\n\nabort <msg>\n    in thông báo lỗi <msg> ra bàn điều khiển và chấm dứt cài đặt\n    Tránh sử dụng 'exit' vì nó sẽ bỏ qua các bước dọn dẹp chấm dứt\n\nset_perm <target> <owner> <group> <permission> [context]\n    nếu [context] không được đặt, mặc định là \"u:object_r:system_file:s0\"\n    chức năng này là một shorthand cho các lệnh sau:\n       chown owner.group target\n       chmod permission target\n       chcon context target\n\nset_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]\n    nếu [context] không được đặt, mặc định là \"u:object_r:system_file:s0\"\n    đối với tất cả các tệp trong <directory>, nó sẽ gọi:\n       bối cảnh cấp phép tệp của nhóm chủ sở hữu tệp set_perm\n    đối với tất cả các thư mục trong <directory> (bao gồm cả chính nó), nó sẽ gọi:\n       set_perm bối cảnh phân quyền của nhóm chủ sở hữu thư mục\n```\n\n## Tập lệnh khởi động\n\nTrong KernelSU, tập lệnh được chia thành hai loại dựa trên chế độ chạy của chúng: chế độ post-fs-data và chế độ dịch vụ late_start:\n\n- chế độ post-fs-data\n   - Giai đoạn này là BLOCKING. Quá trình khởi động bị tạm dừng trước khi thực thi xong hoặc đã trôi qua 10 giây.\n   - Các tập lệnh chạy trước khi bất kỳ mô-đun nào được gắn kết. Điều này cho phép nhà phát triển mô-đun tự động điều chỉnh các mô-đun của họ trước khi nó được gắn kết.\n   - Giai đoạn này xảy ra trước khi Zygote được khởi động, điều này gần như có ý nghĩa đối với mọi thứ trong Android\n   - **CẢNH BÁO:** sử dụng `setprop` sẽ làm quá trình khởi động bị nghẽn! Thay vào đó, vui lòng sử dụng `resetprop -n <prop_name> <prop_value>`.\n   - **Chỉ chạy tập lệnh ở chế độ này nếu cần thiết.**\n- chế độ dịch vụ late_start\n   - Giai đoạn này là NON-BLOCKING. Tập lệnh của bạn chạy song song với phần còn lại của quá trình khởi động.\n   - **Đây là giai đoạn được khuyến nghị để chạy hầu hết các tập lệnh.**\n\nTrong KernelSU, tập lệnh khởi động được chia thành hai loại dựa trên vị trí lưu trữ của chúng: tập lệnh chung và tập lệnh mô-đun:\n\n- Kịch Bản Chung\n   - Được đặt trong `/data/adb/post-fs-data.d`, `/data/adb/service.d`, `/data/adb/post-mount.d` hoặc `/data/adb/boot- đã hoàn thành.d`\n   - Chỉ được thực thi nếu tập lệnh được đặt là có thể thực thi được (`chmod +x script.sh`)\n   - Các tập lệnh trong `post-fs-data.d` chạy ở chế độ post-fs-data và các tập lệnh trong `service.d` chạy ở chế độ dịch vụ late_start.\n   - Các mô-đun **KHÔNG** thêm các tập lệnh chung trong quá trình cài đặt\n- Tập Lệnh Mô-đun\n   - Được đặt trong thư mục riêng của mô-đun\n   - Chỉ thực hiện nếu mô-đun được kích hoạt\n   - `post-fs-data.sh` chạy ở chế độ post-fs-data, `service.sh` chạy ở chế độ dịch vụ late_start, `boot-completed.sh` chạy khi khởi động xong, `post-mount.sh` chạy trên overlayfs được gắn kết.\n\nTất cả các tập lệnh khởi động sẽ chạy trong shell `ash` BusyBox của KernelSU với \"Standalone Mode\" được bật.\n\n## Chế độ late-load {#late-load-mode}\n\nNgoài quy trình khởi động tiêu chuẩn được mô tả ở trên, KernelSU hỗ trợ **chế độ late-load** cho các tình huống LKM (Loadable Kernel Module). Trong chế độ này, mô-đun kernel KernelSU được tải **sau khi hệ thống đã khởi động hoàn toàn**, thay vì trong quá trình init.\n\n### Khi nào late-load xảy ra?\n\nLate-load được kích hoạt bằng cách chạy lệnh `ksud late-load`. Lệnh này:\n\n1. Phát hiện phiên bản KMI hiện tại và tải `kernelsu.ko` tương ứng từ tài nguyên nhúng.\n2. Thực hiện khởi tạo mô-đun (quy tắc SELinux, danh sách cho phép, tính năng, v.v.) thường xảy ra trong quá trình khởi động.\n\nVì hệ thống đã chạy hoàn toàn, một số cơ chế thời gian khởi động không khả dụng hoặc không cần thiết.\n\n### Sự khác biệt so với khởi động tiêu chuẩn\n\n| Hành vi | Khởi động tiêu chuẩn | Chế độ late-load |\n|---------|:---:|:---:|\n| Mô-đun kernel được tải bởi init (PID 1) | Có | Không (tải sau khi khởi động) |\n| Hook kprobe của ksud (execve/read/fstat/input) | Có | Bỏ qua |\n| Phát hiện chế độ an toàn (phím âm lượng) | Có | Luôn tắt |\n| Thu thập nhật ký khởi động (logcat/dmesg) | Có | Bỏ qua |\n| Kiểm tra cùng tồn tại với Magisk | Có | Bỏ qua |\n| Sự kiện `post-fs-data` báo cáo cho kernel | Có | Bỏ qua |\n| Sự kiện `boot-completed` báo cáo cho kernel | Có | Đặt trực tiếp khi khởi tạo |\n| Tập lệnh `post-fs-data.sh` / `post-fs-data.d/` | Có | Thay thế bằng giai đoạn `late-load` |\n| Tải `system.prop` | Có | Có |\n| Gắn kết OverlayFS (metamodule) | Có | Có |\n| Tập lệnh `post-mount.sh` / `post-mount.d/` | Có | Có |\n| Tập lệnh `service.sh` / `service.d/` | Có | Có |\n| Tập lệnh `boot-completed.sh` / `boot-completed.d/` | Có | Có |\n| Biến môi trường `KSU_LATE_LOAD` | Không đặt | Đặt thành `1` |\n| Cờ info kernel `0x4` | Không đặt | Đã đặt |\n\n### Thứ tự thực thi tập lệnh\n\nTrong chế độ late-load, thứ tự thực thi tập lệnh như sau:\n\n```txt\nksud late-load:\n  1. Tải kernelsu.ko (nếu chưa được tải)\n  2. Giải nén tệp nhị phân, xử lý cập nhật mô-đun, tải quy tắc SELinux, khởi tạo tính năng\n  3. Thực thi tập lệnh late-load.d/ và tập lệnh late-load của mô-đun (chặn)\n  4. Tải system.prop (resetprop -n)\n  5. Thực thi tập lệnh mount của metamodule (OverlayFS)\n  6. Thực thi tập lệnh post-mount.d/ và post-mount.sh của mô-đun (chặn)\n  7. Thực thi tập lệnh service.d/ và service.sh của mô-đun (không chặn)\n  8. Thực thi tập lệnh boot-completed.d/ và boot-completed.sh của mô-đun (không chặn)\n```\n\n### Tập lệnh dành riêng cho late-load\n\nMô-đun có thể cung cấp tập lệnh `late-load.sh` chỉ chạy **trong chế độ late-load**, thay thế cho `post-fs-data.sh`. Tập lệnh này chạy trước khi gắn kết OverlayFS, tương tự như `post-fs-data.sh` trong luồng tiêu chuẩn.\n\nNgoài ra, các tập lệnh chung có thể được đặt trong `/data/adb/late-load.d/` để thực thi trong giai đoạn này.\n\n### Phát hiện chế độ late-load trong tập lệnh\n\nMô-đun có thể phát hiện chế độ late-load bằng cách kiểm tra biến môi trường `KSU_LATE_LOAD`:\n\n```sh\nif [ \"$KSU_LATE_LOAD\" = \"1\" ]; then\n    # Đang chạy trong chế độ late-load\n    echo \"Late-load mode detected\"\nfi\n```\n\nĐiều này cho phép các mô-đun điều chỉnh hành vi của mình, ví dụ bỏ qua các thao tác chỉ cần thiết trong quá trình khởi động sớm.\n"
  },
  {
    "path": "website/docs/vi_VN/guide/rescue-from-bootloop.md",
    "content": "# Cứu khỏi bootloop (Vòng lặp khởi động)\n\nKhi flash một thiết bị, chúng ta có thể gặp phải tình trạng máy \"bị brick\". Về lý thuyết, nếu bạn chỉ sử dụng fastboot để flash phân vùng boot hoặc cài đặt các mô-đun không phù hợp khiến máy không khởi động được thì điều này có thể được khắc phục bằng các thao tác thích hợp. Tài liệu này nhằm mục đích cung cấp một số phương pháp khẩn cấp để giúp bạn khôi phục từ thiết bị \"bị brick\".\n\n## Brick bởi flash vào phân vùng boot\n\nTrong KernelSU, các tình huống sau có thể gây ra lỗi khởi động khi flash phân vùng khởi động:\n\n1. Bạn flash image boot sai định dạng. Ví dụ: nếu định dạng khởi động điện thoại của bạn là `gz`, nhưng bạn flash image định dạng `lz4` thì điện thoại sẽ không thể khởi động.\n2. Điện thoại của bạn cần tắt xác minh AVB để khởi động bình thường (thường yêu cầu xóa tất cả dữ liệu trên điện thoại).\n3. Kernel của bạn có một số lỗi hoặc không phù hợp để flash điện thoại của bạn.\n\nBất kể tình huống thế nào, bạn có thể khôi phục bằng cách **flash boot image gốc**. Do đó, khi bắt đầu hướng dẫn cài đặt, chúng tôi thực sự khuyên bạn nên sao lưu boot image gốc trước khi flash. Nếu chưa sao lưu, bạn có thể lấy boot image gốc từ người dùng khác có cùng thiết bị với bạn hoặc từ chương trình cơ sở chính thức (official firmware).\n\n## Brick bởi mô-đun\n\nViệc cài đặt mô-đun có thể là nguyên nhân phổ biến hơn khiến thiết bị của bạn bị brick, nhưng chúng tôi phải nghiêm túc cảnh báo bạn: **Không cài đặt mô-đun từ các nguồn không xác định**! Vì các mô-đun có đặc quyền root nên chúng có thể gây ra thiệt hại không thể khắc phục cho thiết bị của bạn!\n\n### Mô-đun bình thường\n\nNếu bạn đã flash một mô-đun đã được chứng minh là an toàn nhưng khiến thiết bị của bạn không khởi động được thì tình huống này có thể dễ dàng phục hồi trong KernelSU mà không phải lo lắng gì. KernelSU có các cơ chế tích hợp sẵn để giải cứu thiết bị của bạn, bao gồm:\n\n1. Cập nhật AB\n2. Cứu bằng cách nhấn Giảm âm lượng\n\n#### Cập nhật AB\n\nCác bản cập nhật mô-đun của KernelSU lấy cảm hứng từ cơ chế cập nhật AB của hệ thống Android được sử dụng trong các bản cập nhật OTA. Nếu bạn cài đặt một mô-đun mới hoặc cập nhật mô-đun hiện có, nó sẽ không trực tiếp sửa đổi tệp mô-đun hiện đang sử dụng. Thay vào đó, tất cả các mô-đun sẽ được tích hợp vào một hình ảnh cập nhật khác. Sau khi hệ thống được khởi động lại, nó sẽ cố gắng bắt đầu sử dụng hình ảnh cập nhật này. Nếu hệ thống Android khởi động thành công, các mô-đun sẽ được cập nhật thực sự.\n\nVì vậy, phương pháp đơn giản và được sử dụng phổ biến nhất để cứu thiết bị của bạn là **buộc khởi động lại**. Nếu bạn không thể khởi động hệ thống của mình sau khi flash một mô-đun, bạn có thể nhấn và giữ nút nguồn trong hơn 10 giây và hệ thống sẽ tự động khởi động lại; sau khi khởi động lại, nó sẽ quay trở lại trạng thái trước khi cập nhật mô-đun và các mô-đun được cập nhật trước đó sẽ tự động bị tắt.\n\n#### Cứu bằng cách nhấn Giảm âm lượng\n\nNếu bản cập nhật AB vẫn không giải quyết được vấn đề, bạn có thể thử sử dụng **Chế độ an toàn**. Ở Chế độ an toàn, tất cả các mô-đun đều bị tắt.\n\nCó hai cách để vào Chế độ an toàn:\n\n1. Chế Độ An Toàn tích hợp (built-in Safe Mode) của một số hệ thống; một số hệ thống có Chế độ an toàn tích hợp có thể được truy cập bằng cách nhấn và giữ nút giảm âm lượng, trong khi những hệ thống khác (chẳng hạn như MIUI) có thể bật Chế Độ An Toàn trong Recovery. Khi vào Chế Độ An Toàn của hệ thống, KernelSU cũng sẽ vào Chế Độ An Toàn và tự động tắt các mô-đun.\n2. Chế Độ An Toàn tích hợp (built-in Safe Mode) của KernelSU; phương pháp thao tác là **nhấn phím giảm âm lượng liên tục hơn ba lần** sau màn hình khởi động đầu tiên. Lưu ý là nhấn-thả, nhấn-thả, nhấn-thả chứ không phải nhấn giữ.\n\nSau khi vào chế độ an toàn, tất cả các mô-đun trên trang mô-đun của KernelSU Manager đều bị tắt nhưng bạn có thể thực hiện thao tác \"gỡ cài đặt\" để gỡ cài đặt bất kỳ mô-đun nào có thể gây ra sự cố.\n\nChế độ an toàn tích hợp được triển khai trong kernel, do đó không có khả năng thiếu các sự kiện chính do bị chặn. Tuy nhiên, đối với các hạt nhân không phải GKI, có thể cần phải tích hợp mã thủ công và bạn có thể tham khảo tài liệu chính thức để được hướng dẫn.\n\n### Mô-đun độc hại\n\nNếu các phương pháp trên không thể cứu được thiết bị của bạn thì rất có thể mô-đun bạn cài đặt có hoạt động độc hại hoặc đã làm hỏng thiết bị của bạn thông qua các phương tiện khác. Trong trường hợp này, chỉ có hai gợi ý:\n\n1. Xóa sạch dữ liệu và flash hệ thống chính thức.\n2. Tham khảo dịch vụ hậu mãi.\n"
  },
  {
    "path": "website/docs/vi_VN/guide/unofficially-support-devices.md",
    "content": "# Thiết bị hỗ trợ không chính thức\n\n::: warning\nTài liệu này chỉ để tham khảo lưu trữ và không còn được duy trì.\nKể từ KernelSU v1.0, chúng tôi đã ngừng hỗ trợ chính thức cho các thiết bị không phải GKI.\n:::\n\n::: warning\nĐây là trang liệt kê kernel cho các thiết bị không dùng GKI được hỗ trợ bởi các lập trình viên khác.\n\n:::\n\n::: warning\nTrang này chỉ để cho bạn tìm thấy source cho thiết bị của bạn, nó **HOÀN TOÀN KHÔNG** được review bởi _lập trình viên của KernelSU_. Vậy nên hãy chấp nhận rủi ro khi sử dụng chúng.\n\n:::\n\n<script setup>\nimport data from '../../repos.json'\n</script>\n\n<table>\n   <thead>\n      <tr>\n         <th>Người bảo trì</th>\n         <th>Kho lưu trữ</th>\n         <th>Thiết bị hỗ trợ</th>\n      </tr>\n   </thead>\n   <tbody>\n    <tr v-for=\"repo in data\" :key=\"repo.devices\">\n        <td><a :href=\"repo.maintainer_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.maintainer }}</a></td>\n        <td><a :href=\"repo.kernel_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.kernel_name }}</a></td>\n        <td>{{ repo.devices }}</td>\n    </tr>\n   </tbody>\n</table>\n"
  },
  {
    "path": "website/docs/vi_VN/guide/what-is-kernelsu.md",
    "content": "# KernelSU là gì?\n\nKernelSU là một giải pháp root cho các thiết bị Android GKI, nó hoạt động ở chế độ kernel và cấp quyền root cho ứng dụng không gian người dùng trực tiếp trong không gian kernel.\n\n## Tính năng\n\nTính năng chính của KernelSU là **Kernel-based** (dựa trên Kernel). KernelSU hoạt động ở chế độ kernel nên nó có thể cung cấp giao diện kernel mà chúng ta chưa từng có trước đây. Ví dụ: chúng ta có thể thêm điểm dừng phần cứng vào bất kỳ quy trình nào ở chế độ kernel; Chúng ta có thể truy cập bộ nhớ vật lý của bất kỳ quy trình nào mà không bị phát hiện; Chúng ta còn có thể chặn bất kỳ syscall nào trong không gian kernel; v.v.\n\nNgoài ra, KernelSU cung cấp [hệ thống metamodule](metamodule.md), đây là một kiến trúc có thể cắm để quản lý module. Không giống như các giải pháp root truyền thống tích hợp logic mount vào lõi, KernelSU ủy thác điều này cho metamodules. Điều này cho phép bạn cài đặt metamodules (như [meta-overlayfs](https://github.com/tiann/KernelSU/tree/main/userspace/meta-overlayfs)) để cung cấp các sửa đổi systemless cho phân vùng `/system` và các phân vùng khác.\n\n## Hướng dẫn sử dụng\n\nXin hãy xem: [Cách cài đặt](installation)\n\n## Cách để build\n\n[Cách để build](how-to-build)\n\n## Thảo luận\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n"
  },
  {
    "path": "website/docs/vi_VN/index.md",
    "content": "---\nlayout: home\ntitle: Giải pháp root dựa trên kernel dành cho Android\n\nhero:\n  name: KernelSU\n  text: Giải pháp root dựa trên kernel dành cho Android\n  tagline: \"\"\n  image:\n    src: /logo.png\n    alt: KernelSU\n  actions:\n    - theme: brand\n      text: Bắt Đầu\n      link: /guide/what-is-kernelsu\n    - theme: alt\n      text: Xem trên GitHub\n      link: https://github.com/tiann/KernelSU\n\nfeatures:\n  - title: Dựa trên Kernel\n    details: KernelSU đang hoạt động ở chế độ kernel Linux, nó có nhiều quyền kiểm soát hơn đối với các ứng dụng userspace.\n  - title: Kiểm soát truy cập bằng whitelist\n    details: Chỉ ứng dụng được cấp quyền root mới có thể truy cập `su`, các ứng dụng khác không thể nhận được su.\n  - title: Quyền root bị hạn chế\n    details: KernelSU cho phép bạn tùy chỉnh uid, gid, group, capabilities và các quy tắc SELinux của su. Giới hạn sức mạnh của root.\n  - title: Hệ thống Metamodule\n    details: Cơ sở hạ tầng module có thể cắm cho phép sửa đổi /system theo cách systemless. Cài đặt metamodule như meta-overlayfs để bật tính năng mount module.\n\n"
  },
  {
    "path": "website/docs/zh_CN/guide/app-profile.md",
    "content": "# App Profile\n\nApp Profile 是 KernelSU 提供的一种针对各种应用自定义其使用配置的机制。\n\n对授予了 root 权限（也即可以使用 `su`）的应用来说，App Profile 也可以称之为 Root Profile，它可以自定义 `su` 的 `uid`, `gid`, `groups`, `capabilities` 以及 `SELinux` 规则，从而限制 root 用户的权限；比如可以针对防火墙应用仅授予网络权限，而不授予文件访问权限，针对冻结类应用仅授予 shell 权限而不是直接给 root；通过最小化权限原则**把权力关进笼子里**。\n\n对于没有被授予 root 权限的普通应用，App Profile 可以控制内核以及模块系统对此应用的行为；比如是否需要针对此应用卸载模块造成的修改等。内核和模块系统可以通过此配置决定是否要做一些类似“隐藏痕迹”类的操作。\n\n## Root Profile\n\n### UID、GID 和 groups\n\nLinux 系统中有用户和组两个概念。每个用户都有一个用户 ID(UID)，一个用户可以属于多个组，每个组也有组 ID(GID)。该 ID 用于识别系统的用户并确定用户可以访问哪些系统资源。\n\nUID 为 0 的用户被称之为 root 用户，GID 为 0 的组被称之为 root 组；root 用户组通常拥有系统的最高权限。\n\n对于 Android 系统来说，每一个 App 都是一个单独的用户（不考虑 share uid 的情况），拥有一个唯一的 UID。比如 `0` 是 root 用户，`1000` 是 `system`，`2000` 是 ADB shell，10000-19999 的是普通用户。\n\n:::info\n此处的 UID 跟 Android 系统的多用户，或者说工作资料（Work Profile），不是一个概念。工作资料实际上是对 UID 进行分片实现的，比如 10000-19999 是主用户，110000-119999 是工作资料；他们中的任何一个普通应用都拥有自己独有的 UID。\n:::\n\n每一个 App 可以有若干个组，GID 是其主要的组，通常与 UID 一致；其他的组被称之为补充组 (groups)。某些权限是通过组控制的，比如网络访问，蓝牙等。\n\n例如，如果我们在 ADB shell 中执行 `id` 命令，会得到如下输出：\n\n```sh\noriole:/ $ id\nuid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),1078(ext_data_rw),1079(ext_obb_rw),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid),3012(readtracefs) context=u:r:shell:s0\n```\n\n其中，UID 为 `2000`，GID 也即主要组 ID 也为 `2000`；除此之外它还在很多补充组里面，例如 `inet` 组代表可以创建 `AF_INET` 和 `AF_INET6` 的 socket（访问网络），`sdcard_rw` 代表可以读写 sdcard 等。\n\nKernelSU 的 Root Profile 可以自定义执行 `su` 后 root 进程的 UID, GID 和 groups。例如，你可以设置某个 root 应用的 Root Profile 其 UID 为 `2000`，这意味着此应用在使用 `su` 的时候，它的实际权限是 ADB Shell 级别；你可以去掉 groups 中的 `inet`，这样这个 `su` 就无法访问网络。\n\n:::tip 注意\nApp Profile 仅仅是控制 root 应用使用 `su` 后的权限，它并非控制 App 本身的权限！如果 App 本身申请了网络访问权限，那么它即使不使用 `su` 也可以访问网络；为 `su` 去掉 `inet` 组仅仅是让 `su` 无法访问网络。\n:::\n\n与应用通过 `su` 主动切换用户或者组不同，Root Profile 是在内核中强制实施的，不依赖 root 应用的自觉行为，`su` 权限的授予完全取决于用户而非开发者。\n\n### Capabilities\n\nCapabilities 是 Linux 的一种分权机制。\n\n传统的 UNIX 系统为了执行权限检查，将进程分为两类：特权进程（其有效用户 ID 为 0，称为超级用户或 root）和非特权进程（其有效 UID 为非零）。特权进程会绕过所有内核权限检查，而非特权进程则根据其凭据（通常是有效 UID、有效 GID 和补充组列表）进行完整的权限检查。\n\n从 Linux 2.2 开始，Linux 将传统上与超级用户关联的特权分解为独立的单元，称为 Capabilities（有的也翻译为“权能”），它们可以独立启用和禁用。\n\n每一个 Capability 代表一个或者一类权限。比如 `CAP_DAC_READ_SEARCH` 就代表是否有能力绕过文件读取权限检查和目录读取和执行权限检查。如果一个有效 UID 为 `0` 的用户（root 用户）没有 `CAP_DAC_READ_SEARCH` 或者更高 Capalities，这意味着即使它是 root 也不能随意读取文件。\n\nKernelSU 的 Root Profile 可以自定义执行 `su` 后 root 进程的 Capabilities，从而实现只授予“部分 root 权限”。与上面介绍的 UID, GID 不同，某些 root 应用就是需要 `su` 后 UID 是 `0`，此时我们可以通过限制这个 UID 为 `0` 的 root 用户的 Capabilities，就可以限制它能够执行的操作。\n\n:::tip 强烈建议\nLinux 系统关于 Capability 的 [官方文档](https://man7.org/linux/man-pages/man7/capabilities.7.html)，解释了每一项 Capability 所代表的能力，写的非常详细，如果你想要自定义 Capabilities，请务必先阅读此文档。\n:::\n\n### SELinux\n\nSELinux 是一种强大的强制性权限访问控制（MAC）机制。它按照**默认拒绝**的原则运行：任何未经明确允许的行为都会被拒绝。\n\nSELinux 可按两种全局模式运行：\n\n1. 宽容模式：权限拒绝事件会被记录下来，但不会被强制执行。\n2. 强制模式：权限拒绝事件会被记录下来**并**强制执行。\n\n:::warning 警告\n现代的 Android 系统极度依赖 SELinux 来保障整个系统的安全性，我们强烈建议您不要使用任何以“宽容模式”运行的自定义系统，因为那样与裸奔没什么区别。\n:::\n\nSELinux 的完整概念比较复杂，我们这里不打算讲解它的具体工作方式，建议你先通过以下资料来了解其工作原理：\n\n1. [wikipedia](https://en.wikipedia.org/wiki/Security-Enhanced_Linux)\n2. [Redhat: what-is-selinux](https://www.redhat.com/en/topics/linux/what-is-selinux)\n3. [ArchLinux: SELinux](https://wiki.archlinux.org/title/SELinux)\n\nKernelSU 的 Root Profile 可以自定义执行 `su` 后 root 进程的 SELinux context，并且可以针对这个 context 设置特定的访问控制规则，从而更加精细化地控制 root 权限。\n\n通常情况下，应用执行 `su` 后，会将进程切换到一个 **不受任何限制** 的 SELinux 域，比如 `u:r:su:s0`，通过 Root Profile，我们可以将它切换到一个自定义的域，比如 `u:r:app1:s0`，然后为这个域制定一系列规则：\n\n```sh\ntype app1\nenforce app1\ntypeattribute app1 mlstrustedsubject\nallow app1 * * *\n```\n\n注意：此处的 `allow app1 * * *` 仅仅作为演示方便而使用，实际过程中不应使用这个规则，因为它跟 permissive 区别不大。\n\n### 逃逸\n\n如果 Root Profile 的配置不合理，那么可能会发生逃逸的情况：Root Profile 的限制会意外失效。\n\n比如，如果你为 ADB shell 用户设置允许 root 权限（这是相当常见的情况）；然后你给某个普通应用允许 root 权限，但是配置它的 root profile 中的 UID 为 2000（ADB shell 用户的 UID）；那么此时，这个 App 可以通过执行两次 `su` 来获得完整的 root 权限：\n\n1. 第一次执行 `su`，由于 App Profile 强制生效，会正常切换到 UID 为 `2000(adb shell)` 而非 `0(root)`。\n2. 第二次执行 `su`，由于此时它 UID 是 `2000`，而你给 `2000(adb shell)` 配置了允许 root，它会获得完整的 root 权限！\n\n:::warning 注意\n这是完全符合预期的行为，并非 BUG！因此我们建议：\n\n如果你的确需要给 adb 授予 root 权限（比如你是开发者），那么不建议你在配置 Root Profile 的时候将 UID 改成 `2000`，用 `1000(system)` 会更好。\n:::\n\n## Non Root Profile\n\n### 卸载模块\n\nKernelSU 提供了一种 systemless 的方式来修改系统分区，这是通过挂载 overlayfs 来实现的。但有些情况下，App 可能会对这种行为比较敏感；因此，我们可以通过设置“卸载模块”来卸载挂载在这些 App 上的模块。\n\n另外，KernelSU 管理器的设置界面还提供了一个“默认卸载模块”的开关，这个开关默认情况下是**开启**的，这意味着**如果不对 App 做额外的设置**，默认情况下 KernelSU 或者某些模块会对此 App 执行卸载操作。当然，如果你不喜欢这个设置或者这个设置会影响某些 App，可以有如下选择：\n\n1. 保持“默认卸载模块”的开关，然后针对不需要“卸载模块”的 App 进行单独的设置，在 App Profile 中关闭“卸载模块”；（相当于“白名单”）。\n2. 关闭“默认卸载模块”的开关，然后针对需要“卸载模块”的 App 进行单独的设置，在 App Profile 中开启“卸载模块”；（相当于“黑名单”）。\n\n:::info\nKernelSU 在 5.10 及以上内核上，内核会执行“卸载模块”的操作；但在 5.10 以下的设备上，这个开关仅仅是一个“配置项”，KernelSU 本身不会做任何动作，一些模块（如 Zygisksu 会通过这个模块决定是否需要卸载）\n:::\n"
  },
  {
    "path": "website/docs/zh_CN/guide/difference-with-magisk.md",
    "content": "# KernelSU 模块与 Magisk 的差异 {#title}\n\n虽然 KernelSU 模块与 Magisk 模块有很多相似之处，但由于它们的实现机制完全不同，因此不可避免地会有一些差异；如果你希望你的模块能同时在 Magisk 与 KernelSU 中运行，那么你必须了解这些差异。\n\n## 相同之处 {#similarities}\n\n- 模块文件格式: 都以 zip 的方式组织模块，并且模块的格式几乎相同\n- 模块安装目录: 都在 `/data/adb/modules`\n- systemless: 都支持通过模块的形式以 systemless 修改 /system\n- `post-fs-data.sh`: 执行时机完全一致，语义也完全一致\n- `service.sh`: 执行时机完全一致，语义也完全一致\n- `system.prop`: 完全相同\n- `sepolicy.rule`: 完全相同\n- BusyBox：脚本都在 BusyBox 中以“独立模式”运行\n\n## 不同之处 {#differences}\n\n在了解不同之处之前，你需要知道如何区分你的模块是运行在 KernelSU 还是运行在 Magisk 之中；在所有你可以运行模块脚本的地方（`customize.sh`, `post-fs-data.sh`, `service.sh`)，你都可以通过环境变量`KSU` 来区分，在 KernelSU 中，这个环境变量将被设置为 `true`。\n\n以下是一些不同之处：\n\n1. KernelSU 的模块不支持在 Recovery 中安装。\n2. KernelSU 的模块没有内置的 Zygisk 支持（但你可以通过 [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) 来使用 Zygisk 模块）。\n3. **模块挂载架构**：KernelSU 使用 [metamodule 系统](metamodule.md)，将挂载委托给可插拔的 metamodule（例如 `meta-overlayfs`），而 Magisk 将挂载内置在其核心中。KernelSU 需要安装 metamodule 才能启用模块挂载。\n4. KernelSU 模块替换或者删除文件与 Magisk 完全不同。KernelSU 不支持 `.replace` 方式，相反，你需要通过 `mknod filename c 0 0` 创建同名文件夹来删除对应文件。\n5. BusyBox 的目录不同；KernelSU 内置的 BusyBox 在 `/data/adb/ksu/bin/busybox` 而 Magisk 在 `/data/adb/magisk/busybox`；**注意此为 KernelSU 内部行为，未来可能会更改！**\n6. KernelSU 不支持 `.replace` 文件；但 KernelSU 支持 `REPLACE` 和 `REMOVE` 变量。\n7. KernelSU 新增了一种脚本 `boot-completed.sh`，以便在 Android 系统启动后运行某些任务。\n8. KernelSU 新增了一种脚本 `post-mount.sh`，以便在模块挂载完成后运行某些任务。\n"
  },
  {
    "path": "website/docs/zh_CN/guide/faq.md",
    "content": "# 常见问题\n\n## KernelSU 是否支持我的设备？\n\n首先，您的设备应该能够解锁 bootloader。 如果不能，则不支持。\n\n然后在你的设备上安装 KernelSU 管理器并打开它，如果它显示 `不支持` ，那么你的设备没有官方支持的开箱即用的 boot image；但你可以自己编译内核集成 KernelSU 进而使用它。\n\n## KernelSU 是否需要解锁 Bootloader？\n\n当然需要。\n\n## KernelSU 是否支持模块？\n\n支持。但是，如果您的模块需要修改 `/system` 文件，则需要安装 [metamodule](metamodule.md)（例如 `meta-overlayfs`）。其他模块功能无需 metamodule。请查阅 [模块](module.md)。\n\n## KernelSU 是否支持 Xposed？\n\n支持。LSPosed 可以在 [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) 的支持下正常运行。\n\n## KernelSU 支持 Zygisk 吗？\n\nKernelSU 本体不支持 Zygisk，但是你可以用 [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) 来使用 Zygisk 模块。\n\n## KernelSU 与 Magisk 兼容吗？\n\nKernelSU 的模块系统与 Magisk 的 magic mount 有冲突，如果 KernelSU 中启用了任何模块，那么整个 Magisk 将无法工作。\n\n但是如果你只使用 KernelSU 的 `su`，那么它会和 Magisk 一起工作：KernelSU 修改 `kernel` 、 Magisk 修改 `ramdisk`，它们可以一起工作。\n\n## KernelSU 会替代 Magisk 吗？\n\n我们不这么认为，这也不是我们的目标。Magisk 对于用户空间 root 解决方案来说已经足够好了，它会存活很久。KernelSU 的目标是为用户提供内核接口，而不是替代 Magisk。\n\n## KernelSU 可以支持非 GKI 设备吗？\n\n可以。但是你应该下载内核源代码并将 KernelSU 集成到源代码树中并自己编译内核。\n\n## KernelSU 支持 Android 12 以下的设备吗？\n\n影响 KernelSU 兼容性的是设备内核的版本，它与设备的 Android 版本没有直接的关系。唯一有关联的是：**出厂** Android 12 的设备，一定是 5.10 或更高的内核（GKI 设备）；因此结论如下：\n\n1. 出厂 Android 12 的设备必定是支持的（GKI 设备）\n2. 旧版本内核的设备（即使是 Android 12，也可能是旧内核）是兼容的（你需要自己编译内核）\n\n## KernelSU 可以支持旧内核吗？\n\n可以，目前最低支持到 4.14；更低的版本你需要手动移植它，欢迎 PR ！\n\n## 如何为旧内核集成 KernelSU？\n\n参考[教程](how-to-integrate-for-non-gki)\n\n## 为什么我手机系统是 Android 13，但内核版本却是 \"android12-5.10\"？\n\n内核版本与 Android 版本无关，如果你需要刷入 KernelSU，请永远使用**内核版本**而非 Android 版本，如果你为 \"android12-5.10\" 的设备刷入 Android 13 的内核，等待你的将是 bootloop.\n\n## 我是 GKI1.0, 能用 KernelSU 吗？\n\nGKI1 跟 GKI2 完全是两个东西，所以你需要自行编译内核。\n\n## 如何把 `/system` 变成挂载为可读写？\n\n我们不建议你直接修改系统分区，你应该使用[模块功能](module.md) 来做修改；如果你执意要这么做，可以看看 [magisk_overlayfs](https://github.com/HuskyDG/magic_overlayfs)\n\n## KernelSU 能修改 hosts 吗，我如何使用 AdAway？\n\n当然可以。但这个功能 KernelSU 没有内置，你可以安装这个 [systemless-hosts](https://github.com/symbuzzer/systemless-hosts-KernelSU-module)\n\n## 为什么全新安装后模块不工作？\n\n如果您的模块需要修改 `/system` 文件，您需要安装 [metamodule](metamodule.md) 来挂载 `system` 目录。其他模块功能（脚本、sepolicy、system.prop）无需 metamodule 即可工作。\n\n**解决方案**：参阅 [Metamodule 指南](metamodule.md) 获取安装说明。\n\n## 什么是 metamodule，为什么需要它？\n\nMetamodule 是一个特殊模块，为挂载常规模块提供基础设施。请参阅 [Metamodule 指南](metamodule.md) 获取完整说明。\n"
  },
  {
    "path": "website/docs/zh_CN/guide/hidden-features.md",
    "content": "# 隐藏功能\n\n## ksurc\n\n默认情况下，`/system/bin/sh` 会加载 `/system/etc/mkshrc`。\n\n可以通过创建 `/data/adb/ksu/.ksurc` 文件来让 su 加载该文件而不是 `/system/etc/mkshrc`。"
  },
  {
    "path": "website/docs/zh_CN/guide/how-to-build.md",
    "content": "# 如何构建 KernelSU？\n\n::: warning\n该文档仅供存档参考，不再维护更新。\n自 KernelSU v3.0 版本之后，为了更快的迭代和构建速度，我们放弃了对 GKI 镜像模式的官方支持。推荐使用 `Ylarod/ddk` 构建 LKM 使用。\n:::\n\n首先，您应该阅读内核构建的 Android 官方文档：\n\n1. [构建内核](https://source.android.com/docs/setup/build/building-kernels)\n2. [通用内核映像 (GKI) 发布构建](https://source.android.com/docs/core/architecture/kernel/gki-release-builds)\n\n::: warning\n本文档适用于 GKI 设备，如果你是旧内核，请参考[如何为非GKI设备集成 KernelSU](how-to-integrate-for-non-gki)\n:::\n\n## 构建内核\n\n### 同步内核源码\n\n```sh\nrepo init -u https://android.googlesource.com/kernel/manifest\nmv <kernel_manifest.xml> .repo/manifests\nrepo init -m manifest.xml\nrepo sync\n```\n\n`<kernel_manifest.xml>` 是一个可以唯一确定构建的清单文件，您可以使用该清单进行可重新预测的构建。 您应该从 [通用内核映像 (GKI) 发布构建](https://source.android.com/docs/core/architecture/kernel/gki-release-builds) 下载清单文件 \n\n### 构建\n\n请先查看 [官方文档](https://source.android.com/docs/setup/build/building-kernels)。\n\n例如，我们需要构建 aarch64 内核镜像：\n\n```sh\nLTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh\n```\n\n不要忘记添加 `LTO=thin`, 否则，如果您的计算机内存小于 24GB，构建可能会失败.\n\n从 Android 13 开始，内核由 `bazel` 构建:\n\n```sh\ntools/bazel build --config=fast //common:kernel_aarch64_dist\n```\n\n## 使用 KernelSU 构建内核\n\n如果您可以成功构建内核，那么构建 KernelSU 就很容易，根据自己的需求在内核源代码根目录中运行以下任一命令：\n\n::: code-group\n\n```sh[最新 tag(稳定版本)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\n```sh[main 分支(开发版本)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\n```\n\n```sh[指定 tag(比如 v0.5.2)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\n```\n\n:::\n\n然后重建内核，您将获得带有 KernelSU 的内核映像！\n"
  },
  {
    "path": "website/docs/zh_CN/guide/how-to-integrate-for-non-gki.md",
    "content": "# 如何为非 GKI 内核集成 KernelSU {#introduction}\n\n::: warning\n该文档仅供存档参考，不再维护更新。\n自 KernelSU v1.0 版本之后，我们放弃了对非 GKI 设备的官方支持。\n:::\n\nKernelSU 可以被集成到非 GKI 内核中，现在它最低支持到内核 4.14 版本；理论上也可以支持更低的版本。\n\n由于非 GKI 内核的碎片化极其严重，因此通常没有统一的方法来编译它，所以我们也无法为非 GKI 设备提供 boot 镜像。但你完全可以自己集成 KernelSU 然后编译内核使用。\n\n首先，你必须有能力从你设备的内核源码编译出一个可以开机并且能正常使用的内核，如果内核不开源，这通常难以做到。\n\n如果你已经做好了上述准备，那有两个方法来集成 KernelSU 到你的内核之中。\n\n1. 借助 `kprobe` 自动集成\n2. 手动修改内核源码\n\n## 使用 kprobe 集成 {#using-kprobes}\n\nKernelSU 使用 kprobe 机制来做内核的相关 hook，如果 *kprobe* 可以在你编译的内核中正常运行，那么推荐用这个方法来集成。\n\n首先，把 KernelSU 添加到你的内核源码树，在内核的根目录执行以下命令：\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.9.5\n```\n\n:::info\n[KernelSU 1.0 及更高版本已经不再支持非 GKI 内核](https://github.com/tiann/KernelSU/issues/1705)，最后的支持版本为 `v0.9.5`，请注意使用正确的版本。\n:::\n\n然后，你需要检查你的内核是否开启了 *kprobe* 相关的配置，如果没有开启，需要添加以下配置：\n\n```\nCONFIG_KPROBES=y\nCONFIG_HAVE_KPROBES=y\nCONFIG_KPROBE_EVENTS=y\n```\n\n最后，重新编译你的内核即可。\n\n如果你发现 KPROBES 仍未生效，很有可能是因为它的依赖项`CONFIG_MODULES`没有被启用（如果还是未生效请键入`make menuconfig`搜索 KPROBES 的其它依赖并启用）\n\n如果你在集成 KernelSU 之后手机无法启动，那么很可能你的内核中 **kprobe 工作不正常**，你需要修复这个 bug 或者用第二种方法。\n\n:::tip 如何验证是否是 kprobe 的问题？\n\n注释掉 `KernelSU/kernel/ksu.c` 中 `ksu_sucompat_init()` 和 `ksu_ksud_init()`，如果正常开机，那么就是 kprobe 的问题；或者你可以手动尝试使用 kprobe 功能，如果不正常，手机会直接重启。\n:::\n\n## 手动修改内核源码 {#modify-kernel-source-code}\n\n如果 kprobe 工作不正常（通常是上游的 bug 或者内核版本过低），那你可以尝试这种方法：\n\n首先，把 KernelSU 添加到你的内核源码树，在内核的根目录执行以下命令：\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.9.5\n```\n\n请注意，某些设备的 defconfig 文件可能在`arch/arm64/configs/设备代号_defconfig`或位于`arch/arm64/configs/vendor/设备代号_defconfig`。在您的 defconfig 文件中，将`CONFIG_KSU`设置为`y`以启用 KernelSU，或设置为`n`以禁用。比如在某个 defconfig 中：\n`arch/arm64/configs/...` \n```sh\n+# KernelSU\n+CONFIG_KSU=y\n```\n\n然后，将 KernelSU 调用添加到内核源代码中，这里有几个补丁可以参考：\n\n::: code-group\n\n```diff[exec.c]\ndiff --git a/fs/exec.c b/fs/exec.c\nindex ac59664eaecf..bdd585e1d2cc 100644\n--- a/fs/exec.c\n+++ b/fs/exec.c\n@@ -1890,11 +1890,14 @@ static int __do_execve_file(int fd, struct filename *filename,\n \treturn retval;\n }\n\n+#ifdef CONFIG_KSU\n+extern bool ksu_execveat_hook __read_mostly;\n+extern int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv,\n+\t\t\tvoid *envp, int *flags);\n+extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,\n+\t\t\t\t void *argv, void *envp, int *flags);\n+#endif\n static int do_execveat_common(int fd, struct filename *filename,\n \t\t\t      struct user_arg_ptr argv,\n \t\t\t      struct user_arg_ptr envp,\n \t\t\t      int flags)\n {\n+   #ifdef CONFIG_KSU\n+\tif (unlikely(ksu_execveat_hook))\n+\t\tksu_handle_execveat(&fd, &filename, &argv, &envp, &flags);\n+\telse\n+\t\tksu_handle_execveat_sucompat(&fd, &filename, &argv, &envp, &flags);\n+   #endif\n \treturn __do_execve_file(fd, filename, argv, envp, flags, NULL);\n }\n```\n```diff[open.c]\ndiff --git a/fs/open.c b/fs/open.c\nindex 05036d819197..965b84d486b8 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -348,6 +348,8 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn ksys_fallocate(fd, mode, offset, len);\n }\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t int *flags);\n+#endif\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -355,6 +357,7 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n  */\n long do_faccessat(int dfd, const char __user *filename, int mode)\n {\n \tconst struct cred *old_cred;\n \tstruct cred *override_cred;\n \tstruct path path;\n \tstruct inode *inode;\n \tstruct vfsmount *mnt;\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n+   #ifdef CONFIG_KSU\n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n+   #endif\n \n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n```diff[read_write.c]\ndiff --git a/fs/read_write.c b/fs/read_write.c\nindex 650fc7e0f3a6..55be193913b6 100644\n--- a/fs/read_write.c\n+++ b/fs/read_write.c\n@@ -434,10 +434,14 @@ ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)\n }\n EXPORT_SYMBOL(kernel_read);\n\n+#ifdef CONFIG_KSU\n+extern bool ksu_vfs_read_hook __read_mostly;\n+extern int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,\n+\t\t\tsize_t *count_ptr, loff_t **pos);\n+#endif\n ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)\n {\n \tssize_t ret;\n+   #ifdef CONFIG_KSU \n+\tif (unlikely(ksu_vfs_read_hook))\n+\t\tksu_handle_vfs_read(&file, &buf, &count, &pos);\n+   #endif\n+\n \tif (!(file->f_mode & FMODE_READ))\n \t\treturn -EBADF;\n \tif (!(file->f_mode & FMODE_CAN_READ))\n```\n```diff[stat.c]\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 376543199b5a..82adcef03ecc 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -148,6 +148,8 @@ int vfs_statx_fd(unsigned int fd, struct kstat *stat,\n }\n EXPORT_SYMBOL(vfs_statx_fd);\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+#endif\n+\n /**\n  * vfs_statx - Get basic and extra attributes by filename\n  * @dfd: A file descriptor representing the base dir for a relative filename\n@@ -170,6 +172,7 @@ int vfs_statx(int dfd, const char __user *filename, int flags,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_AUTOMOUNT;\n\n+   #ifdef CONFIG_KSU\n+\tksu_handle_stat(&dfd, &filename, &flags);\n+   #endif\n \tif ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t       AT_EMPTY_PATH | KSTAT_QUERY_FLAGS)) != 0)\n \t\treturn -EINVAL;\n```\n:::\n\n主要是要改四个地方：\n\n1. do_faccessat，通常位于 `fs/open.c`\n2. do_execveat_common，通常位于 `fs/exec.c`\n3. vfs_read，通常位于 `fs/read_write.c`\n4. vfs_statx，通常位于 `fs/stat.c`\n\n如果你的内核没有 `vfs_statx`, 使用 `vfs_fstatat` 来代替它：\n```diff\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 068fdbcc9e26..5348b7bb9db2 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -87,6 +87,8 @@ int vfs_fstat(unsigned int fd, struct kstat *stat)\n }\n EXPORT_SYMBOL(vfs_fstat);\n \n+#ifdef CONFIG_KSU \n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+#endif\n+\n int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \t\tint flag)\n {\n@@ -94,6 +96,8 @@ int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = 0;\n+   #ifdef CONFIG_KSU \n+\tksu_handle_stat(&dfd, &filename, &flag);\n+   #endif\n+\n \tif ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t      AT_EMPTY_PATH)) != 0)\n \t\tgoto out;\n```\n\n对于早于 4.17 的内核，如果没有 `do_faccessat`，可以直接找到 `faccessat` 系统调用的定义然后修改：\n\n```diff\ndiff --git a/fs/open.c b/fs/open.c\nindex 2ff887661237..e758d7db7663 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -355,6 +355,9 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn error;\n }\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t        int *flags);\n+#endif\n+\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -370,6 +373,8 @@ SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n+   #ifdef CONFIG_KSU\n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n+   #endif\n+\n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n\n### 安全模式 \n\n要使用 KernelSU 内置的安全模式，你还需要修改 `drivers/input/input.c` 中的 `input_handle_event` 方法：\n\n:::tip\n强烈建议开启此功能，对用户救砖会非常有帮助！\n:::\n\n:::info 莫名其妙进入安全模式？\n如果你采用手动集成的方式，并且没有禁用`CONFIG_KPROBES`，那么用户在开机之后按音量下，也可能触发安全模式！因此如果使用手动集成，你需要关闭 `CONFIG_KPROBES`！\n:::\n\n```diff\ndiff --git a/drivers/input/input.c b/drivers/input/input.c\nindex 45306f9ef247..815091ebfca4 100755\n--- a/drivers/input/input.c\n+++ b/drivers/input/input.c\n@@ -367,10 +367,13 @@ static int input_get_disposition(struct input_dev *dev,\n \treturn disposition;\n }\n\n+#ifdef CONFIG_KSU\n+extern bool ksu_input_hook __read_mostly;\n+extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value);\n+#endif\n+\n static void input_handle_event(struct input_dev *dev,\n \t\t\t       unsigned int type, unsigned int code, int value)\n {\n\tint disposition = input_get_disposition(dev, type, code, &value);\n+   #ifdef CONFIG_KSU\n+\tif (unlikely(ksu_input_hook))\n+\t\tksu_handle_input_handle_event(&type, &code, &value);\n+   #endif\n \n \tif (disposition != INPUT_IGNORE_EVENT && type != EV_SYN)\n \t\tadd_input_randomness(type, code, value);\n```\n\n### pm 命令执行失败？\n\n你需要同时修改 `fs/devpts/inode.c`，补丁如下：\n\n```diff\ndiff --git a/fs/devpts/inode.c b/fs/devpts/inode.c\nindex 32f6f1c68..d69d8eca2 100644\n--- a/fs/devpts/inode.c\n+++ b/fs/devpts/inode.c\n@@ -602,6 +602,8 @@ struct dentry *devpts_pty_new(struct pts_fs_info *fsi, int index, void *priv)\n        return dentry;\n }\n\n+extern int ksu_handle_devpts(struct inode*);\n+\n /**\n  * devpts_get_priv -- get private data for a slave\n  * @pts_inode: inode of the slave\n@@ -610,6 +612,7 @@ struct dentry *devpts_pty_new(struct pts_fs_info *fsi, int index, void *priv)\n  */\n void *devpts_get_priv(struct dentry *dentry)\n {\n+       ksu_handle_devpts(dentry->d_inode);\n        if (dentry->d_sb->s_magic != DEVPTS_SUPER_MAGIC)\n                return NULL;\n        return dentry->d_fsdata;\n```\n\n### path_umount {#how-to-backport-path-umount}\n\n你可以通过从 K5.9 向旧版本移植 `path_umount`，在 GKI 之前的内核上获得卸载模块的功能。你可以通过以下补丁作为参考:\n\n```diff\n--- a/fs/namespace.c\n+++ b/fs/namespace.c\n@@ -1739,6 +1739,39 @@ static inline bool may_mandlock(void)\n }\n #endif\n\n+static int can_umount(const struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\n+\tif (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW))\n+\t\treturn -EINVAL;\n+\tif (!may_mount())\n+\t\treturn -EPERM;\n+\tif (path->dentry != path->mnt->mnt_root)\n+\t\treturn -EINVAL;\n+\tif (!check_mnt(mnt))\n+\t\treturn -EINVAL;\n+\tif (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */\n+\t\treturn -EINVAL;\n+\tif (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN))\n+\t\treturn -EPERM;\n+\treturn 0;\n+}\n+\n+int path_umount(struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\tint ret;\n+\n+\tret = can_umount(path, flags);\n+\tif (!ret)\n+\t\tret = do_umount(mnt, flags);\n+\n+\t/* we mustn't call path_put() as that would clear mnt_expiry_mark */\n+\tdput(path->dentry);\n+\tmntput_no_expire(mnt);\n+\treturn ret;\n+}\n /*\n  * Now umount can handle mount points as well as block devices.\n  * This is important for filesystems which use unnamed block devices.\n```\n\n改完之后重新编译内核即可。\n"
  },
  {
    "path": "website/docs/zh_CN/guide/installation.md",
    "content": "# 安装 {#title}\n\n## 检查您的设备是否被支持 {#check-if-supported}\n\n从 [GitHub Releases](https://github.com/tiann/KernelSU/releases) 下载 KernelSU 管理器应用，然后将应用程序安装到设备并打开：\n\n- 如果应用程序显示 “不支持”，则表示您的设备不支持 KernelSU，你需要自己编译设备的内核才能使用，KernelSU 官方不会也永远不会为你提供一个可以刷写的 boot 镜像。\n- 如果应用程序显示 “未安装”，那么 KernelSU 支持您的设备；可以进行下一步操作。\n\n:::info\n对于显示“不支持”的设备，这里有一个[非官方支持设备列表](unofficially-support-devices.md)，你可以用这个列表里面的内核自行编译。\n:::\n\n## 备份你的 boot.img {#backup-boot-image}\n\n在进行刷机操作之前，你必须先备份好自己的原厂 boot.img。如果你后续刷机出现了任何问题，你都可以通过使用 fastboot 刷回原厂 boot 来恢复系统。\n\n::: warning\n任何刷机操作都是有风险的，请务必做好这一步再进行下一步操作！！必要时你还可以备份你手机的所有数据。\n:::\n\n## 必备知识 {#acknowage}\n\n### ADB 和 fastboot {#adb-and-fastboot}\n\n此教程默认你会使用 ADB 和 fastboot 工具，如果你没有了解过，建议使用搜索引擎先学习相关知识。\n\n### KMI\n\nKMI 全称 Kernel Module Interface，相同 KMI 的内核版本是**兼容的** 这也是 GKI 中“通用”的含义所在；反之，如果 KMI 不同，那么这些内核之间无法互相兼容，刷入与你设备 KMI 不同的内核镜像可能会导致死机。\n\n具体来说，对 GKI 的设备，其内核版本格式应该如下：\n\n```txt\nKernelRelease :=\nVersion.PatchLevel.SubLevel-AndroidRelease-KmiGeneration-suffix\nw      .x         .y       -zzz           -k            -something\n```\n\n其中，`w.x-zzz-k` 为 KMI 版本。例如，一个设备内核版本为`5.10.101-android12-9-g30979850fc20`，那么它的 KMI 为 `5.10-android12-9`；理论上刷入其他这个 KMI 的内核也能正常开机。\n\n::: tip\n请注意，内核版本中的 SubLevel 不属于 KMI 的范畴！也就是说 `5.10.101-android12-9-g30979850fc20` 与 `5.10.137-android12-9-g30979850fc20` 的 KMI 相同！\n:::\n\n### 安全补丁级别 {#security-patch-level}\n\n新的 Android 设备上可能采取了防回滚机制，它不允许刷入一个安全补丁更旧的内核。比如，如果你的设备内核是 `5.10.101-android12-9-g30979850fc20`，它的安全补丁为 `2023-11`；即使你刷入与内核 KMI 一致的内核，如果安全补丁级别比 `2023-11` 要老（例如`2023-06`），那么很可能会无法开机。\n\n因此，在保持 KMI 一致的情况下，优先采用安全补丁级别更新的内核。\n\n### 内核版本与 Android 版本 {#kernel-version-vs-android-version}\n\n请注意：**内核版本与 Android 版本并不一定相同！**\n\n如果您发现您的内核版本是 `android12-5.10.101`，然而你 Android 系统的版本为 Android 13 或者其他；请不要觉得奇怪，因为 Android 系统的版本与 Linux 内核的版本号不一定是一致的；Linux 内核的版本号一般与**设备出厂的时候自带的 Android 系统的版本一致**，如果后续 Android 系统升级，内核版本一般不会发生变化。如果你需要刷机，**请以内核版本为准！！**\n\n## 安装介绍 {#installationintroduction}\n\n自 `0.9.0` 版本以后，在 GKI 设备中，KernelSU 支持两种运行模式：\n\n1. `GKI`：使用**通用内核镜像**（GKI）替换掉设备原有的内核。\n2. `LKM`：使用**可加载内核模块**（LKM）的方式加载到设备内核中，不会替换掉设备原有的内核。\n\n这两种方式适用于不同的场景，你可以根据自己的需求选择。\n\n### GKI 模式 {#gki-mode}\n\nGKI 模式会替换掉设备原有的内核，使用 KernelSU 提供的通用内核镜像。GKI 模式的优点是：\n\n1. 通用型强，适用于大多数设备；比如三星开启了 KNOX 的设备，LKM 模式无法运作。还有一些冷门的魔改设备，也只能使用 GKI 模式；\n2. 不依赖官方固件即可使用；不需要等待官方固件更新，只要 KMI 一致，就可以使用；\n\n### LKM 模式 {#lkm-mode}\n\nLKM 模式不会替换掉设备原有的内核，而是使用可加载内核模块的方式加载到设备内核中。LKM 模式的优点是：\n\n1. 不会替换掉设备原有的内核；如果你对设备原有的内核有特殊需求，或者你希望在使用第三方内核的同时使用 KernelSU，可以使用 LKM 模式；\n2. 升级和 OTA 较为方便；升级 KernelSU 时，可以直接在管理器里面安装，无需再手动刷写；系统 OTA 后，可以直接安装到第二个槽位，也无需再手动刷写；\n3. 适用于一些特殊场景；比如使用临时 ROOT 权限也可以加载 LKM，由于不需要替换 boot 分区，因此不会触发 avb，不会使设备意外变砖；\n4. LKM 可以被临时卸载；如果你临时想取消 root，可以卸载 LKM，这个过程不需要刷写分区，甚至也不用重启设备；如果你想再次 root，只需要重启设备即可；\n\n:::tip 两种模式共存\n打开管理器后，你可以在首页看到设备当前运行的模式；注意 GKI 模式的优先级高于 LKM，如你既使用 GKI 内核替换掉了原有的内核，又使用 LKM 的方式修补了 GKI 内核，那么 LKM 会被忽略，设备将永远以 GKI 的模式运行。\n:::\n\n### 选哪个？ {#which-one}\n\n如果你的设备是手机，我们建议您优先考虑 LKM 模式；如果你的设备是模拟器、WSA 或者 Waydroid 等，我们建议您优先考虑 GKI 模式。\n\n## LKM 安装\n\n### 获取官方固件\n\n使用 LKM 的模式，需要获取官方固件，然后在官方固件的基础上修补；如果你使用的是第三方内核，可以把第三方内核的 boot.img 作为官方固件。\n\n获取官方固件的方法有很多，如果你的设备支持 `fastboot boot`，那么我们最推荐以及最简单的方法是使用 `fastboot boot` 临时启动 KernelSU 提供的 GKI 内核，然后安装管理器，最后在管理器中直接安装；这种方法不需要你手动下载官方固件，也不需要你手动提取 boot。\n\n如果你的设备不支持 `fastboot boot`，那么你可能需要手动去下载官方固件包，然后从中提取 boot。\n\n与 GKI 模式不同，LKM 模式会修改 `ramdisk`，因此在出厂 Android 13 的设备上，它需要修补的是 `init_boot` 分区而非 `boot` 分区；而 GKI 模式则永远是操作 `boot` 分区。\n\n### 使用管理器\n\n打开管理器，点击右上角的安装图标，会出现若干个选项：\n\n1. 选择并修补一个文件；如果你手机目前没有 root 权限，你可以选择这个选项，然后选择你的官方固件，管理器会自动修补它；你只需要刷入这个修补后的文件，即可永久获取 root 权限；\n2. 直接安装；如果你手机已经 root，你可以选择这个选项，管理器会自动获取你的设备信息，然后自动修补官方固件，然后刷入；你可以考虑使用 `fastboot boot` KernelSU 的 GKI 内核来获取临时 root 安装管理器，然后再使用这个选项；这种方式也是 KernelSU 升级最主要的方式；\n3. 安装到另一个分区；如果你的设备支持 A/B 分区，你可以选择这个选项，管理器会自动修补官方固件，然后安装到另一个分区；这种方式适用于 OTA 后的设备，你可以在 OTA 后直接安装到另一个分区，然后重启设备即可；\n\n### 使用命令行\n\n如果你不想使用管理器，你也可以使用命令行来安装 LKM；KernelSU 提供的 `ksud` 工具可以帮助你快速修补官方固件，然后刷入。\n\n这个工具支持 macOS、Linux 和 Windows，你可以在 [GitHub Release](https://github.com/tiann/KernelSU/releases) 下载对应的版本。\n\n使用方法：`ksud boot-patch` 具体的使用方法你可以查看命令行帮助。\n\n```sh\noriole:/ # ksud boot-patch -h\nPatch boot or init_boot images to apply KernelSU\n\nUsage: ksud boot-patch [OPTIONS]\n\nOptions:\n  -b, --boot <BOOT>              boot image path, if not specified, will try to find the boot image automatically\n  -k, --kernel <KERNEL>          kernel image path to replace\n  -m, --module <MODULE>          LKM module path to replace, if not specified, will use the builtin one\n  -i, --init <INIT>              init to be replaced\n  -u, --ota                      will use another slot when boot image is not specified\n  -f, --flash                    Flash it to boot partition after patch\n  -o, --out <OUT>                output path, if not specified, will use current directory\n      --magiskboot <MAGISKBOOT>  magiskboot path, if not specified, will use builtin one\n      --kmi <KMI>                KMI version, if specified, will use the specified KMI\n  -h, --help                     Print help\n```\n\n需要说明的几个选项：\n\n1. `--magiskboot` 选项可以指定 magiskboot 的路径，如果不指定，ksud 会在环境变量中查找；如果你不知道如何获取 magiskboot，可以查阅[这里](#patch-boot-image)；\n2. `--kmi` 选项可以指定 `KMI` 版本，如果你的设备内核名字没有遵循 KMI 规范，你可以通过这个选项来指定；\n\n最常见的使用方法为：\n\n```sh\nksud boot-patch -b <boot.img> --kmi android13-5.10\n```\n\n## GKI 安装\n\nGKI 的安装方法有如下几种，各自适用于不同的场景，请按需选择：\n\n1. 使用 KernelSU 提供的**通用内核镜像**使用 fastboot 安装\n2. 使用内核刷写 App（如 KernelFlasher）安装\n3. 手动修补 boot.img 然后安装\n4. 使用自定义 Recovery（如 TWRP）安装\n\n## 使用 KernelSU 提供的 boot.img 安装 {#install-by-kernelsu-boot-image}\n\n如果你设备的 `boot.img` 采用常用的压缩格式，那么可以采用 KernelSU 提供的的通用内核镜像直接刷入，它不需要 TWRP 或者自行修补镜像。\n\n### 找到合适的 boot.img {#found-propery-image}\n\nKernelSU 为 GKI 设备提供了通用的 boot.img，您应该将 boot.img 刷写到设备的 boot 分区。\n\n您可以从 [GitHub Release](https://github.com/tiann/KernelSU/releases) 下载 boot.img, 请注意您应该使用正确版本的 boot.img。如果您不知道应该下载哪一个文件，请仔细阅读本文档中关于 [KMI](#kmi) 和[安全补丁级别](#security-patch-level)的描述。\n\n通常情况下，同一个 KMI 和 安全补丁级别下会有三个不同格式的 boot 文件，它们除了内核压缩格式不同之外都一样。请检查您原有 boot.img 的内核压缩格式，您应该使用正确的格式，例如 `lz4`、`gz`；如果是用不正确的压缩格式，刷入 boot 后可能无法开机。\n\n::: info\n1. 您可以通过 magiskboot 来获取你原来 boot 的压缩格式；当然您也可以询问与您机型相同的其他更有经验的童鞋。另外，内核的压缩格式通常不会发生变化，如果您使用某个压缩格式成功开机，后续可优先尝试这个格式。\n2. 小米设备通常使用 `gz` 或者 **不压缩**。\n3. Pixel 设备有些特殊，请查看下面的教程。\n:::\n\n### 将 boot.img 刷入设备 {#flash-boot-image}\n\n使用 `adb` 连接您的设备，然后执行 `adb reboot bootloader` 进入 fastboot 模式，然后使用此命令刷入 KernelSU：\n\n```sh\nfastboot flash boot boot.img\n```\n\n::: info\n如果你的设备支持 `fastboot boot`，可以先使用 `fastboot boot boot.img` 来先尝试使用 boot.img 引导系统，如果出现意外，再重启一次即可开机。\n:::\n\n### 重启 {#reboot}\n\n刷入完成后，您应该重新启动您的设备：\n\n```sh\nfastboot reboot\n```\n\n## 使用内核刷写 App 安装 {#install-by-kernel-flasher}\n\n步骤：\n\n1. 下载 AnyKernel3 的刷机包，如果你不知道下载哪一个，请仔细查阅上述文档中关于 [KMI](#kmi) 和[安全补丁级别](#security-patch-level)的描述；下载错误的刷机包很可能导致无法开机，请注意备份。\n2. 打开内核刷写 App（授予必要的 root 权限），使用提供的 AnyKernel3 刷机包刷入。\n\n这种方法需要内核刷写 App 拥有 root 权限，你可以用如下几种方法实现：\n\n1. 你的设备已经获取了 root 权限，比如你已经安装好了 KernelSU 想升级到最新的版本，又或者你通过其他方法（如 Magisk）获取了 root。\n2. 如果你的手机没有 root，但手机支持 `fastboot boot boot.img` 这种临时启动的方法，你可以用 KernelSU 提供的 GKI 镜像临时启动你的设备，获取临时的 root 权限，然后使用内核刷写器刷入获取永久 root 权限。\n\n\n如果您以前没有使用过内核刷写 App，建议使用以下应用：\n\n1. [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases)\n2. [Franco Kernel Manager](https://play.google.com/store/apps/details?id=com.franco.kernel)\n3. [Ex Kernel Manager](https://play.google.com/store/apps/details?id=flar2.exkernelmanager)\n\n## 手动修补 boot.img {#patch-boot-image}\n\n对于某些设备来说，其 boot.img 格式不那么常见，比如不是 `lz4`, `gz` 和未压缩；最典型的就是 Pixel，它 boot.img 的格式是 `lz4_legacy` 压缩，ramdisk 可能是 `gz` 也可能是 `lz4_legacy` 压缩；此时如果你直接刷入 KernelSU 提供的 boot.img，手机可能无法开机；这时候，你可以通过手动修补 boot.img 来实现。\n\n任何情况下都推荐使用 `magiskboot` 来修补 boot 镜像，有两个方法：\n\n1. [magiskboot](https://github.com/topjohnwu/Magisk/releases)\n2. [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci)\n\nMagisk 官方提供的 `magiskboot` 只能运行在 Android/Linux 设备上，如果你想在 macOS/Windows 上使用 `magiskboot` 可以使用第二个方法。\n\n::: tip\n不再推荐使用 Android-Image-Kitchen，因为它可能没有合理地处理 boot 元数据（比如安全补丁级别），从而导致某些设备上会无法启动。\n:::\n\n### 准备 {#patch-preparation}\n\n1. 获取你手机的原厂 boot.img；你可以通过你手机的线刷包解压后之间获取，如果你是卡刷包，那你也许需要 [payload-dumper-go](https://github.com/ssut/payload-dumper-go)\n2. 下载 KernelSU 提供的与你设备 KMI 版本一致的 AnyKernel3 刷机包；如果您不知道应该下载哪一个文件，请仔细阅读本文档中关于 [KMI](#kmi) 和[安全补丁级别](#security-patch-level)的描述。\n3. 解压缩 AnyKernel3 刷机包，获取其中的 `Image` 文件，此文件为 KernelSU 的内核文件。\n\n### 在 Android 设备上使用 magiskboot {#using-magiskboot-on-Android-devices}\n\n1. 在 Magisk 的 [Release 页面](https://github.com/topjohnwu/Magisk/releases) 下载最新的 Magisk 安装包。\n2. 将 `Magisk-*(version).apk` 重命名为 `Magisk-*.zip` 然后解压缩。\n3. 将解压后的 `Magisk-*/lib/arm64-v8a/libmagiskboot.so` 文件，使用 adb push 到手机：`adb push Magisk-*/lib/arm64-v8a/libmagiskboot.so /data/local/tmp/magiskboot`\n4. 使用 adb 将原厂 boot.img 和 AnyKernel3 中的 Image 推送到手机\n5. adb shell 进入 /data/local/tmp/ 目录，然后赋予刚 push 文件的可执行权限 `chmod +x magiskboot`\n6. adb shell 进入 /data/local/tmp/ 目录，执行 `./magiskboot unpack boot.img` 此时会解包 `boot.img` 得到一个叫做 `kernel` 的文件，这个文件为你原厂的 kernel\n7. 使用 `Image` 替换 `kernel`: `mv -f Image kernel`\n8. 执行 `./magiskboot repack boot.img` 打包 img，此时你会得到一个 `new-boot.img` 的文件，使用这个文件 fastboot 刷入设备即可。\n\n### 在 macOS/Windows/Linux 上使用 magiskboot {#using-magiskboot-on-PC}\n\n1. 在 [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci) 下载适合你操作系统的 `magiskboot` 二进制文件。\n2. 在你的 PC 上准备好设备原厂的 boot.img 和 KernelSU 的 Image。\n3. `chmod +x magiskboot`\n4. 在你 PC 上合适的目录执行 `./magiskboot unpack boot.img` 来解包 `boot.img`, 你会得到一个 `kernel` 文件，这个文件是你设备原厂的 kernel。\n5. 使用 `Image` 替换 `kernel`: `mv -f Image kernel`\n6. 执行 `./magiskboot repack boot.img` 打包 img，此时你会得到一个 `new-boot.img` 的文件，使用这个文件 fastboot 刷入设备即可。\n\n:::info\nMagisk 官方的 `magiskboot` 可以在 Linux 设备上执行，如果你是 Linux 用户，可以直接用官方版本。\n:::\n\n## 使用自定义 Recovery 安装 {#install-by-recovery}\n\n前提：你的设备必须有自定义的 Recovery，如 TWRP；如果没有或者只有官方 Recovery，请使用其他方法。\n\n步骤：\n\n1. 在 KernelSU 的 [Release 页面](https://github.com/tiann/KernelSU/releases) 下载与你手机版本匹配的以 AnyKernel3 开头的 zip 刷机包；如果你不知道下载哪一个，请仔细查阅上述文档中关于 **KMI** 和**安全补丁级别**的描述；下载错误的刷机包很可能导致无法开机，请注意备份。\n2. 重启手机进入 TWRP。\n3. 使用 adb 将 AnyKernel3-*.zip 放到手机 /sdcard 然后在 TWRP 图形界面选择安装；或者你也可以直接 `adb sideload AnyKernel-*.zip` 安装。\n\nPS. 这种方法适用于任何情况下的安装（不限于初次安装或者后续升级），只要你用 TWRP 就可以操作。\n\n## 其他变通方法 {#other-methods}\n\n其实所有这些安装方法的主旨只有一个，那就是**替换原厂的内核为 KernelSU 提供的内核**；只要能实现这个目的，就可以安装；比如以下是其他可行的方法：\n\n1. 首先安装 Magisk，通过 Magisk 获取 root 权限后使用内核刷写器刷入 KernelSU 的 AnyKernel 包。\n2. 使用某些 PC 上的刷机工具箱刷入 KernelSU 提供的内核。\n\n如果这些方法导致无法开机，请优先尝试用 `magiskboot` 的方法。\n\n## 安装后：模块支持 {#post-installation-module-support}\n\n::: warning 修改系统文件需要 METAMODULE\n如果您想使用修改 `/system` 文件的模块，需要在安装 KernelSU 后安装一个 **metamodule**。仅使用脚本、sepolicy 或 system.prop 的模块无需 metamodule。\n:::\n\n**要获得 `/system` 修改支持**，请参阅 [Metamodule 指南](metamodule.md) 了解：\n- 什么是 metamodule 以及为什么需要它们\n- 如何安装官方的 `meta-overlayfs` metamodule\n- 其他 metamodule 选项\n"
  },
  {
    "path": "website/docs/zh_CN/guide/metamodule.md",
    "content": "# 元模块\n\n元模块是 KernelSU 的一项革新性功能，它将关键的模块系统能力从核心转移到可插拔模块中。这种架构转变在保持 KernelSU 稳定性和安全性的同时，为模块生态系统释放了更大的创新潜力。\n\n## 什么是元模块?\n\n元模块是一种特殊类型的 KernelSU 模块，为模块系统提供核心基础设施功能。与常规模块不同，元模块控制常规模块的*安装和挂载方式*。\n\n元模块是一种基于插件的扩展机制，允许完全自定义 KernelSU 的模块管理基础设施。通过将挂载和安装逻辑委托给元模块，KernelSU 避免成为脆弱的检测点，同时支持多样化的实现策略。\n\n**主要特征:**\n\n- **基础设施角色**: 元模块提供常规模块依赖的服务\n- **单实例**: 只允许一个元模块处于运行状态\n- **优先执行**: 元模块脚本在常规模块脚本之前运行\n- **特殊钩子**: 提供三个用于安装、挂载和清理的钩子脚本\n\n## 为什么需要元模块?\n\n传统的 Root 解决方案将挂载逻辑内置在核心中，这使得它们更容易被检测且难以演进。KernelSU 的元模块架构通过关注点分离解决了这些问题。\n\n**战略优势:**\n\n- **减少检测面**: KernelSU 本身不执行挂载，减少了检测向量\n- **稳定性**: 核心保持稳定，而挂载实现可以不断演进\n- **创新性**: 社区可以开发替代挂载策略\n- **选择性**: 用户可以选择最适合其需求的实现\n\n**挂载灵活性:**\n\n- **无挂载**: 对于仅使用无挂载模块的用户，完全避免挂载开销\n- **OverlayFS 挂载**: 传统方法，支持读写层(通过 `meta-overlayfs`)\n- **Magic Mount**: Magisk 兼容挂载，以获得更好的应用兼容性\n- **自定义实现**: 基于 FUSE 的 overlayfs、自定义 VFS 挂载或全新方法\n\n**超越挂载:**\n\n- **可扩展性**: 添加内核模块支持等功能，无需修改核心 KernelSU\n- **模块化**: 独立于 KernelSU 版本更新实现\n- **定制化**: 为特定设备或用例创建专门的解决方案\n\n::: warning 重要\n如果没有安装元模块，依赖挂载的模块，其挂载功能将不会生效。新安装的 KernelSU 需要安装元模块(如 `meta-overlayfs`)才能使模块正常工作。\n:::\n\n## 对于用户\n\n### 安装元模块\n\n像安装常规模块一样安装元模块:\n\n1. 下载元模块 ZIP 文件(例如 `meta-overlayfs.zip`)\n2. 打开 KernelSU Manager 应用\n3. 点击浮动操作按钮(➕)\n4. 选择元模块 ZIP 文件\n5. 重启设备\n\n`meta-overlayfs` 元模块是官方参考实现，提供传统的基于 overlayfs 的模块挂载，支持读写系统分区。\n\n### 检查活动的元模块\n\n您可以在 KernelSU Manager 应用的模块页面中查看当前活动的元模块。活动的元模块将显示在模块列表中，并带有特殊标识。\n\n### 卸载元模块\n\n::: danger 警告\n卸载元模块会影响**所有**模块。移除后，模块将不再被挂载，直到您安装另一个元模块。\n:::\n\n卸载步骤:\n\n1. 打开 KernelSU Manager\n2. 在模块列表中找到元模块\n3. 点击卸载(您会看到特殊警告)\n4. 确认操作\n5. 重启设备\n\n卸载后，如果您需要元模块的功能，应该安装另一个元模块。\n\n### 单元模块约束\n\n只允许一个元模块处于运行状态。如果您尝试安装第二个元模块，KernelSU 将阻止安装以避免冲突。\n\n切换元模块的步骤:\n\n1. 卸载所有常规模块\n2. 卸载当前元模块\n3. 重启\n4. 安装新元模块\n5. 重新安装常规模块\n6. 再次重启\n\n## 对于模块开发者\n\n如果您正在开发常规 KernelSU 模块，您不需要太担心元模块。只要用户安装了兼容的元模块(如 `meta-overlayfs`)，您的模块就能正常工作。\n\n**您需要知道的:**\n\n- **挂载需要元模块**: 模块中的 `system` 目录只有在用户安装了提供挂载功能的元模块时才会被挂载\n- **无需更改代码**: 现有模块无需修改即可继续工作\n\n::: tip\n如果您熟悉 Magisk 模块开发，您的模块在安装元模块后将在 KernelSU 中以相同方式工作，因为它提供了 Magisk 兼容的挂载。\n:::\n\n## 对于元模块开发者\n\n创建元模块允许您自定义 KernelSU 处理模块安装、挂载和卸载的方式。\n\n### 基本要求\n\n元模块通过 `module.prop` 中的特殊属性来识别:\n\n```txt\nid=meta-example\nname=My Custom Metamodule\nversion=1.0\nversionCode=1\nauthor=Your Name\ndescription=Custom module mounting implementation\nmetamodule=1\n```\n\n`metamodule=1`(或 `metamodule=true`)属性将此模块标记为元模块。没有此属性，模块将被视为常规模块。\n\n### 文件结构\n\n元模块结构:\n\n```txt\nmeta-example/\n├── module.prop              (必须包含 metamodule=1)\n│\n│      *** 元模块特定钩子 ***\n├── metamount.sh             (可选: 自定义挂载处理程序)\n├── metainstall.sh           (可选: 常规模块的安装钩子)\n├── metauninstall.sh         (可选: 常规模块的清理钩子)\n│\n│      *** 标准模块文件(全部可选) ***\n├── customize.sh             (安装自定义)\n├── post-fs-data.sh          (post-fs-data 阶段脚本)\n├── service.sh               (late_start service 脚本)\n├── boot-completed.sh        (启动完成脚本)\n├── uninstall.sh             (元模块自己的卸载脚本)\n└── [任何其他文件]\n```\n\n除了特殊的元模块钩子外，元模块可以使用所有标准模块功能(生命周期脚本等)。\n\n### 钩子脚本\n\n元模块可以提供最多三个特殊钩子脚本:\n\n#### 1. metamount.sh - 挂载处理程序\n\n**目的**: 控制启动期间模块的挂载方式。\n\n**执行时机**: 参考 [执行顺序](#execution-order)\n\n**环境变量:**\n\n- `MODDIR`: 元模块的目录路径(例如 `/data/adb/modules/meta-example`)\n- 所有标准 KernelSU 环境变量\n\n**职责:**\n\n- 以无系统方式挂载所有已启用的模块\n- 检查 `skip_mount` 标志\n- 处理特定模块的挂载要求\n\n::: danger 关键要求\n执行挂载操作时，**必须**将源/设备名称设置为 `\"KSU\"`。这将挂载标识为属于 KernelSU。\n\n**示例(正确):**\n\n```sh\nmount -t overlay -o lowerdir=/lower，upperdir=/upper，workdir=/work KSU /target\n```\n\n**对于现代挂载 API**，设置源字符串:\n\n```rust\nfsconfig_set_string(fs， \"source\"， \"KSU\")?;\n```\n\n这对于 KernelSU 正确识别和管理其挂载至关重要。\n:::\n\n**示例脚本:**\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\n\n# 示例: 简单的绑定挂载实现\nfor module in /data/adb/modules/*; do\n    if [ -f \"$module/disable\" ] || [ -f \"$module/skip_mount\" ]; then\n        continue\n    fi\n\n    if [ -d \"$module/system\" ]; then\n        # 使用 source=KSU 挂载(必需!)\n        mount -o bind，dev=KSU \"$module/system\" /system\n    fi\ndone\n```\n\n#### 2. metainstall.sh - 安装钩子\n\n**目的**: 自定义常规模块的安装方式。\n\n**执行时机**: 在模块安装期间，文件提取后但安装完成前。此脚本被内置安装程序**source**(而非执行)，类似于 `customize.sh` 的工作方式。\n\n**环境变量和函数:**\n\n此脚本继承内置 `install.sh` 的所有变量和函数:\n\n- **变量**: `MODPATH`、`TMPDIR`、`ZIPFILE`、`ARCH`、`API`、`IS64BIT`、`KSU`、`KSU_VER`、`KSU_VER_CODE`、`BOOTMODE` 等\n- **函数**:\n  - `ui_print <msg>` - 向控制台打印消息\n  - `abort <msg>` - 打印错误并终止安装\n  - `set_perm <target> <owner> <group> <permission> [context]` - 设置文件权限\n  - `set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]` - 递归设置权限\n  - `install_module` - 调用内置模块安装过程\n\n**用例:**\n\n- 在内置安装之前或之后处理模块文件(准备好后调用 `install_module`)\n- 移动模块文件\n- 验证模块兼容性\n- 设置特殊目录结构\n- 初始化模块特定资源\n\n**注意**: 安装元模块本身时**不会**调用此脚本。\n\n#### 3. metauninstall.sh - 清理钩子\n\n**目的**: 卸载常规模块时清理资源。\n\n**执行时机**: 在模块卸载期间，在删除模块目录之前。\n\n**环境变量:**\n\n- `MODULE_ID`: 正在卸载的模块的 ID\n\n**用例:**\n\n- 处理文件\n- 清理符号链接\n- 释放分配的资源\n- 更新内部跟踪\n\n**示例脚本:**\n\n```sh\n#!/system/bin/sh\n# 卸载常规模块时调用\nMODULE_ID=\"$1\"\nIMG_MNT=\"/data/adb/metamodule/mnt\"\n\n# 从镜像中删除模块文件\nif [ -d \"$IMG_MNT/$MODULE_ID\" ]; then\n    rm -rf \"$IMG_MNT/$MODULE_ID\"\nfi\n```\n\n### 执行顺序 {#execution-order}\n\n了解启动执行顺序对于元模块开发至关重要:\n\n```txt\npost-fs-data 阶段:\n  1. 执行通用 post-fs-data.d 脚本\n  2. restorecon，加载 sepolicy.rule\n  3. 执行元模块的 post-fs-data.sh(如果存在)\n  4. 执行常规模块的 post-fs-data.sh\n  5. 加载 system.prop\n  6. 执行元模块的 metamount.sh\n     └─> 以无系统方式挂载所有模块\n  7. post-mount.d 阶段运行\n     - 通用 post-mount.d 脚本\n     - 元模块的 post-mount.sh(如果存在)\n     - 常规模块的 post-mount.sh\n\nservice 阶段:\n  1. 执行通用 service.d 脚本\n  2. 执行元模块的 service.sh(如果存在)\n  3. 执行常规模块的 service.sh\n\nboot-completed 阶段:\n  1. 执行通用 boot-completed.d 脚本\n  2. 执行元模块的 boot-completed.sh(如果存在)\n  3. 执行常规模块的 boot-completed.sh\n```\n\n**要点:**\n\n- `metamount.sh` 在所有 post-fs-data 脚本(元模块和常规模块)**之后**运行\n- 元模块生命周期脚本(`post-fs-data.sh`、`service.sh`、`boot-completed.sh`)始终在常规模块脚本之前运行\n- `.d` 目录中的通用脚本在元模块脚本之前运行\n- `post-mount` 阶段在挂载完成后运行\n\n### 符号链接机制\n\n当安装元模块时，KernelSU 会创建一个符号链接:\n\n```sh\n/data/adb/metamodule -> /data/adb/modules/<metamodule_id>\n```\n\n这为访问活动元模块提供了稳定的路径，无论其 ID 如何。\n\n**好处:**\n\n- 一致的访问路径\n- 轻松检测活动元模块\n- 简化配置\n\n### 真实示例: meta-overlayfs\n\n`meta-overlayfs` 元模块是官方参考实现。它展示了元模块开发的最佳实践。\n\n#### 架构\n\n`meta-overlayfs` 使用**双目录架构**:\n\n1. **元数据目录**: `/data/adb/modules/`\n   - 包含 `module.prop`、`disable`、`skip_mount` 标记\n   - 启动期间快速扫描\n   - 存储占用小\n\n2. **内容目录**: `/data/adb/metamodule/mnt/`\n   - 包含实际模块文件(system、vendor、product 等)\n   - 存储在 ext4 镜像(`modules.img`)中\n   - 使用 ext4 功能优化空间\n\n#### metamount.sh 实现\n\n以下是 `meta-overlayfs` 如何实现挂载处理程序:\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\nIMG_FILE=\"$MODDIR/modules.img\"\nMNT_DIR=\"$MODDIR/mnt\"\n\n# 如果尚未挂载，则挂载 ext4 镜像\nif ! mountpoint -q \"$MNT_DIR\"; then\n    mkdir -p \"$MNT_DIR\"\n    mount -t ext4 -o loop，rw，noatime \"$IMG_FILE\" \"$MNT_DIR\"\nfi\n\n# 为双目录支持设置环境变量\nexport MODULE_METADATA_DIR=\"/data/adb/modules\"\nexport MODULE_CONTENT_DIR=\"$MNT_DIR\"\n\n# 执行挂载二进制文件\n# (实际挂载逻辑在 Rust 二进制文件中)\n\"$MODDIR/meta-overlayfs\"\n```\n\n#### 主要特性\n\n**Overlayfs 挂载:**\n\n- 使用内核 overlayfs 进行真正的无系统修改\n- 支持多个分区(system、vendor、product、system_ext、odm、oem)\n- 通过 `/data/adb/modules/.rw/` 支持读写层\n\n**源标识:**\n\n```rust\n// 来自 meta-overlayfs/src/mount.rs\nfsconfig_set_string(fs， \"source\"， \"KSU\")?;  // 必需!\n```\n\n这为所有 overlay 挂载设置 `dev=KSU`，实现正确识别。\n\n### 最佳实践\n\n开发元模块时:\n\n1. **始终将源设置为\"KSU\"**以进行挂载操作 - 内核卸载和 zygisksu 卸载需要此设置才能正确卸载\n2. **优雅地处理错误** - 启动过程对时间敏感\n3. **尊重标准标志** - 支持 `skip_mount` 和 `disable`\n4. **记录操作** - 使用 `echo` 或日志记录进行调试\n5. **彻底测试** - 挂载错误可能导致启动循环\n6. **记录行为** - 清楚地解释您的元模块做什么\n7. **提供迁移路径** - 帮助用户从其他解决方案切换\n\n### 测试您的元模块\n\n发布前:\n\n1. 在干净的 KernelSU 设置上**测试安装**\n2. **验证挂载**各种模块类型\n3. **检查兼容性**与常见模块\n4. **测试卸载**和清理\n5. **验证启动性能**(metamount.sh 是阻塞的!)\n6. **确保正确的错误处理**以避免启动循环\n\n## 常见问题\n\n### 我需要元模块吗?\n\n**对于用户**: 仅当您想使用需要挂载的模块时。如果您只使用运行脚本而不修改系统文件的模块，则不需要元模块。\n\n**对于模块开发者**: 不需要，您正常开发模块。仅当您的模块需要挂载时，用户才需要元模块。\n\n**对于高级用户**: 仅当您想自定义挂载行为或创建替代挂载实现时。\n\n### 我可以有多个元模块吗?\n\n不可以。一次只能安装一个元模块。这可以防止冲突并确保可预测的行为。\n\n### 如果我卸载了唯一的元模块会怎样?\n\n模块将不再被挂载。您的设备将正常启动，但模块修改将不会应用，直到您安装另一个元模块。\n\n### meta-overlayfs 是必需的吗?\n\n不是。它提供与大多数模块兼容的标准 overlayfs 挂载。如果您需要不同的行为，可以创建自己的元模块。\n\n## 另请参阅\n\n- [模块指南](module.md) - 通用模块开发\n- [与 Magisk 的区别](difference-with-magisk.md) - 比较 KernelSU 和 Magisk\n- [如何构建](how-to-build.md) - 从源代码构建 KernelSU\n"
  },
  {
    "path": "website/docs/zh_CN/guide/module-config.md",
    "content": "# 模块配置\n\nKernelSU 提供了一个内置的配置系统,允许模块存储持久化或临时的键值设置。配置以二进制格式存储在 `/data/adb/ksu/module_configs/<module_id>/`,具有以下特性:\n\n## 配置类型\n\n- **持久配置** (`persist.config`):重启后保留,直到明确删除或卸载模块\n- **临时配置** (`tmp.config`):在每次启动时的 post-fs-data 阶段自动清除\n\n读取配置时,对于同一个键,临时值优先于持久值。\n\n## 在模块脚本中使用配置\n\n所有模块脚本(`post-fs-data.sh`、`service.sh`、`boot-completed.sh` 等)运行时都会设置 `KSU_MODULE` 环境变量为模块 ID。您可以使用 `ksud module config` 命令来管理模块的配置:\n\n```bash\n# 获取配置值\nvalue=$(ksud module config get my_setting)\n\n# 设置持久配置值\nksud module config set my_setting \"some value\"\n\n# 设置临时配置值(重启后清除)\nksud module config set --temp runtime_state \"active\"\n\n# 从 stdin 设置值(适用于多行或复杂数据)\nksud module config set my_key <<EOF\n多行\n文本值\nEOF\n\n# 或从命令管道输入\necho \"value\" | ksud module config set my_key\n\n# 显式使用 stdin 标志\ncat file.json | ksud module config set json_data --stdin\n\n# 列出所有配置项(合并持久和临时配置)\nksud module config list\n\n# 删除配置项\nksud module config delete my_setting\n\n# 删除临时配置项\nksud module config delete --temp runtime_state\n\n# 清除所有持久配置\nksud module config clear\n\n# 清除所有临时配置\nksud module config clear --temp\n```\n\n## 验证限制\n\n配置系统强制执行以下限制:\n\n- **最大键长度**:256 字节\n- **最大值长度**:1MB (1048576 字节)\n- **最大配置项数**:每个模块 32 个\n- **键格式**:必须匹配 `^[a-zA-Z][a-zA-Z0-9._-]+$`(与模块 ID 相同)\n  - 必须以字母(a-zA-Z)开头\n  - 可包含字母、数字、点(`.`)、下划线(`_`)或连字符(`-`)\n  - 最小长度:2 个字符\n- **值格式**:无限制 - 可包含任何 UTF-8 字符,包括换行符、控制字符等\n  - 以二进制格式存储,带长度前缀,确保安全处理所有数据\n\n## 生命周期\n\n- **启动时**:所有临时配置在 post-fs-data 阶段清除\n- **模块卸载时**:所有配置(持久和临时)自动删除\n- 配置以二进制格式存储,使用魔数 `0x4b53554d`(\"KSUM\")和版本验证\n\n## 使用场景\n\n配置系统适用于:\n\n- **用户偏好**:存储用户通过 WebUI 或 action 脚本配置的模块设置\n- **功能开关**:在不重新安装的情况下启用/禁用模块功能\n- **运行时状态**:跟踪应在重启时重置的临时状态(使用临时配置)\n- **安装设置**:记住模块安装时做出的选择\n- **复杂数据**:存储 JSON、多行文本、Base64 编码数据或任何结构化内容(最多 1MB)\n\n::: tip 最佳实践\n- 对于应在重启后保留的用户偏好,使用持久配置\n- 对于应在启动时重置的运行时状态或功能开关,使用临时配置\n- 在脚本中使用配置值之前验证它们\n- 使用 `ksud module config list` 命令调试配置问题\n:::\n\n## 高级功能\n\n模块配置系统提供了用于高级用例的特殊配置键:\n\n### 覆盖模块描述 {#overriding-module-description}\n\n您可以通过设置 `override.description` 配置键来动态覆盖 `module.prop` 中的 `description` 字段:\n\n```bash\n# 覆盖模块描述\nksud module config set override.description \"在管理器中显示的自定义描述\"\n```\n\n当获取模块列表时,如果存在 `override.description` 配置,它将替换 `module.prop` 中的原始描述。这对于以下场景很有用:\n- 在模块描述中显示动态状态信息\n- 向用户显示运行时配置详情\n- 基于模块状态更新描述而无需重新安装\n\n### 声明管理的功能\n\n模块可以使用 `manage.<feature>` 配置模式声明它们管理的 KernelSU 功能。支持的功能对应于 KernelSU 内部的 `FeatureId` 枚举:\n\n**支持的功能:**\n- `su_compat` - SU 兼容模式\n- `kernel_umount` - 内核自动卸载\n\n```bash\n# 声明此模块管理 SU 兼容性并将其启用\nksud module config set manage.su_compat true\n\n# 声明此模块管理内核卸载并将其禁用\nksud module config set manage.kernel_umount false\n\n# 移除功能管理(模块不再控制此功能)\nksud module config delete manage.su_compat\n```\n\n**工作原理:**\n- `manage.<feature>` 键的存在表示模块正在管理该功能\n- 值表示期望的状态:`true`/`1` 代表启用,`false`/`0`(或任何其他值)代表禁用\n- 要停止管理某个功能,请完全删除该配置键\n\n管理的功能通过模块列表 API 以 `managedFeatures` 字段(逗号分隔的字符串)公开。这允许:\n- KernelSU 管理器检测哪些模块管理哪些 KernelSU 功能\n- 防止多个模块尝试管理同一功能时发生冲突\n- 更好地协调模块与核心 KernelSU 功能之间的关系\n\n::: warning 仅支持预定义功能\n仅使用上面列出的预定义功能名称(`su_compat`、`kernel_umount`)。这些对应于实际的 KernelSU 内部功能。使用其他功能名称不会导致错误,但没有任何功能作用。\n:::\n"
  },
  {
    "path": "website/docs/zh_CN/guide/module-webui.md",
    "content": "# 模块 WebUI\n\nKernelSU 的模块除了执行启动脚本和修改系统文件之外，还支持显示 UI 界面和与用户交互。\n\n你可以通过任何 Web 技术编写 HTML + CSS + JavaScript 页面，KernelSU 的管理器将通过 WebView 显示这些页面。此外，KernelSU 还提供了一些用于与系统交互的 JavaScript API，例如执行 shell 命令。\n\n## WebUI 根目录\n\nWeb 资源文件应放置在模块根目录的 `webroot` 子目录中，并且其中**必须**有一个名为`index.html`的文件，该文件是模块页面入口。包含 Web 界面的最简单的模块结构如下：\n\n````txt\n❯ tree .\n.\n|-- module.prop\n`-- webroot\n     `--index.html\n````\n\n:::warning\n安装模块时，KernelSU 会自动设置 `webroot` 目录的权限和 SELinux context，如果您不知道自己在做什么，请不要自行设置该目录的权限！\n:::\n\n如果您的页面包含 CSS 和 JavaScript，您也需要将其放入此目录中。\n\n## JavaScript API\n\n如果只是一个显示页面，那它和普通网页没有什么区别。更重要的是，KernelSU 提供了一系列的系统 API，可以让您实现模块特有的功能。\n\nKernelSU 提供了一个 JavaScript 库并[在 npm 上发布](https://www.npmjs.com/package/kernelsu)，您可以在网页的 JavaScript 代码中使用它。\n\n例如，您可以执行 shell 命令来获取特定配置或修改属性：\n\n```JavaScript\nimport { exec } from 'kernelsu';\n\nconst { errno, stdout } = await exec(\"getprop ro.product.model\");\n````\n\n再比如，你可以让网页全屏显示，或者显示一个 Toast。\n\n[API 文档](https://www.npmjs.com/package/kernelsu)\n\n如果您发现现有的 API 不能满足您的需求或者使用不方便，欢迎[在这里](https://github.com/tiann/KernelSU/issues)给我们提出建议！\n\n## 一些技巧\n\n1. 您可以正常使用`localStorage`存储一些数据，但卸载管理器后，这些数据将会丢失。 如果需要持久保存，可以自己将数据写入某个目录。\n2. 对于简单的页面，我建议您使用 [parceljs](https://parceljs.org/) 进行打包。它零配置，使用非常方便。不过，如果你是前端高手或者有自己的喜好，那就选择你喜欢的吧！\n"
  },
  {
    "path": "website/docs/zh_CN/guide/module.md",
    "content": "# 模块开发指南 {#introduction}\n\nKernelSU 提供了一个模块机制，它可以在保持系统分区完整性的同时达到修改系统分区的效果；这种机制通常被称之为 systemless。\n\nKernelSU 的模块运作机制与 Magisk 几乎是一样的，如果你熟悉 Magisk 模块的开发，那么开发 KernelSU 的模块大同小异，你可以跳过下面有关模块的介绍，只需要了解 [KernelSU 模块与 Magisk 模块的异同](difference-with-magisk.md)。\n\n::: warning 仅修改系统文件需要 METAMODULE\nKernelSU 使用 [metamodule](metamodule.md) 架构来挂载 `system` 目录。**只有当您的模块需要修改 `/system` 文件**（通过 `system` 目录）时，才需要安装 metamodule（例如 [meta-overlayfs](https://github.com/tiann/KernelSU/releases)）。其他模块功能如脚本、sepolicy 规则和 system.prop 无需 metamodule 即可工作。\n:::\n\n## 模块界面\n\nKernelSU 的模块支持显示界面并与用户交互，请参阅 [WebUI 文档](module-webui.md)。\n\n## 模块配置\n\nKernelSU 提供了一个内置的配置系统，允许模块存储持久化或临时的键值设置。详情请参阅[模块配置文档](module-config.md)。\n\n## Busybox\n\nKernelSU 提供了一个功能完备的 BusyBox 二进制文件（包括完整的 SELinux 支持）。可执行文件位于 `/data/adb/ksu/bin/busybox`。\nKernelSU 的 BusyBox 支持运行时可切换的 \"ASH Standalone Shell Mode\"。\n这种独立模式意味着在运行 BusyBox 的 ash shell 时，每个命令都会直接使用 BusyBox 中内置的应用程序，而不管 PATH 设置为什么。\n例如，`ls`、`rm`、`chmod` 等命令将不会使用 PATH 中设置的命令（在 Android 的情况下，默认情况下分别为 `/system/bin/ls`、`/system/bin/rm` 和 `/system/bin/chmod`），而是直接调用 BusyBox 内置的应用程序。\n这确保了脚本始终在可预测的环境中运行，并始终具有完整的命令套件，无论它运行在哪个 Android 版本上。\n要强制一个命令不使用 BusyBox，你必须使用完整路径调用可执行文件。\n\n在 KernelSU 上下文中运行的每个 shell 脚本都将在 BusyBox 的 ash shell 中以独立模式运行。对于第三方开发者相关的内容，包括所有启动脚本和模块安装脚本。\n\n对于想要在 KernelSU 之外使用这个“独立模式”功能的用户，有两种启用方法:\n\n1. 设置环境变量 `ASH_STANDALONE` 为 `1`。例如：`ASH_STANDALONE=1 /data/adb/ksu/bin/busybox sh <script>`\n2. 使用命令行选项切换：`/data/adb/ksu/bin/busybox sh -o standalone <script>`\n\n为了确保所有后续的 `sh` shell 都在独立模式下执行，第一种是首选方法（这也是 KernelSU 和 KernelSU 管理器内部使用的方法），因为环境变量会被继承到子进程中。\n\n::: tip 与 Magisk 的差异\n\nKernelSU 的 BusyBox 现在是直接使用 Magisk 项目编译的二进制文件，**感谢 Magisk！**\n因此，你完全不用担心 BusyBox 脚本与在 Magisk 和 KernelSU 之间的兼容问题，因为他们是完全一样的！\n:::\n\n## KernelSU 模块 {#kernelsu-modules}\n\nKernelSU 模块就是一个放置在 `/data/adb/modules` 内且满足如下结构的文件夹：\n\n```txt\n/data/adb/modules\n├── .\n├── .\n|\n├── $MODID                  <--- 模块的文件夹名称与模块 ID 相同\n│   │\n│   │      *** 模块配置文件 ***\n│   │\n│   ├── module.prop         <--- 此文件保存模块相关的一些配置，如模块 ID、版本等\n│   │\n│   │      *** 模块内容 ***\n│   │\n│   ├── system              <--- 这个文件夹通常会被挂载到系统\n│   │   ├── ...\n│   │   ├── ...\n│   │   └── ...\n│   │\n│   │      *** 标记文件 ***\n│   │\n│   ├── skip_mount          <--- 如果这个文件存在，那么模块的 `/system` 将不会被挂载\n│   ├── disable             <--- 如果这个文件存在，那么模块会被禁用\n│   ├── remove              <--- 如果这个文件存在，下次重启的时候模块会被移除\n│   │\n│   │      *** 可选文件 ***\n│   │\n│   ├── post-fs-data.sh     <--- 这个脚本将会在 post-fs-data 模式下运行\n│   ├── post-mount.sh       <--- 这个脚本将会在 post-mount 模式下运行\n│   ├── service.sh          <--- 这个脚本将会在 late_start 服务模式下运行\n│   ├── boot-completed.sh   <--- 这个脚本将会在 Android 系统启动完毕后以服务模式运行\n|   ├── uninstall.sh        <--- 这个脚本将会在模块被卸载时运行\n│   ├── system.prop         <--- 这个文件中指定的属性将会在系统启动时通过 resetprop 更改\n│   ├── sepolicy.rule       <--- 这个文件中的 SELinux 策略将会在系统启动时加载\n│   │\n│   │      *** 自动生成的目录，不要手动创建或者修改！ ***\n│   │\n│   ├── vendor              <--- 如果 /system/vendor 是符号链接且存在，从 $MODID/system/vendor 移动到模块根目录\n│   ├── product             <--- 如果 /system/product 是符号链接且存在，从 $MODID/system/product 移动到模块根目录\n│   ├── system_ext          <--- 如果 /system/system_ext 是符号链接且存在，从 $MODID/system/system_ext 移动到模块根目录\n│   │\n│   │      *** Any additional files / folders are allowed ***\n│   │\n│   ├── ...\n│   └── ...\n|\n├── another_module\n│   ├── .\n│   └── .\n├── .\n├── .\n```\n\n::: tip 与 Magisk 的差异\nKernelSU 没有内置的针对 Zygisk 的支持，因此模块中没有 Zygisk 相关的内容，但你可以通过 [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) 来支持 Zygisk 模块，此时 Zygisk 模块的内容与 Magisk 所支持的 Zygisk 是完全相同的。\n:::\n\n### module.prop\n\nmodule.prop 是一个模块的配置文件，在 KernelSU 中如果模块中不包含此文件，那么它将不被认为是一个模块；此文件的格式如下：\n\n```txt\nid=<string>\nname=<string>\nversion=<string>\nversionCode=<int>\nauthor=<string>\ndescription=<string>\nupdateJson=<url> (optional)\nactionIcon=<path> (optional)\nwebuiIcon=<path> (optional)\n```\n\n- id 必须与这个正则表达式匹配：`^[a-zA-Z][a-zA-Z0-9._-]+$` 例如：✓ `a_module`，✓ `a.module`，✓ `module-101`，✗ `a module`，✗ `1_module`，✗ `-a-module`。这是您的模块的唯一标识符，发布后不应更改。\n- versionCode 必须是一个整数，用于比较版本。\n- 其他未在上面提到的内容可以是任何单行字符串。\n- 请确保使用 UNIX（LF）换行类型，而不是 Windows（CR + LF）或 Macintosh（CR）。\n- actionIcon 和 webuiIcon 是可选的图标路径，用作管理器中模块\n  Action 快捷方式和 WebUI 快捷方式的默认图标。这些路径必须是基于模\n  块根目录的相对路径。例如 `actionIcon=icon/icon.png`\n  将会解析为 `<MODDIR>/icon/icon.png`。\n\n::: tip 动态描述\n`description` 字段可以在运行时使用模块配置系统动态覆盖。详情请参阅[覆盖模块描述](module-config.md#overriding-module-description)。\n:::\n\n### Shell 脚本 {#shell-scripts}\n\n请阅读 [启动脚本](#boot-scripts) 一节，以了解 `post-fs-data.sh`, `post-mount.sh`, `service.sh` 和 `boot-completed.sh` 之间的区别。对于大多数模块开发者来说，如果您只需要运行一个启动脚本，`service.sh` 应该已经足够了。\n\n在您的模块的所有脚本中，请使用`MODDIR=${0%/*}`来获取您的模块的基本目录路径；请勿在脚本中硬编码您的模块路径。\n\n:::tip 与 Magisk 的差异\n你可以通过环境变量 `KSU` 来判断脚本是运行在 KernelSU 还是 Magisk 中，如果运行在 KernelSU，这个值会被设置为 `true`。\n:::\n\n### `system` 目录 {#system-directories}\n\n这个目录的内容会在系统启动后，以 `overlayfs` 的方式叠加在系统的 `/system` 分区之上，这意味着：\n\n1. 系统中对应目录的同名文件会被此目录的文件覆盖。\n2. 系统中对应目录的同名文件夹会与此目录的文件夹合并。\n\n如果你想删掉系统原来目录某个文件或者文件夹，你需要在模块目录通过 `mknod filename c 0 0` 来创建一个 `filename` 的同名文件；这样 overlayfs 系统会自动 whiteout 等效删除此文件（`/system` 分区并没有被更改）。\n\n你也可以在 `customize.sh` 中声明一个名为 `REMOVE` 并且包含一系列目录的变量来执行删除操作，KernelSU 会自动为你在模块对应目录执行 `mknod <TARGET> c 0 0`。例如：\n\n```sh\nREMOVE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n```\n\n上面的这个列表将会执行： `mknod $MODPATH/system/app/YouTuBe c 0 0` 和 `mknod $MODPATH/system/app/Bloatware c 0 0`；并且 `/system/app/YouTube` 和 `/system/app/Bloatware` 将会在模块生效后被删除。\n\n如果你想替换掉系统的某个目录，你需要在模块目录创建一个相同路径的目录，然后为此目录设置此属性：`setfattr -n trusted.overlay.opaque -v y <TARGET>`；这样 overlayfs 系统会自动将系统内相应目录替换（`/system` 分区并没有被更改）。\n\n你可以在 `customize.sh` 中声明一个名为 `REPLACE` 并且包含一系列目录的变量来执行替换操作，KernelSU 会自动为你在模块对应目录执行相关操作。例如：\n\n```sh\nREPLACE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n```\n\n上面这个列表将会：自动创建目录 `$MODPATH/system/app/YouTube` 和 `$MODPATH//system/app/Bloatware`，然后执行 `setfattr -n trusted.overlay.opaque -v y $$MODPATH/system/app/YouTube` 和 `setfattr -n trusted.overlay.opaque -v y $$MODPATH/system/app/Bloatware`；并且 `/system/app/YouTube` 和 `/system/app/Bloatware` 将会在模块生效后替换为空目录。\n\n::: tip 与 Magisk 的差异\n\nKernelSU 的 systemless 机制是通过内核的 overlayfs 实现的，而 Magisk 当前则是通过 magic mount (bind mount)，二者实现方式有着巨大的差异，但最终的目标实际上是一致的：不修改物理的 `/system` 分区但实现修改 `/system` 文件。\n:::\n\n如果你对 overlayfs 感兴趣，建议阅读 Linux Kernel 关于 [overlayfs 的文档](https://docs.kernel.org/filesystems/overlayfs.html)\n\n### system.prop\n\n这个文件的格式与 `build.prop` 完全相同：每一行都是 `[key]=[value]` 的形式。\n\n### sepolicy.rule\n\n如果您的模块需要一些额外的 SELinux 策略补丁，请将这些规则添加到此文件中。这个文件中的每一行都将被视为一个策略语句。\n\n## 模块安装包 {#module-installer}\n\nKernelSU 的模块安装包就是一个可以通过 KernelSU 管理器 APP 刷入的 zip 文件，此 zip 文件的格式如下：\n\n```txt\nmodule.zip\n│\n├── customize.sh                       <--- (Optional, more details later)\n│                                           This script will be sourced by update-binary\n├── ...\n├── ...  /* 其他模块文件 */\n│\n```\n\n:::warning\nKernelSU 模块不支持在 Recovery 中安装！！\n:::\n\n### 定制安装过程 {#customizing-installation}\n\n如果你想控制模块的安装过程，可以在模块的目录下创建一个名为 `customize.sh` 的文件，这个脚本将会在模块被解压后**导入**到当前 shell 中，如果你的模块需要根据设备的 API 版本或者设备构架做一些额外的操作，那这个脚本将非常有用。\n\n如果你想完全控制脚本的安装过程，你可以在 `customize.sh` 中声明 `SKIPUNZIP=1` 来跳过所有的默认安装步骤；此时，你需要自行处理所有安装过程（如解压模块，设置权限等）\n\n`customize.sh` 脚本以“独立模式”运行在 KernelSU 的 BusyBox `ash` shell 中。你可以使用如下变量和函数：\n\n#### 变量 {#variables}\n\n- `KSU` (bool): 标记此脚本运行在 KernelSU 环境下，此变量的值将永远为 `true`，你可以通过它区分 Magisk。\n- `KSU_VER` (string): KernelSU 当前的版本名字 (如： `v0.4.0`)\n- `KSU_VER_CODE` (int): KernelSU 用户空间当前的版本号 (如. `10672`)\n- `KSU_KERNEL_VER_CODE` (int): KernelSU 内核空间当前的版本号 (如. `10672`)\n- `BOOTMODE` (bool): 此变量在 KernelSU 中永远为 `true`\n- `MODPATH` (path): 当前模块的安装目录\n- `TMPDIR` (path): 可以存放临时文件的目录\n- `ZIPFILE` (path): 当前模块的安装包文件\n- `ARCH` (string): 设备的 CPU 构架，有如下几种： `arm`, `arm64`, `x86`, or `x64`\n- `IS64BIT` (bool): 是否是 64 位设备\n- `API` (int): 当前设备的 Android API 版本 (如：Android 6.0 上为 `23`)\n\n::: warning\n`MAGISK_VER_CODE` 在 KernelSU 中永远为 `25200`，`MAGISK_VER` 则为 `v25.2`，请不要通过这两个变量来判断是否是 KernelSU！\n:::\n\n#### 函数 {#functions}\n\n```txt\nui_print <msg>\n    print <msg> to console\n    Avoid using 'echo' as it will not display in custom recovery's console\n\nabort <msg>\n    print error message <msg> to console and terminate the installation\n    Avoid using 'exit' as it will skip the termination cleanup steps\n\nset_perm <target> <owner> <group> <permission> [context]\n    if [context] is not set, the default is \"u:object_r:system_file:s0\"\n    this function is a shorthand for the following commands:\n       chown owner.group target\n       chmod permission target\n       chcon context target\n\nset_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]\n    if [context] is not set, the default is \"u:object_r:system_file:s0\"\n    for all files in <directory>, it will call:\n       set_perm file owner group filepermission context\n    for all directories in <directory> (including itself), it will call:\n       set_perm dir owner group dirpermission context\n```\n\n## 启动脚本 {#boot-scripts}\n\n在 KernelSU 中，根据脚本运行模式的不同分为两种：post-fs-data 模式和 late_start 服务模式。\n\n- post-fs-data 模式\n  - 这个阶段是阻塞的。在执行完成之前或者 10 秒钟之后，启动过程会暂停。\n  - 脚本在任何模块被挂载之前运行。这使得模块开发者可以在模块被挂载之前动态地调整它们的模块。\n  - 这个阶段发生在 Zygote 启动之前。\n  - 使用 setprop 会导致启动过程死锁！请使用 `resetprop -n <prop_name> <prop_value>` 代替。\n  - **只有在必要时才在此模式下运行脚本**。\n\n- late_start 服务模式\n  - 这个阶段是非阻塞的。你的脚本会与其余的启动过程**并行**运行。\n  - **大多数脚本都建议在这种模式下运行**。\n\n在 KernelSU 中，启动脚本根据存放位置的不同还分为两种：通用脚本和模块脚本。\n\n- 通用脚本\n  - 放置在 `/data/adb/post-fs-data.d`, `/data/adb/post-mount.d`, `/data/adb/service.d` 或 `/data/adb/boot-completed.d` 中。\n  - 只有在脚本被设置为可执行（`chmod +x script.sh`）时才会被执行。\n  - 在 `post-fs-data.d` 中的脚本以 post-fs-data 模式运行，在 `service.d` 中的脚本以 late_start 服务模式运行。\n  - 模块**不应**在安装过程中添加通用脚本。\n\n- 模块脚本\n  - 放置在模块自己的文件夹中。\n  - 只有当模块被启用时才会执行。\n  - `post-fs-data.sh` 以 post-fs-data 模式运行，`post-mount.sh` 以 post-mount 模式运行，而 `service.sh` 则以 late_start 服务模式运行，`boot-completed` 在 Android 系统启动完毕后以服务模式运行。\n\n所有启动脚本都将在 KernelSU 的 BusyBox ash shell 中运行，并启用“独立模式”。\n\n### 启动脚本的流程解疑 {#Boot-scripts-process-explanation}\n\n以下是 Android 的相关启动流程（部分省略），其中包括了 KernelSU 的操作（带前导星号），应该能帮助你更好地理解这些启动脚本的用途：\n\n```txt\n0. Bootloader (nothing on screen)\nload patched boot.img\nload kernel:\n    - GKI mode: GKI kernel with KernelSU integrated\n    - LKM mode: stock kernel\n...\n\n1. kernel exec init (oem logo on screen):\n    - GKI mode: stock init\n    - LKM mode: exec ksuinit, insmod kernelsu.ko, exec stock init\nmount /dev, /dev/pts, /proc, /sys, etc.\nproperty-init -> read default props\nread init.rc\n...\nearly-init -> init -> late_init\nearly-fs\n   start vold\nfs\n  mount /vendor, /system, /persist, etc.\npost-fs-data\n  *safe mode check\n  *execute general scripts in post-fs-data.d/\n  *load sepolicy.rule\n  *mount tmpfs\n  *execute module scripts post-fs-data.sh\n    **(Zygisk)./bin/zygisk-ptrace64 monitor\n  *(pre)load system.prop (same as resetprop -n)\n  *remount modules /system\n  *execute general scripts in post-mount.d/\n  *execute module scripts post-mount.sh\nzygote-start\nload_all_props_action\n  *execute resetprop (actual set props for resetprop with -n option)\n... -> boot\n  class_start core\n    start-service logd, console, vold, etc.\n  class_start main\n    start-service adb, netd (iptables), zygote, etc.\n\n2. kernel2user init (rom animation on screen, start by service bootanim)\n*execute general scripts in service.d/\n*execute module scripts service.sh\n*set props for resetprop without -p option\n  **(Zygisk) hook zygote (start zygiskd)\n  **(Zygisk) mount zygisksu/module.prop\nstart system apps (autostart)\n...\nboot complete (broadcast ACTION_BOOT_COMPLETED event)\n*execute general scripts in boot-completed.d/\n*execute module scripts boot-completed.sh\n\n3. User operable (lock screen)\ninput password to decrypt /data/data\n*actual set props for resetprop with -p option\nstart user apps (autostart)\n```\n\n如果你对 Android 的 init 语言感兴趣，推荐阅读[文档](https://android.googlesource.com/platform/system/core/+/master/init/README.md)。\n\n## Late-load 模式 {#late-load-mode}\n\n除了上述标准启动流程外，KernelSU 还支持 **late-load 模式**，用于 LKM（可加载内核模块）场景。在该模式下，KernelSU 内核模块在**系统完全启动后**加载，而非在 init 过程中加载。\n\n### 什么时候触发 late-load？\n\n通过运行 `ksud late-load` 命令触发。该命令会：\n\n1. 检测当前 KMI 版本，从内嵌资源中加载对应的 `kernelsu.ko`。\n2. 执行模块初始化（SELinux 规则、白名单、feature 等），这些工作在标准启动中发生在 boot 阶段。\n\n由于系统已经完全运行，某些启动时的机制不可用或不需要。\n\n### 与标准启动的差异\n\n| 行为 | 标准启动 | Late-load 模式 |\n|------|:---:|:---:|\n| 内核模块由 init (PID 1) 加载 | 是 | 否（启动后加载） |\n| ksud 的 kprobe 钩子 (execve/read/fstat/input) | 是 | 跳过 |\n| 安全模式检测（音量键） | 是 | 始终禁用 |\n| 启动日志抓取 (logcat/dmesg) | 是 | 跳过 |\n| Magisk 共存检测 | 是 | 跳过 |\n| `post-fs-data` 事件通知内核 | 是 | 跳过 |\n| `boot-completed` 事件通知内核 | 是 | 初始化时直接设置 |\n| `post-fs-data.sh` / `post-fs-data.d/` 脚本 | 是 | 由 `late-load` 阶段替代 |\n| `system.prop` 加载 | 是 | 是 |\n| OverlayFS 挂载（metamodule） | 是 | 是 |\n| `post-mount.sh` / `post-mount.d/` 脚本 | 是 | 是 |\n| `service.sh` / `service.d/` 脚本 | 是 | 是 |\n| `boot-completed.sh` / `boot-completed.d/` 脚本 | 是 | 是 |\n| `KSU_LATE_LOAD` 环境变量 | 未设置 | 设置为 `1` |\n| 内核 info 标志位 `0x4` | 未设置 | 已设置 |\n\n### 脚本执行顺序\n\n在 late-load 模式下，脚本执行顺序如下：\n\n```txt\nksud late-load:\n  1. 加载 kernelsu.ko（如果尚未加载）\n  2. 释放二进制文件、处理模块更新、加载 SELinux 规则、初始化 feature\n  3. 执行 late-load.d/ 通用脚本和模块的 late-load 脚本（阻塞）\n  4. 加载 system.prop (resetprop -n)\n  5. 执行 metamodule mount 脚本（OverlayFS 挂载）\n  6. 执行 post-mount.d/ 通用脚本和模块的 post-mount.sh（阻塞）\n  7. 执行 service.d/ 通用脚本和模块的 service.sh（非阻塞）\n  8. 执行 boot-completed.d/ 通用脚本和模块的 boot-completed.sh（非阻塞）\n```\n\n### Late-load 专用脚本\n\n模块可以提供 `late-load.sh` 脚本，该脚本**仅在 late-load 模式下运行**，作为 `post-fs-data.sh` 的替代。该脚本在 OverlayFS 挂载之前运行，与标准流程中的 `post-fs-data.sh` 时机类似。\n\n此外，通用脚本可以放置在 `/data/adb/late-load.d/` 目录下，在该阶段执行。\n\n### 在脚本中检测 late-load 模式\n\n模块可以通过 `KSU_LATE_LOAD` 环境变量检测当前是否处于 late-load 模式：\n\n```sh\nif [ \"$KSU_LATE_LOAD\" = \"1\" ]; then\n    # 当前处于 late-load 模式\n    echo \"Late-load mode detected\"\nfi\n```\n\n这使得模块可以据此调整自身行为，例如跳过仅在早期启动时才需要的操作。\n"
  },
  {
    "path": "website/docs/zh_CN/guide/rescue-from-bootloop.md",
    "content": "# 救砖 {#intruduction}\n\n在刷机的时候我们可能会遇到设备“变砖”的情况，理论上讲，如果你只是使用 fastboot 刷入 boot 分区或者安装不合适的模块导致设备无法启动，那么这都可以通过合适的操作恢复手机；本文档旨在提供一些急救方法让你可以在“变砖”中恢复。\n\n## 刷入 boot 变砖\n\n在 KernelSU 中，刷入 boot 变砖有如下可能：\n\n1. 你刷入了错误格式的 boot 镜像。比如你的手机 boot 格式是 `gz` 的，但你刷入了 `lz4` 格式的镜像，那么此时手机无法启动。\n2. 你的手机需要关闭 avb 验证才能正常启动（注意这通常意味着需要清除手机所有数据）。\n3. 你的 kernel 有某些 bug 或者你的 kernel 不适合你这个手机刷入。\n\n无论哪种情况，你都可以通过**刷入原厂 boot**恢复；因此，在安装教程最开始，我们已经强烈建议大家，在刷机之前备份自己的原厂 boot！如果你没有备份，那么你可以通过其他跟你相同设备的童鞋或者官方固件包获取原厂 boot。\n\n## 刷入模块变砖\n\n刷入模块变砖可能是大家遇到更常见的情况，但是这里必须郑重告诉大家：**请勿刷入来路不明的模块！！**。因为模块其实是有 root 权限的，它完全可能导致你的设备发生不可逆的损坏！\n\n### 普通模块变砖\n\n如果大家刷入某些开源的或者被证明是安全的模块使得手机无法启动，那么这种情况在 KernelSU 中非常容易恢复，完全无需担心。KernelSU 内置了如下两种机制来救砖：\n\n1. AB 更新\n2. 音量键救砖\n\n#### AB 更新 {#ab-update}\n\nKernelSU 的模块更新借鉴了 Android 系统 OTA 更新时的 AB 更新机制，如果你安装了新模块或者对已有模块有更新操作，不会直接操作当前使用的模块文件，而是会把所有模块构建成另外一个 update 镜像；系统重启之后，会使用这个 update 镜像尝试启动一次，如果 Android 系统成功启动，才会真正更新模块。\n\n因此，最简单最常用的救砖方法就是：**强制重启一次**。如果你在刷某个模块之后系统无法启动，你可以长按电源键超过 10 秒，系统会自动重启；重启之后会回滚到更新模块之前的状态，之前更新的模块会被自动禁用。\n\n#### 音量键救砖 {#volume-down}\n\n如果 AB 更新依然无法解决，你可以尝试使用**安全模式**。进入安全模式之后，所有的模块都会被禁用。\n\n进入安全模式的方法有两种：\n\n1. 某些系统自带的安全模式；有些系统是长按音量下，有些系统（比如MIUI）可以在 Recovery 中开启安全模式。进入系统的安全模式后，KernelSU 也会进入安全模式，自动禁用模块。\n2. KernelSU 内置的安全模式；操作方法：开机第一屏后，**连续按音量下键超过三次**。注意是按下-松开、按下-松开、按下-松开，不是按着不动。\n\n进入安全模式以后，KernelSU 管理器的模块页面所有模块都被禁用，但你可以执行“卸载”操作，卸载可能会有问题的模块。\n\n内置的安全模式是在内核里面实现的，因此不会出现按键事件被拦截导致捕获不到的情况。不过对于非 GKI 内核，可能需要手动集成代码，可以参考官网教程。\n\n### 格机或其他病毒模块变砖\n\n如果以上方法无法拯救你的设备，那么很有可能你装的模块有恶意操作或者通过其他方式损坏了你的设备，这种情况下，只有两个建议：\n\n1. 清除数据后刷入完整刷入官方系统。\n2. 咨询售后服务。\n"
  },
  {
    "path": "website/docs/zh_CN/guide/unofficially-support-devices.md",
    "content": "# 非官方支持设备\n\n::: warning\n该文档仅供存档参考，不再维护更新。\n自 KernelSU v1.0 版本之后，我们放弃了对非 GKI 设备的官方支持。\n:::\n\n::: warning\n本文档列出由其他开发者维护的支持 KernelSU 的非 GKI 设备内核\n:::\n\n::: warning\n本文档仅方便查找设备对应源码，这并不意味该源码**被** KernelSU 开发者**审查**，你应自行承担使用风险。\n:::\n\n<script setup>\nimport data from '../../repos.json'\n</script>\n\n<table>\n   <thead>\n      <tr>\n         <th>维护者</th>\n         <th>仓库地址</th>\n         <th>支持设备</th>\n      </tr>\n   </thead>\n   <tbody>\n    <tr v-for=\"repo in data\" :key=\"repo.devices\">\n        <td><a :href=\"repo.maintainer_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.maintainer }}</a></td>\n        <td><a :href=\"repo.kernel_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.kernel_name }}</a></td>\n        <td>{{ repo.devices }}</td>\n    </tr>\n   </tbody>\n</table>"
  },
  {
    "path": "website/docs/zh_CN/guide/what-is-kernelsu.md",
    "content": "# 什么是 KernelSU？ {#introduction}\n\nKernelSU 是 Android GKI 设备的 root 解决方案，它工作在内核模式，并直接在内核空间中为用户空间应用程序授予 root 权限。\n\n## 功能 {#features}\n\nKernelSU 的主要特点是它是**基于内核的**。KernelSU 运行在内核空间，所以它可以提供我们以前从未有过的内核接口。例如，我们可以在内核模式下为任何进程添加硬件断点；我们可以在任何进程的物理内存中访问，而无人知晓；我们可以在内核空间拦截任何系统调用; 等等。\n\n此外，KernelSU 提供了 [metamodule 系统](metamodule.md)，这是一个可插拔的模块管理架构。与将挂载逻辑内置到核心的传统 root 方案不同，KernelSU 将此功能委托给 metamodule。这允许您安装 [meta-overlayfs](https://github.com/tiann/KernelSU/tree/main/userspace/meta-overlayfs) 等 metamodule，以提供对 `/system` 分区和其他分区的无系统修改。\n\n## 如何使用 {#how-to-use}\n\n请参考: [安装](installation)\n\n## 如何构建 {#how-to-build}\n\n请参考: [如何构建](how-to-build)\n\n## 讨论 {#discussion}\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n"
  },
  {
    "path": "website/docs/zh_CN/index.md",
    "content": "---\nlayout: home\ntitle: Android 上的内核级的 root 方案\n\nhero:\n  name: KernelSU\n  text: Android 上的内核级的 root 方案\n  tagline: \"\"\n  image:\n    src: /logo.png\n    alt: KernelSU\n  actions:\n    - theme: brand\n      text: 开始了解\n      link: /zh_CN/guide/what-is-kernelsu\n    - theme: alt\n      text: 在 GitHub 中查看\n      link: https://github.com/tiann/KernelSU\n\nfeatures:\n  - title: 基于内核\n    details: KernelSU 运行在内核空间，对用户空间应用有更强的掌控。\n  - title: 白名单访问控制\n    details: 只有被授权的 App 才可以访问 `su`，而其他 App 无法感知其存在。\n  - title: 受限制的 root 权限\n    details: KernelSU 可以自定义 `su` 的 uid, gid, groups, capabilities 和 SELinux 规则：把 root 权限关进笼子里。\n  - title: Metamodule 模块系统\n    details: 可插拔的模块基础架构，支持无系统修改。安装 meta-overlayfs 等 metamodule 来启用模块挂载。\n\n"
  },
  {
    "path": "website/docs/zh_TW/guide/app-profile.md",
    "content": "# App Profile {#app-profile}\n\nApp Profile 是 KernelSU 提供的一種針對各種應用程式自訂其使用配置的機制。\n\n對於授予了 root 權限（即可以使用 `su`）的應用程式來說，App Profile 也可以稱為 Root Profile，它可以自訂 `su` 的 `uid`、`gid`、`groups`、` capabilities` 以及 `SELinux context` 規則，從而限制 root 使用者的權限。\n例如可以針對防火牆應用程式僅授予網路權限，而不授予檔案存取權限，針對凍結類別應用程式僅授予 shell 權限而不是直接給 root ；透過最小化權限原則**把權力關進籠子裡**。\n\n對於沒有被授予 root 權限的普通應用，App Profile 可以控制核心以及模組系統對此應用的行為；例如是否需要針對此應用程式卸載模組造成的修改等。核心和模組系統可以透過此配置決定是否要做一些類似「隱藏痕跡」類別的操作。\n\n## Root Profile {#root-profile}\n\n### UID、GID 和 groups {#uid-gid-and-groups}\n\nLinux 系統中有使用者和群組兩個概念。每個使用者都有一個使用者 ID(UID)，一個使用者可以屬於多個群組，每個群組也有群組 ID(GID)。此 ID 用於識別系統的使用者並確定使用者可以存取哪些系統資源。\n\nUID 為 0 的使用者稱為 root 使用者，GID 為 0 的群組稱為 root 群組；root 使用者群組通常擁有系統的最高權限。\n\n對於 Android 系統來說，每個應用程式都是一個單獨的使用者（不考慮 share uid 的情況），擁有一個唯一的 UID。例如 `0` 是 root 使用者，`1000` 是 `system`，`2000` 是 ADB shell，10000-19999 的是一般使用者。\n\n:::info 補充\n此處的 UID 跟 Android 系統的多使用者，或者說工作資料（Work Profile），是不同概念。工作資料實際上是對 UID 進行分片實現的，例如 10000-19999 是主使用者，110000-119999 是工作資料；他們中的任何一個普通應用都擁有自己獨有的 UID。\n:::\n\n每一個應用程式可以有若干個群組，GID 是其主要的群組，通常與 UID 一致；其他的群組稱為補充群組(groups)。某些權限是透過群組控制的，例如網路訪問，藍牙等。\n\n例如，如果我們在 ADB shell 中執行 `id` 指令，會得到以下輸出：\n\n```sh\noriole:/ $ id\nuid=2000(shell) gid=2000(shell) groups=2000(shell),1004(input),1007(log),1011(adb),1015(sdcard_rw),1028(sdcard_r),1078(ext_data_ww) (ext_obb_rw),3001(net_bt_admin),3002(net_bt),3003(inet),3006(net_bw_stats),3009(readproc),3011(uhid),3012(readreadtracefs:s05:\n```\n\n其中，UID 為`2000`，GID 也即主要組 ID 也為 `2000`；除此之外它還在許多補充組裡面，例如 `inet` 組代表可以創建 `AF_INET` 和 `AF_INET6` 的 socket（存取網路），`sdcard_rw` 代表可以讀寫 sdcard 等。\n\nKernelSU 的 Root Profile 可以自訂執行 `su` 後 root 程式的 UID, GID 和 groups。例如，你可以設定某個 root 應用程式的Root Profile 其UID 為`2000`，這表示此應用程式在使用`su` 的時候，它的實際權限是ADB Shell 等級；你可以去掉groups 中的`inet` ，這樣這個`su` 就無法存取網路。\n\n:::tip 注意\nApp Profile 只是控制 root 應用程式使用 `su` 後的權限，它並非控制應用程式本身的權限！如果應用程式本身申請了網路存取權限，那麼它即使不使用 `su` 也可以存取網路；為 `su` 去掉 `inet` 群組只是讓 `su` 無法存取網路。\n:::\n\n與應用程式透過 `su` 主動切換使用者或群組不同，Root Profile 是在核心中強制實施的，不依賴 root 應用程式的自覺行為，`su` 權限的授予完全取決於使用者而非開發者。\n\n### Capabilities {#capabilities}\n\nCapabilities 是 Linux 的一種分權機制。\n\n傳統的 UNIX 系統為了執行權限檢查，將流程分為兩類：特權程式（其等效使用者 ID 為 0，稱為超級使用者或 root）和非特權程式（其等效 UID 為非零）。特權程式會繞過所有核心權限檢查，而非特權程式則根據其憑證（通常是等校 UID、等效 GID 和補充群組清單）進行完整的權限檢查。\n\n從 Linux 2.2開始，Linux 將傳統上與超級使用者關聯的特權分解為獨立的單元，稱為 Capabilities（有的也翻譯為「權能」），它們可以獨立啟用和停用。\n\n每一個 Capability 代表一個或一類權限。例如 `CAP_DAC_READ_SEARCH` 就代表是否有能力繞過檔案讀取權限檢查和目錄讀取和執行權限檢查。如果一個有效 UID 為 `0` 的使用者（root 使用者）沒有 `CAP_DAC_READ_SEARCH` 或更高 Capalities，這表示即使它是 root 也不能​​隨意讀取檔案。\n\nKernelSU 的 Root Profile 可以自訂執行 `su` 後 root 程式的 Capabilities，從而實現只授予「部分 root 權限」。與上面介紹的UID, GID 不同，某些 root 應用就是需要 `su` 後 UID 是 `0`，此時我們可以透過限制這個 UID 為 `0` 的 root 使用者的 Capabilities，就可以限制它能夠執行的操作。\n\n:::tip 強烈建議\nLinux 的 Capability [官方文件](https://man7.org/linux/man-pages/man7/capabilities.7.html)詳細解釋了每一項 Capability 所代表的能力，如果你想要自訂Capabilities，請務必先閱讀此文件。\n:::\n\n### SELinux {#selinux}\n\nSELinux 是一種強大的強制權限存取控制（MAC）機制。它按照**預設拒絕**的原則運作：任何未經明確允許的行為都會被拒絕。\n\nSELinux 可依兩種全域模式運作：\n\n1. 寬容模式：權限拒絕事件會被記錄下來，但不會被強制執行。\n2. 強制模式：權限拒絕事件會被記錄下來**並且**強制執行。\n\n:::warning 警告\n現代的 Android 系統極度依賴 SELinux 來保障整個系統的安全性，我們強烈建議您不要使用任何以「寬容模式」運作的自訂系統，因為那樣與裸奔沒什麼區別。\n:::\n\nSELinux 的完整概念比較複雜，我們這裡不打算講解它的具體運作方式，建議你先透過以下資料來了解其運作原理：\n\n1. [wikipedia](https://en.wikipedia.org/wiki/Security-Enhanced_Linux)\n2. [Redhat: what-is-selinux](https://www.redhat.com/en/topics/linux/what-is-selinux)\n3. [ArchLinux: SELinux](https://wiki.archlinux.org/title/SELinux)\n\nKernelSU 的 Root Profile 可以自訂執行 `su` 後 root 程式的 SELinux context，並且可以針對這個 context 設定特定的存取控制規則，從而更精細地控制 root 權限。\n\n通常情況下，應用程式執行 `su` 後，會將進程切換到一個**不受任何限制** 的 SELinux 域，例如`u:r:su:s0`，透過 Root Profile，我們可以將它切換到一個自訂的作用域，例如 `u:r:app1:s0`，然後為這個作用域制定一系列規則：\n\n```sh\ntype app1\nenforce app1\ntypeattribute app1 mlstrustedsubject\nallow app1 * * *\n```\n\n注意：此處的 `allow app1 * * *` 僅僅作為示範方便而使用，實際過程中不應使用這個規則，因為它跟寬容模式區別不大。\n\n### 逃逸 {#escalation}\n\n如果 Root Profile 的配置不合理，那麼可能會發生逃逸的情況：Root Profile 的限制會意外失效。\n\n例如，如果你為ADB shell 使用者設定允許root 權限（這是相當常見的情況）；然後你給某個普通應用程式允許 root 權限，但是配置它的 root profile 中的 UID 為 2000（ADB shell 使用者的UID）；那麼此時，這個 App 可以透過執行兩次 `su` 來獲得完整的root 權限：\n\n1. 第一次執行 `su`，由於 App Profile 強制生效，會正常切換到 UID 為 `2000` (adb shell) 而非 `0` (root)。\n2. 第二次執行 `su`，由於此時它 UID 是 `2000`，而你給 `2000` (adb shell) 配置了允許 root，它會獲得完整的 root 權限！\n\n:::warning 注意\n這是完全符合預期的行為，並非 BUG！因此我們建議：\n\n如果你的確需要給 adb 授予 root 權限（例如你是開發者），那麼不建議你在配置 Root Profile 的時候將 UID 改成 `2000`，用 `1000` (system) 會更好。\n:::\n\n## Non Root Profile {#non-root-profile}\n\n### 卸載模組 {#umount-modules}\n\nKernelSU 提供了一種無須直接修改系統分區的方式 (systemless) 來修改系統分區，這是透過掛載 overlayfs 來實現的。但有些情況下，App 可能會對這種行為比較敏感；因此，我們可以透過設定「卸載模組」來卸載掛載在這些應用程式上的模組。\n\n另外，KernelSU 管理器的設定介面還提供了一個「預設卸載模組」的開關，這個開關預設是**開啟**的，這表示**如果不對應用程式做額外的設定**，預設情況下 KernelSU 或某些模組會對此應用程式執行卸載操作。當然，如果你不喜歡這個設定或這個設定會影響某些 App，你可以有以下選擇：\n\n1. 保持「預設卸載模組」的開關，然後針對不需要「卸載模組」的應用程式進行單獨的設置，在 App Profile 中關閉「卸載模組」；（相當於「白名單」）。\n2. 關閉「預設卸載模組」的開關，然後針對需要「卸載模組」的應用程式進行單獨的設置，在 App Profile 中開啟「卸載模組」；（相當於「黑名單」）。\n\n:::info 提示\nKernelSU 在 5.10 及以上內核上，內核無須任何修改就可以卸載模組；但在 5.10 以下的設備上，這個開關僅僅是一個\"設定\"，KernelSU 本身不會做任何動作，如果你希望在 5.10 以前的內核可以卸載模組，你需要將 `path_unmount` 函數向後移植到 `fs/namespace.c`，您可以在[如何為非 GKI 核心整合 KernelSU](how-to-integrate-for-non-gki.md#how-to-backport-path_unpount)獲取更多資訊。一些模組（如 ZygiskNext）也會透過這個設定決定是否需要卸載。\n:::"
  },
  {
    "path": "website/docs/zh_TW/guide/difference-with-magisk.md",
    "content": "# KernelSU 與 Magisk 的差異 {#difference-with-magisk}\n\n儘管 KernelSU 模組和 Magisk 模組之間有許多相似之處，但由於它們完全不同的實作機制，不可避免地存在一些差異；如果您想讓您的模組同時在 Magisk 和 KernelSU 上運作，那麼您必須瞭解這些差異。\n\n## 相同之處 {#similarities}\n\n- 模組檔案格式：都以 Zip 的格式組織模組，並且模組的格式幾乎相同\n- 模組安裝目錄：都位於 `/data/adb/modules`\n- 無系統修改：都支援透過模組以無系統修改的方式來更改 `/system`\n- `post-fs-data.sh`：執行階段和語義完全相同\n- `service.sh`：執行階段和語義完全相同\n- `system.prop`：完全相同\n- `sepolicy.rule`：完全相同\n- BusyBox：指令碼在 BusyBox 中以「獨立模式」執行\n\n## 不同之處 {#differences}\n\n在瞭解不同之處之前，您需要知道如何區分您的模組是在 KernelSU 還是 Magisk 中執行；在所有可以執行模組指令碼的位置 (`customize.sh`, `post-fs-data.sh`, `service.sh`)，您都可以使用環境變數 `KSU` 來區分，在 KernelSU 中，這個環境變數將被設定為 `true`。\n\n以下是一些不同之處：\n\n1. KernelSU 的模組無法在 Recovery 中安裝。\n2. KernelSU 的模組沒有內建的 Zygisk 支援 (但您可以透過 [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) 來使用 Zygisk 模組)。\n3. **模組掛載架構**：KernelSU 使用 [metamodule 系統](metamodule.md)，將掛載委託給可插拔的 metamodule(例如 `meta-overlayfs`)，而 Magisk 將掛載內建於其核心中。KernelSU 需要安裝 metamodule 才能啟用模組掛載。\n4. KernelSU 模組取代或刪除檔案與 Magisk 完全不同。KernelSU 不支援 `.replace` 方法，相反，您需要透過 `mknod filename c 0 0` 建立相同名稱的資料夾以刪除對應檔案。\n5. BusyBox 的目錄不同。KernelSU 內建的 BusyBox 在 `/data/adb/ksu/bin/busybox`，而 Magisk 在 `/data/adb/magisk/busybox`。**注意此為 KernelSU 內部行為，未來可能會變更！**\n6. KernelSU 不支援 `.replace` 檔案；但 KernelSU 支援 `REPLACE` 和 `REMOVE` 變數以移除或取代檔案與資料夾。\n7. KernelSU 新增了 `boot-completed.sh` 腳本，以便在 Android 系統啟動完成後執行某些任務。\n8. KernelSU 新增了 `post-mount.sh` 腳本，以便在模組掛載完成後執行某些任務。\n"
  },
  {
    "path": "website/docs/zh_TW/guide/faq.md",
    "content": "# 常見問題\n\n## KernelSU 是否支援我的裝置？\n\n首先，您的裝置應該能解鎖 Bootloader。如果不能，則不支援。\n\n然後在您的裝置上安裝 KernelSU 管理員並開啟它，如果它顯示 `不支援`，那麼您的裝置沒有官方支援的開箱即用的 Boot 映像；但您可以自行建置核心來源並整合 KernelSU 以繼續使用。\n\n## KernelSU 是否需要解鎖 Bootloader？\n\n當然需要。\n\n## KernelSU 是否支援模組？\n\n支援，大多數 Magisk 模組都可以在 KernelSU 上運作。但是，如果您的模組需要修改 `/system` 檔案，您需要安裝 [metamodule](metamodule.md)(如 `meta-overlayfs`)。其他模組功能無需 metamodule 即可運作。請參閱 [模組指南](module.md) 以獲取更多資訊。\n\n## KernelSU 是否支援 Xposed ？\n\n支援。[Dreamland](https://github.com/canyie/Dreamland) 和 [TaiChi](https://taichi.cool) 可以正常運作。LSPosed 可以在 [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) 的支援下正常運作。\n\n## KernelSU 支援 Zygisk 嗎？\n\nKernelSU 沒有內建 Zygisk 支援，但是您可以用 [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) 來使用 Zygisk 模組。\n\n## KernelSU 與 Magisk 相容嗎？\n\nKernelSU 的模組系統與 Magisk 的 magic mount 存在衝突，如果在 KernelSU 中啟用了任何模組，那麼整個 Magisk 將無法正常運作。\n\n但是如果您只使用 KernelSU 的 `su`，那么它會和 Magisk 一同運作：KernelSU 修改 `kernel`、Magisk 修改 `ramdisk`，它們可以搭配使用。\n\n## KernelSU 会取代 Magisk 嗎？\n\n我們不這樣認為，這也不是我們的目標。Magisk 對於使用者空間 Root 解決方案來說已經足夠優秀了，它會存在很長一段時間。KernelSU 的目標是為使用者提供核心介面，而非取代 Magisk。\n\n## KernelSU 可以支援非 GKI 裝置嗎？\n\n可以。但是您應該下載核心來源並整合 KernelSU 至來源樹狀結構並自行編譯核心。\n\n## KernelSU 支援 Android 12 以下的裝置嗎？\n\n影響 KernelSU 相容性的是裝置的核心版本，它與 Android 版本並無直接關係。唯一有關聯的是：**原廠** Android 12 的裝置，一定是 5.10 或更高的核心 (GKI 裝置)；因此結論如下：\n\n1. 原廠 Android 12 的裝置必定支援 (GKI 裝置)\n2. 舊版核心的裝置 (即使是 Android 12，也可能是舊版核心) 是相容的 (您需要自行建置核心)\n\n## KernelSU 可以支援舊版核心嗎？\n\n可以，目前最低支援到 4.14；更低的版本您需要手動移植它，歡迎 PR！\n\n## 如何為舊版核心整合 KernelSU？\n\n請參閱[指南](how-to-integrate-for-non-gki.md)\n\n## 為何我的 Android 版本為 13，但核心版本卻是 \"android12-5.10\"？\n\n核心版本與 Android 版本無關，如果您要使用 KernelSU，請一律使用**核心版本**而非 Android 版本，如果你為 \"android12-5.10\" 的裝置寫入 Android 13 的核心，等候您的將會是開機迴圈。\n\n## 我是 GKI1.0，能用 KernelSU 嗎？\n\nGKI1 與 GKI2 完全不同，所以您需要自行編譯核心。\n\n## KernelSU 支援 --mount-master/全域掛接命名空間嗎？\n\n目前沒有 (未來可能會支援)，但實際上有很多種方法手動進入全域命名空間，無需 `su` 內建支援，比如：\n\n1. `nsenter -t 1 -m sh` 可以取得一個全域 mount namespace 的 shell.\n2. 在您要執行的命令前新增 `nsenter --mount=/proc/1/ns/mnt` 即可使此命令在全域 mount namespace 下執行。KernelSU 本身也使用了 [這種方法](https://github.com/tiann/KernelSU/blob/77056a710073d7a5f7ee38f9e77c9fd0b3256576/manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt#L115)\n\n## KernelSU 可以修改 Hosts 嗎？ 我要怎麼使用 AdAway？\n當然。但是 KernelSU 沒有內建的 Hosts 支持，您可以安裝 [systemless-hosts](https://github.com/symbuzzer/systemless-hosts-KernelSU-module) 來做到這一點。\n\n## 為什麼全新安裝後模組不工作？\n\n如果您的模組需要修改 `/system` 檔案，您需要安裝 [metamodule](metamodule.md) 來掛載 `system` 目錄。其他模組功能(腳本、sepolicy、system.prop)無需 metamodule 即可運作。\n\n**解決方案**：參閱 [Metamodule 指南](metamodule.md) 獲取安裝說明。\n\n## 什麼是 metamodule，為什麼需要它？\n\nMetamodule 是一個特殊模組，為掛載常規模組提供基礎架構。請參閱 [Metamodule 指南](metamodule.md) 獲取完整說明。\n"
  },
  {
    "path": "website/docs/zh_TW/guide/hidden-features.md",
    "content": "# 隱藏功能 {#hidden-features}\n\n## .ksurc\n\n預設狀況下，`/system/bin/sh` 會載入 `/system/etc/mkshrc`。\n\n可以透過建立 `/data/adb/ksu/.ksurc` 檔案來讓 `su` 載入此檔案而非 `/system/etc/mkshrc`。"
  },
  {
    "path": "website/docs/zh_TW/guide/how-to-build.md",
    "content": "# 如何建置 KernelSU? {#how-to-build-kernelsu}\n\n::: warning\n該文件僅供存檔參考，不再維護更新。\n自 KernelSU v3.0 版本之後，為了更快的迭代和建置速度，我們放棄了對 GKI 映像模式的官方支援。推薦使用 `Ylarod/ddk` 建置 LKM 使用。\n:::\n\n首先,您需要閱讀核心建置的 Android 官方文件：\n\n1. [建置核心](https://source.android.com/docs/setup/build/building-kernels)\n2. [標準核心映像 (GKI) 發行組建](https://source.android.com/docs/core/architecture/kernel/gki-release-builds)\n\n::: warning 警告\n此文件適用於 GKI 裝置，如果您是舊版核心，請參閱[如何為非 GKI 裝置整合 KernelSU](how-to-integrate-for-non-gki)\n:::\n\n## 建置核心 {#build-kernel}\n\n### 同步核心原始碼 {#sync-the-kernel-source-code}\n\n```sh\nrepo init -u https://android.googlesource.com/kernel/manifest\nmv <kernel_manifest.xml> .repo/manifests\nrepo init -m manifest.xml\nrepo sync\n```\n\n`<kernel_manifest.xml>` 是一個可以唯一確定組建的資訊清單，您可以使用這個資訊清單進行可重新預測的組建。您需要從[標準核心映像 (GKI) 發行組建](https://source.android.com/docs/core/architecture/kernel/gki-release-builds)下載資訊清單。\n\n### 建置 {#build}\n\n請先查看[官方文件](https://source.android.com/docs/setup/build/building-kernels)。\n\n例如，我們需要建置 aarch64 核心映像：\n\n```sh\nLTO=thin BUILD_CONFIG=common/build.config.gki.aarch64 build/build.sh\n```\n\n不要忘記新增 `LTO=thin`，否則，如果您的電腦記憶體小於 24GB，建置可能會失敗。\n\n從 Android 13 開始，核心使用 `bazel` 建置：\n\n```sh\ntools/bazel build --config=fast //common:kernel_aarch64_dist\n```\n\n:::info 你可能需要知道...\n對於某些 Android 14 核心，要使 Wi-Fi/藍牙正常工作，可能需要刪除所有受 GKI 保護的匯出：\n\n```sh\nrm common/android/abi_gki_protected_exports_*\n```\n:::\n\n## 與 KernelSU 一起建置核心 {#build-kernel-with-kernelsu}\n\n如果您可以成功建置核心，那麼建置 KernelSU 就會非常輕鬆，依自己的需求在核心原始碼根目錄中執行以下任一命令：\n\n::: code-group\n\n```sh[最新 tag (穩定版本)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -\n```\n\n```sh[main 分支 (開發版本)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s main\n```\n\n```sh[選取 tag (例如 v0.5.2)]\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.5.2\n```\n\n:::\n\n然後重新建置核心，您將會得到一個帶有 KernelSU 的核心映像！\n"
  },
  {
    "path": "website/docs/zh_TW/guide/how-to-integrate-for-non-gki.md",
    "content": "# 如何為非 GKI 核心整合 KernelSU {#how-to-integrate-kernelsu-for-non-gki-kernels}\n\n::: warning\n該文件僅供存檔參考，不再維護更新。\n自 KernelSU v1.0 版本之後，我們放棄了對非 GKI 裝置的官方支援。\n:::\n\nKernelSU 可以被整合到非 GKI 核心中，現在它最低支援到核心 4.14 版本；理論上也可以支援更低的版本。\n\n由於非 GKI 核心的片段化極其嚴重，因此通常沒有統一的方法來建置它，所以我們也無法為非 GKI 裝置提供 Boot 映像。但您完全可以自行整合 KernelSU 並建置核心以繼續使用。\n\n首先，您必須有能力從您裝置的核心原始碼建置出一個可以開機並且能夠正常使用的核心，如果核心並非開放原始碼，這通常難以做到。\n\n如果您已經做好了上述準備，那有兩個方法來將 KernelSU 整合至您的核心之中。\n\n1. 藉助 `kprobe` 自動整合\n2. 手動修改核心原始碼\n\n## 使用 kprobe 整合 {#integrate-with-kprobe}\n\nKernelSU 使用 kprobe 機制來處理核心的相關 hook，如果 *kprobe* 可以在您建置的核心中正常運作，那麼建議使用這個方法進行整合。\n\n首先，把 KernelSU 新增至您的核心來源樹狀結構，再核心的根目錄執行以下命令：\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.9.5\n```\n\n:::info 公告\n[KernelSU 1.0 及更新版本不再支援非 GKI 核心](https://github.com/tiann/KernelSU/issues/1705)。最後一個支援的版本為 `v0.9.5`，請確保使用的版本正確。\n:::\n\n然後，您需要檢查您的核心是否啟用 *kprobe*，如果未啟用，則需要新增以下設定：\n\n```\nCONFIG_KPROBES=y\nCONFIG_HAVE_KPROBES=y\nCONFIG_KPROBE_EVENTS=y\n```\n\n最後，重新建置您的核心即可。\n\n如果您發現 KPROBES 仍未生效，很有可能是因為它依賴的 `CONFIG_MODULES` 並未被啟用，如果還是未生效請輸入 `make menuconfig` 搜尋 KPROBES 的其他相依性並啟用。\n\n如果您在整合 KernelSU 之後手機無法啟動，那麼很可能您的核心中 **kprobe 無法正常運作**，您需要修正這個錯誤，或者使用第二種方法。\n\n:::tip 如何檢查 kprobe 是否損毀？\n\n將 `KernelSU/kernel/ksu.c` 中的 `ksu_sucompat_init()` 和 `ksu_ksud_init()` 註解掉，如果正常開機，即 kprobe 已損毀；或者您可以手動嘗試使用 kprobe 功能，如果不正常，手機會直接重新啟動。\n:::\n\n:::info 如何為非 GKI 核心啟用卸載模組功能\n\n如果你的內核版本小於 5.10，你應該將 `path_umount` 向後移植至 `fs/namespace.c`。卸載模組功能依賴於這個函數。如果你沒有向後移植 `path_umount`，卸載模組功能將無法工作。你可以在[這裡查看更多關於 `path_unmount` 的資料](#how-to-backport-path_unpount)。 \n:::\n\n## 手動修改核心原始碼 {#manually-modify-the-kernel-source}\n\n如果 kprobe 無法正常運作 (在4.8之前可能是上游或核心的錯誤)，那您可以嘗試這種方法：\n\n首先，將 KernelSU 新增至您的原始碼樹狀結構，在核心的根目錄執行以下命令：\n\n```sh\ncurl -LSs \"https://raw.githubusercontent.com/tiann/KernelSU/main/kernel/setup.sh\" | bash -s v0.9.5\n```\n請記住，在某些裝置上，您的 `defconfig` 可能位於 `arch/arm64/configs` 中，或在其他情況下位於 `arch/arm64/configs/vendor/你的defconfig` 中。無論您使用哪個 `defconfig`，請確保使用 `CONFIG_KSU=y` 啟用KernelSU，或使用 `n` 停用它。例如，如果您選擇啟用它，則 `defconfig` 應包含以下字串：\n```conf\n# KernelSU\nCONFIG_KSU=y\n```\n\n然後，手動修改核心原始碼，您可以參閱下方的 patch：\n\n::: code-group\n\n```diff[exec.c]\ndiff --git a/fs/exec.c b/fs/exec.c\nindex ac59664eaecf..bdd585e1d2cc 100644\n--- a/fs/exec.c\n+++ b/fs/exec.c\n@@ -1890,11 +1890,14 @@ static int __do_execve_file(int fd, struct filename *filename,\n \treturn retval;\n }\n \n+extern bool ksu_execveat_hook __read_mostly;\n+extern int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv,\n+\t\t\tvoid *envp, int *flags);\n+extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,\n+\t\t\t\t void *argv, void *envp, int *flags);\n static int do_execveat_common(int fd, struct filename *filename,\n \t\t\t      struct user_arg_ptr argv,\n \t\t\t      struct user_arg_ptr envp,\n \t\t\t      int flags)\n {\n+\tif (unlikely(ksu_execveat_hook))\n+\t\tksu_handle_execveat(&fd, &filename, &argv, &envp, &flags);\n+\telse\n+\t\tksu_handle_execveat_sucompat(&fd, &filename, &argv, &envp, &flags);\n \treturn __do_execve_file(fd, filename, argv, envp, flags, NULL);\n }\n```\n```diff[open.c]\ndiff --git a/fs/open.c b/fs/open.c\nindex 05036d819197..965b84d486b8 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -348,6 +348,8 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn ksys_fallocate(fd, mode, offset, len);\n }\n \n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t int *flags);\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -355,6 +357,7 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n  */\n long do_faccessat(int dfd, const char __user *filename, int mode)\n {\n \tconst struct cred *old_cred;\n \tstruct cred *override_cred;\n \tstruct path path;\n \tstruct inode *inode;\n \tstruct vfsmount *mnt;\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n \n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n \n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n```diff[read_write.c]\ndiff --git a/fs/read_write.c b/fs/read_write.c\nindex 650fc7e0f3a6..55be193913b6 100644\n--- a/fs/read_write.c\n+++ b/fs/read_write.c\n@@ -434,10 +434,14 @@ ssize_t kernel_read(struct file *file, void *buf, size_t count, loff_t *pos)\n }\n EXPORT_SYMBOL(kernel_read);\n \n+extern bool ksu_vfs_read_hook __read_mostly;\n+extern int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr,\n+\t\t\tsize_t *count_ptr, loff_t **pos);\n ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)\n {\n \tssize_t ret;\n \n+\tif (unlikely(ksu_vfs_read_hook))\n+\t\tksu_handle_vfs_read(&file, &buf, &count, &pos);\n+\n \tif (!(file->f_mode & FMODE_READ))\n \t\treturn -EBADF;\n \tif (!(file->f_mode & FMODE_CAN_READ))\n```\n```diff[stat.c]\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 376543199b5a..82adcef03ecc 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -148,6 +148,8 @@ int vfs_statx_fd(unsigned int fd, struct kstat *stat,\n }\n EXPORT_SYMBOL(vfs_statx_fd);\n \n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+\n /**\n  * vfs_statx - Get basic and extra attributes by filename\n  * @dfd: A file descriptor representing the base dir for a relative filename\n@@ -170,6 +172,7 @@ int vfs_statx(int dfd, const char __user *filename, int flags,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW | LOOKUP_AUTOMOUNT;\n \n+\tksu_handle_stat(&dfd, &filename, &flags);\n \tif ((flags & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t       AT_EMPTY_PATH | KSTAT_QUERY_FLAGS)) != 0)\n \t\treturn -EINVAL;\n```\n\n:::\n\n主要修改四個項目：\n\n1. do_faccessat，通常位於 `fs/open.c`\n2. do_execveat_common，通常位於 `fs/exec.c`\n3. vfs_read，通常位於 `fs/read_write.c`\n4. vfs_statx，通常位於 `fs/stat.c`\n\n如果您的核心沒有 `vfs_statx`，使用 `vfs_fstatat` 將其取代：\n\n```diff\ndiff --git a/fs/stat.c b/fs/stat.c\nindex 068fdbcc9e26..5348b7bb9db2 100644\n--- a/fs/stat.c\n+++ b/fs/stat.c\n@@ -87,6 +87,8 @@ int vfs_fstat(unsigned int fd, struct kstat *stat)\n }\n EXPORT_SYMBOL(vfs_fstat);\n \n+extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags);\n+\n int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \t\tint flag)\n {\n@@ -94,6 +96,8 @@ int vfs_fstatat(int dfd, const char __user *filename, struct kstat *stat,\n \tint error = -EINVAL;\n \tunsigned int lookup_flags = 0;\n \n+\tksu_handle_stat(&dfd, &filename, &flag);\n+\n \tif ((flag & ~(AT_SYMLINK_NOFOLLOW | AT_NO_AUTOMOUNT |\n \t\t      AT_EMPTY_PATH)) != 0)\n \t\tgoto out;\n```\n\n對於早於 4.17 的核心，如果沒有 `do_faccessat`，可以直接找到 `faccessat` 系統呼叫的定義並進行修改：\n\n```diff\ndiff --git a/fs/open.c b/fs/open.c\nindex 2ff887661237..e758d7db7663 100644\n--- a/fs/open.c\n+++ b/fs/open.c\n@@ -355,6 +355,9 @@ SYSCALL_DEFINE4(fallocate, int, fd, int, mode, loff_t, offset, loff_t, len)\n \treturn error;\n }\n \n+extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode,\n+\t\t\t        int *flags);\n+\n /*\n  * access() needs to use the real uid/gid, not the effective uid/gid.\n  * We do this by temporarily clearing all FS-related capabilities and\n@@ -370,6 +373,8 @@ SYSCALL_DEFINE3(faccessat, int, dfd, const char __user *, filename, int, mode)\n \tint res;\n \tunsigned int lookup_flags = LOOKUP_FOLLOW;\n \n+\tksu_handle_faccessat(&dfd, &filename, &mode, NULL);\n+\n \tif (mode & ~S_IRWXO)\t/* where's F_OK, X_OK, W_OK, R_OK? */\n \t\treturn -EINVAL;\n```\n\n### 安全模式 {#safe-mode}\n\n若要啟用 KernelSU 內建的安全模式，您還需要修改 `drivers/input/input.c` 中的 `input_handle_event` 方法：\n\n:::tip 小建議\n強烈建議啟用此功能，如果遇到開機迴圈，這將會非常有用！\n:::\n\n```diff\ndiff --git a/drivers/input/input.c b/drivers/input/input.c\nindex 45306f9ef247..815091ebfca4 100755\n--- a/drivers/input/input.c\n+++ b/drivers/input/input.c\n@@ -367,10 +367,13 @@ static int input_get_disposition(struct input_dev *dev,\n \treturn disposition;\n }\n \n+extern bool ksu_input_hook __read_mostly;\n+extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value);\n+\n static void input_handle_event(struct input_dev *dev,\n \t\t\t       unsigned int type, unsigned int code, int value)\n {\n\tint disposition = input_get_disposition(dev, type, code, &value);\n+\n+\tif (unlikely(ksu_input_hook))\n+\t\tksu_handle_input_handle_event(&type, &code, &value);\n \n \tif (disposition != INPUT_IGNORE_EVENT && type != EV_SYN)\n \t\tadd_input_randomness(type, code, value);\n```\n\n:::info 不小心進入安全模式？\n如果您使用手動整合且不停用 `CONFIG_KPROBES`，那麼您將可能會在啟動後透過按下音量來減少按鈕來觸發安全模式！因此，如果使用手動集成，您需要停用 `CONFIG_KPROBES` ！\n:::\n\n### 無法在終端中執行 `pm` ? {#failed-to-execute-pm-in-terminal}\n\n你應該修改 `fs/devpts/inode.c`，參考:\n\n```diff\ndiff --git a/fs/devpts/inode.c b/fs/devpts/inode.c\nindex 32f6f1c68..d69d8eca2 100644\n--- a/fs/devpts/inode.c\n+++ b/fs/devpts/inode.c\n@@ -602,6 +602,8 @@ struct dentry *devpts_pty_new(struct pts_fs_info *fsi, int index, void *priv)\n        return dentry;\n }\n\n+#ifdef CONFIG_KSU\n+extern int ksu_handle_devpts(struct inode*);\n+#endif\n+\n /**\n  * devpts_get_priv -- get private data for a slave\n  * @pts_inode: inode of the slave\n@@ -610,6 +612,7 @@ struct dentry *devpts_pty_new(struct pts_fs_info *fsi, int index, void *priv)\n  */\n void *devpts_get_priv(struct dentry *dentry)\n {\n+       #ifdef CONFIG_KSU\n+       ksu_handle_devpts(dentry->d_inode);\n+       #endif\n        if (dentry->d_sb->s_magic != DEVPTS_SUPER_MAGIC)\n                return NULL;\n        return dentry->d_fsdata;\n```\n\n### 如何向後移植 path_umount {#how-to-backport-path_unpount}\n\n你可以透過向後移植 `path_umount` 來讓卸載模組功能在低於 5.10 的非 GKI 核心上運作。你可以參考這個修改:\n\n```diff\n--- a/fs/namespace.c\n+++ b/fs/namespace.c\n@@ -1739,6 +1739,39 @@ static inline bool may_mandlock(void)\n }\n #endif\n\n+static int can_umount(const struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\n+\tif (flags & ~(MNT_FORCE | MNT_DETACH | MNT_EXPIRE | UMOUNT_NOFOLLOW))\n+\t\treturn -EINVAL;\n+\tif (!may_mount())\n+\t\treturn -EPERM;\n+\tif (path->dentry != path->mnt->mnt_root)\n+\t\treturn -EINVAL;\n+\tif (!check_mnt(mnt))\n+\t\treturn -EINVAL;\n+\tif (mnt->mnt.mnt_flags & MNT_LOCKED) /* Check optimistically */\n+\t\treturn -EINVAL;\n+\tif (flags & MNT_FORCE && !capable(CAP_SYS_ADMIN))\n+\t\treturn -EPERM;\n+\treturn 0;\n+}\n+\n+int path_umount(struct path *path, int flags)\n+{\n+\tstruct mount *mnt = real_mount(path->mnt);\n+\tint ret;\n+\n+\tret = can_umount(path, flags);\n+\tif (!ret)\n+\t\tret = do_umount(mnt, flags);\n+\n+\t/* we mustn't call path_put() as that would clear mnt_expiry_mark */\n+\tdput(path->dentry);\n+\tmntput_no_expire(mnt);\n+\treturn ret;\n+}\n /*\n  * Now umount can handle mount points as well as block devices.\n  * This is important for filesystems which use unnamed block devices.\n```\n\n最後，再次建置您的核心，KernelSU 將會如期運作。\n"
  },
  {
    "path": "website/docs/zh_TW/guide/installation.md",
    "content": "# 安裝 {#title}\n\n## 檢查您的裝置是否受支援 {#check-if-your-device-is-supported}\n\n從 [GitHub Releases](https://github.com/tiann/KernelSU/releases) 下載 KernelSU 管理器，然後安裝至裝置並開啟：\n\n- 如果顯示「不支援」，則表示您的裝置不支援 KernelSU，您需要自行編譯核心才能繼續使用，KernelSU 官方也永遠不會提供一個您可以寫入的 Boot 映像。\n- 如果顯示「未安裝」，那麼 KernelSU 支援您的裝置。\n\n::: info 提示\n對於顯示「不支援」的裝置，這裡有一個[非官方支援裝置清單](unofficially-support-devices.md)，您可以使用這個清單裡的核心自行編譯。\n:::\n\n## 備份您的原廠 boot.img {#backup-stock-boot-img}\n\n在寫入核心映像前，您必須預先備份您的原廠 boot.img。如果您在後續寫入中出現了任何問題，您都可以透過使用 Fastboot 寫回原廠 Boot 以還原系統。\n\n::: warning 警告\n寫入核心映像可能會造成資料遺失，請確保做好這一步再繼續進行下一步作業！！必要時您還可以備份您手機的所有資料。\n:::\n\n## 必要知識 {#necessary-knowledge}\n\n### ADB 和 Fastboot {#adb-and-fastboot}\n\n預設狀況下，您將會使用 ADB 和 Fastboot 工具，如果您不知道它們，建議使用搜尋引擎先瞭解相關內容。\n\n### KMI\n\nKMI 全稱 Kernel Module Interface，相同 KMI 的核心版本是**相容的**，這也是 GKI 中「標準」的涵義所在。反之，如果 KMI 不同，那麼這些核心之間無法彼此相容，寫入與您裝置 KMI 不同的核心映像可能會導致無法開機。\n\n具體來講，對於 GKI 的裝置，其核心版本格式應該如下：\n\n```txt\nKernelRelease :=\nVersion.PatchLevel.SubLevel-AndroidRelease-KmiGeneration-suffix\nw      .x         .y       -zzz           -k            -something\n```\n\n其中，`w.x-zzz-k` 為 KMI 版本。例如，一部裝置核心版本為 `5.10.101-android12-9-g30979850fc20`，那麼它的 KMI 為 `5.10-android12-9`，理論上寫入其他這個 KMI 的核心也能正常開機。\n\n::: tip 補充\n請注意，核心版本中的 SubLevel 並非 KMI 的一部分！也就是說 `5.10.101-android12-9-g30979850fc20` 與 `5.10.137-android12-9-g30979850fc20` 的 KMI 相同！\n:::\n\n### 安全性修補程式等級 {#security-patch-level}\n\n較新的 Android 裝置可能具有防回滾機制，不允許寫入具有較舊安全性修補程式等級的啟動映像。例如，如果您的裝置核心為 `5.10.101-android12-9-g30979850fc20`，則其安全修補程式等級為 `2023-11`；即使寫入了 KMI 對應的核心，如果安全修補程式等級早於 `2023-11`（例如 `2023-06`），也可能會導致無法開機。\n\n因此，最好使用具有最新安全性修補程式等級的核心來維護與 KMI 的對應關係。\n\n### 核心版本與 Android 版本 {#kernel-version-vs-android-version}\n\n請注意：**核心版本與 Android 版本並不一定相同！**\n\n如果您發現您的核心版本是 `android12-5.10.101`，然而您 Android 系統的版本為 Android 13 或更高，請不要覺得奇怪，因為 Android 系統的版本與 Linux 核心的版本號碼並非一致。Linux 核心的版本號碼一般與**裝置出廠時隨附的 Android 系統的版本一致**，如果後續 Android 系統更新，核心版本一般不會發生變化。如果您需要寫入，**請以核心版本為準！！**\n\n## 安裝簡介 {#introduction}\n\n自 `0.9.0` 版本以後，在 GKI 裝置上，KernelSU 支援兩種運作模式：\n\n1. `GKI`：使用**通用核心鏡像**（GKI）取代掉裝置原有的核心。\n2. `LKM`：使用**可載入核心模組**（LKM）的方式載入到裝置核心中，不會替換掉裝置原有的核心。\n\n這兩種方式適用於不同的場景，你可以根據自己的需求選擇。\n\n### GKI 模式 {#gki-mode}\n\nGKI 模式會替換掉裝置原有的核心，使用 KernelSU 提供的通用核心鏡像。 GKI 模式的優點是：\n\n1. 通用型高，適用於大多數裝置；例如開啟了 KNOX 的三星裝置、或是 LKM 模式無法運作的裝置。還有一些冷門的魔改裝置，也只能使用 GKI 模式。\n2. 不依賴官方韌體即可使用；不需要等待官方韌體更新，只要 KMI 一致，就可以使用。\n\n### LKM 模式 {#lkm-mode}\n\nLKM 模式不會替換掉裝置原有的核心，而是使用可載入核心模組的方式載入到裝置核心中。 LKM 模式的優點是：\n\n1. 不會取代裝置原有的核心：如果你對裝置原有的核心有特殊需求，或是你希望在使用第三方核心的同時使用 KernelSU，可以使用 LKM 模式。\n2. 升級和 OTA 較為方便：升級 KernelSU 時，可以直接在管理器內部安裝，無需再手動寫入；系統 OTA 後，可以直接安裝到第二個槽位，也無需再手動寫入。\n3. 適用於一些特殊場景：例如使用臨時 root 權限也可以載入 LKM，由於不需要替換 boot 分區，因此不會觸發 avb，不會使裝置意外變磚。\n4. LKM 可以被暫時卸載：如果你暫時想取消 root，可以卸載 LKM，這個過程不需要寫入分區，甚至也不用重啟裝置。如果你想重新取得 root，只需要重啟裝置即可。\n\n:::tip 兩種模式共存\n打開管理器後，你可以在首頁看到裝置目前運行的模式。注意 GKI 模式的優先級高於 LKM ，如你既使用 GKI 核心替換掉了原有的核心，又使用 LKM 的方式修補了 GKI 核心，那麼 LKM 會被忽略，裝置將永遠以 GKI 的模式運作。\n:::\n\n### 選哪個？ {#which-one}\n\n如果你的裝置是手機，我們建議您優先考慮 LKM 模式。\n如果你的裝置是模擬器、WSA 或 Waydroid 等，我們建議您優先考慮 GKI 模式。\n\n## LKM 安裝 {#lkm-installation}\n\n### 取得官方韌體 {#get-the-official-firmware}\n\n使用 LKM 的模式，需要取得官方韌體，然後在官方韌體的基礎上修補；如果你使用的是第三方核心，可以把第三方核心的 boot.img 作為官方韌體。\n\n取得官方韌體的方法有很多，如果你的裝置支援 `fastboot boot`，那麼我們最推薦以及最簡單的方法是使用 `fastboot boot` 臨時啟動 KernelSU 提供的 GKI 核心，並參考[使用管理器](#use-the-manager)安裝。\n\n如果你的裝置不支援 `fastboot boot`，那麼你可能需要手動去下載官方韌體包，然後從中提取 boot。\n\n與 GKI 模式不同，LKM 模式會修改 `ramdisk`，因此在出廠 Android 13 的裝置上，通常它需要修補的是 `init_boot` 分區而非 `boot` 分區；而 GKI 模式則永遠是修改 `boot` 分區。\n\n### 使用管理器 {#use-the-manager}\n\n開啟管理器，點選右上角的安裝圖標，會出現若干個選項：\n\n1. 選擇並修補一個文件：如果你手機目前沒有 root 權限，你可以選擇這個選項，然後選擇你的官方韌體，管理器會自動修補它。你只需要寫入這個修補後的文件，即可永久取得 root 權限。\n2. 直接安裝：如果你手機已經 root，你可以選擇這個選項，管理器會自動獲取你的裝置資訊，然後自動修補官方韌體，然後寫入。你可以考慮使用 `fastboot boot` KernelSU 的 GKI 核心來取得臨時 root 安裝管理器，然後再使用這個選項。**這種方式也是 KernelSU 升級最主要的方式**。\n3. 安裝到另一個分割區：如果你的裝置支援 A/B 分區，你可以選擇這個選項，管理器會自動修補官方韌體，然後安裝到另一個分區。這種方式適用於 OTA 後的裝置，你可以在 OTA 後直接安裝到另一個分割區，然後重新啟動裝置即可。\n\n### 使用命令列{#use-the-command-line}\n\n如果你不想使用管理器，你也可以使用命令列來安裝 LKM。KernelSU 提供的 `ksud` 可以幫助你快速修補官方韌體，然後寫入。\n\n這個工具支援 macOS、Linux 和 Windows，你可以在 [GitHub Release](https://github.com/tiann/KernelSU/releases) 下載對應的版本。\n\n使用方法：`ksud boot-patch`。 你可以查看命令列的提示了解具體的使用方法。\n\n```sh\nhusky:/ # ksud boot-patch -h\nPatch boot or init_boot images to apply KernelSU\n\nUsage: ksud boot-patch [OPTIONS]\n\nOptions:\n  -b, --boot <BOOT>              boot image path, if not specified, will try to find the boot image automatically\n  -k, --kernel <KERNEL>          kernel image path to replace\n  -m, --module <MODULE>          LKM module path to replace, if not specified, will use the builtin one\n  -i, --init <INIT>              init to be replaced\n  -u, --ota                      will use another slot when boot image is not specified\n  -f, --flash                    Flash it to boot partition after patch\n  -o, --out <OUT>                output path, if not specified, will use current directory\n      --magiskboot <MAGISKBOOT>  magiskboot path, if not specified, will use builtin one\n      --kmi <KMI>                KMI version, if specified, will use the specified KMI\n  -h, --help                     Print help\n```\n需要說明的幾個選項：\n1. `--magiskboot` 選項可以指定 magiskboot 的路徑，如果不指定，ksud 會在環境變數中尋找。如果你不知道如何取得 magiskboot，可以參考[這裡](#patch-boot-image)。\n2. `--kmi` 選項可以指定 `KMI` 版本，如果你的裝置核心名字沒有遵循 KMI 規範，你可以透過這個選項來指定。\n\n最常見的使用方法為：\n```sh\nksud boot-patch -b <boot.img> --kmi android13-5.10\n```\n## GKI 安裝{#gki-mode-installation}\nGKI 的安裝方式有以下幾種，各自適用於不同的場景，請依需求選擇：\n\n1. 使用 KernelSU 提供的 boot.img 透過 Fastboot 安裝\n2. 使用核心寫入程式 (例如 KernelFlasher) 安裝\n3. 使用自訂 Recovery (例如 TWRP) 安裝\n4. 手動修補 boot.img 並安裝\n\n## 使用 KernelSU 提供的 boot.img 安裝 {#install-with-boot-img-provided-by-kernelsu}\n\n如果你的裝置的 `boot.img` 使用常見的壓縮格式，你可以直接寫入 KernelSU 提供的 GKI 核心映像，這種方法無需 TWRP，也無需您的手機有 Root 權限；適用於您初次安裝 KernelSU。\n\n### 找到合適的 boot.img {#find-proper-boot-img}\n\nKernelSU 為 GKI 裝置提供了標準 boot.img，您需要將 boot.img 寫入至裝置的 Boot 分區。\n\n您可以從 [GitHub Release](https://github.com/tiann/KernelSU/releases) 下載 boot.img，請注意，您應該使用正確版本的 boot.img。如果你不知道你該下載哪個檔案，請詳細閱讀文檔中的 [KMI](#kmi) 與[安全性修補程式等級](#security-patch-level)。\n\n通常，在相同的 KMI 和安全性修補程式等級下，會存在三種不同格式的啟動檔案。除了核心壓縮格式之外，它們都是相同的。請檢查您原來的 boot.img 的核心壓縮格式。您應該使用正確的格式，例如 `lz4` 、 `gz`，如果你使用了不正確的壓縮格式，你可能會在寫入後無法開機。\n\n::: info 關於 boot.img 的壓縮格式\n1. 您可以透過 magiskboot 以取得您的原始 Boot 的壓縮格式。當然，您也可以詢問與您相同型號的其他更有經驗的使用者。另外，核心的壓縮格式通常不會出現變更，如果您使用的某個壓縮格式成功開機，後續可以優先嘗試這個格式。\n2. 小米裝置通常 `gz` 或者 **不壓縮**。\n3. Pixel 裝置有些特殊，請遵循下方的指示。\n:::\n\n### 將 boot.img 寫入至裝置 {#flash-boot-img-to-device}\n\n使用 `adb` 連接您的裝置，然後執行 `adb reboot bootloader` 進入 fastboot 模式，然後使用此命令寫入 KernelSU：\n\n```sh\nfastboot flash boot boot.img\n```\n\n::: info 提示\n如果您的裝置支援 `fastboot boot`，可以先使用 `fastboot boot boot.img` 來嘗試使用 boot.img 開機進入系統，如果出現意外，重新啟動即可開機。\n:::\n\n### 重新開機 {#reboot}\n\n寫入完成後，您應該重新啟動您的裝置：\n\n```sh\nfastboot reboot\n```\n\n## 使用核心寫入程式安裝 {#install-with-kernel-flasher}\n\n先決條件：您的裝置必須已經 Root。例如您已經安裝了 Magisk 並取得 Root 存取權，或者您已經安裝了舊版本的 KernelSU 需升級到其他版本的 KernelSU；如果您的裝置並未 Root，請嘗試其他方法。\n\n步驟：\n\n1. 下載 AnyKernel3 的 Zip 檔。如果你不知道你該下載哪個檔案，請詳細閱讀文檔中的 [KMI](#kmi) 與[安全性修補程式等級](#security-patch-level)。\n2. 開啟核心寫入程式提供的 AnyKernel3 Zip 檔案並寫入核心。\n\n如果您先前並未使用過核心寫入應用程式，可以嘗試下面幾個：\n\n1. [Kernel Flasher](https://github.com/capntrips/KernelFlasher/releases)\n2. [Franco Kernel Manager](https://play.google.com/store/apps/details?id=com.franco.kernel)\n3. [Ex Kernel Manager](https://play.google.com/store/apps/details?id=flar2.exkernelmanager)\n\nP.S. 這種方法在更新 KernelSU 時比較方便，無需電腦即可完成 (注意備份！)。\n\n## 手動修補 boot.img {#patch-boot-image}\n\n對於某些裝置來說，其 boot.img 格式並不是很常見，不屬於 `lz4`，`gz` 和未壓縮；最典型的就是 Pixel，它的 boot.img 格式是 `lz4_legacy` 壓縮，ramdisk 可能是 `gz` 也可能是 `lz4_legacy` 壓縮；此時如果您直接寫入 KernelSU 提供的 boot.img，手機可能無法開機。這時，您可以透過手動修補 boot.img 來完成。\n\n永遠建議使用 `magiskboot` 來修補映像，一般有兩種修補方法：\n\n1. [magiskboot](https://github.com/topjohnwu/Magisk/releases)\n2. [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci)\n\n其中，官方的 `magiskboot` 僅能在 Android 上使用，若您想在電腦上完成，可以嘗試第二個選項。\n\n### 準備 {#preparation}\n\n1. 取得您手機的原廠 boot.img，您可以從您的裝置製造商取得，您也可能需要 [payload-dumper-go](https://github.com/ssut/payload-dumper-go)。\n2. 下載 KernelSU 提供的與您的裝置 KMI 一致的 AnyKernel3 Zip 檔 (可參閱[使用自訂 Recovery 安裝](#install-with-custom-recovery))。\n3. 解壓縮 AnyKernel3 Zip 檔，取得其中的 `Image` 檔，此檔案為具有 KernelSU 的核心。\n\n### 在 Android 上使用 magiskboot {#using-magiskboot-on-Android-devices}\n\n1. 在 Magisk 的 [Release 頁面](https://github.com/topjohnwu/Magisk/releases) 下載最新的 Magisk。\n2. 將 `Magisk-*(version).apk` 重新命名為 `Magisk-*.zip` 並解壓縮。\n3. 使用 Adb 將 magiskboot 推入至手機：`adb push Magisk-*/lib/arm64-v8a/libmagiskboot.so /data/local/tmp/magiskboot`。\n4. 使用 Adb 將原廠 boot.img 和 AnyKernel3 中的 Image 推入至手機。\n5. adb shell 進入 /data/local/tmp/ 目錄，然後賦予先前推入的檔案可執行權限 `chmod +x magiskboot`。\n6. adb shell 進入 /data/local/tmp/ 目錄，執行 `./magiskboot unpack boot.img` 此時會將 `boot.img` 解除封裝，得到一個名為 `kernel` 的檔案，這個檔案是您的原廠核心。\n7. 使用 `Image` 取代 `kernel`: `mv -f Image kernel`\n8. 執行 `./magiskboot repack boot.img` 重新封裝映像，此時您會得到一個 `new-boot.img` 檔案，透過 Fastboot 將這個檔案寫入至裝置即可。\n\n### 在 Windows/macOS/Linux PC 上使用 magiskboot {#using-magiskboot-on-PC}\n\n1. 在 [magiskboot_build](https://github.com/ookiineko/magiskboot_build/releases/tag/last-ci) 下載對應的 magiskboot。\n2. (僅linux)賦予檔案可執行權限 `chmod +x magiskboot`。\n3. 執行 `./magiskboot unpack boot.img` 此時會將 `boot.img` 解除封裝，得到一個名為 `kernel` 的檔案，這個檔案是您的原廠核心。\n4. 使用 `Image` 取代 `kernel`: `mv -f Image kernel`\n5. 執行 `./magiskboot repack boot.img` 重新封裝映像，此時您會得到一個 `new-boot.img` 檔案，透過 Fastboot 將這個檔案寫入至裝置即可。\n\n## 使用自訂 Recovery 安裝 {#install-with-custom-recovery}\n\n先決條件：您的裝置必須有自訂的 Recovery，例如 TWRP。如果沒有或者只有官方 Recovery，請使用其他方法。\n\n步驟：\n\n1. 在 KernelSU 的 [Release 頁面](https://github.com/tiann/KernelSU/releases) 下載與您手機版本相符的以 AnyKernel3 開頭的 Zip 檔；例如，手機核心版本為 `android12-5.10.66`，那麼您應該下載 `AnyKernel3-android12-5.10.66_yyyy-MM.zip` 這個檔案 (其中 `yyyy` 為年份，`MM` 為月份)。\n2. 重新開機手機至 TWRP。\n3. 使用 Adb 將 AnyKernel3-*.zip 放置到手機 `/sdcard` 然後在 TWRP 圖形使用者介面選擇並安裝；或者您也可以直接 `adb sideload AnyKernel-*.zip` 安裝。\n\nPS. 這種方法適用於任何狀況下的安裝 (不限於初次安裝或後續更新)，只要您用 TWRP 就可以進行作業。\n\n## GKI的其他替代方法 {#other-methods}\n\n其實所有這些安裝方法的主旨只有一個，那就是**將原廠核心取代為 KernelSU 提供的核心**。只要能實現這個目的，就可以安裝，比如以下是其他可行的方法：\n\n1. 首先安裝 Magisk，透過 Magisk 取得 Root 權限後使用核心寫入程式寫入 KernelSU 的 AnyKernel Zip。\n2. 使用某些 PC 上的寫入工具組寫入 KernelSU 提供的核心。\n\n但是，如果不起作用，請嘗試 Magiskboot 方法。\n\n## 安裝後：模組支援 {#post-installation}\n\n::: warning 用於系統檔案修改的 METAMODULE\n如果您想使用修改 `/system` 檔案的模組，您需要在安裝 KernelSU 後安裝 **metamodule**。僅使用腳本、sepolicy 或 system.prop 的模組無需 metamodule 即可運作。\n:::\n\n**若需 `/system` 修改支援**，請參閱 [Metamodule 指南](metamodule.md)以：\n- 了解什麼是 metamodule 以及為何需要它\n- 安裝官方 `meta-overlayfs` metamodule\n- 了解其他 metamodule 選項\n\n"
  },
  {
    "path": "website/docs/zh_TW/guide/metamodule.md",
    "content": "# 元模組\n\n元模組是 KernelSU 的一項革命性功能,它將關鍵的模組系統能力從核心守護程序轉移到可插拔模組中。這種架構轉變在保持 KernelSU 穩定性和安全性的同時,為模組生態系統釋放了更大的創新潛力。\n\n## 什麼是元模組?\n\n元模組是一種特殊類型的 KernelSU 模組,為模組系統提供核心基礎設施功能。與修改系統檔案的常規模組不同,元模組控制常規模組的*安裝和掛載方式*。\n\n元模組是一種基於外掛的擴充機制,允許完全自訂 KernelSU 的模組管理基礎設施。透過將掛載和安裝邏輯委託給元模組,KernelSU 避免成為脆弱的檢測點,同時支援多樣化的實作策略。\n\n**主要特徵:**\n\n- **基礎設施角色**: 元模組提供常規模組依賴的服務\n- **單實例**: 一次只能安裝一個元模組\n- **優先執行**: 元模組腳本在常規模組腳本之前執行\n- **特殊鉤子**: 提供三個用於安裝、掛載和清理的鉤子腳本\n\n## 為什麼需要元模組?\n\n傳統的 Root 解決方案將掛載邏輯內建在核心中,這使得它們更容易被檢測且難以演進。KernelSU 的元模組架構透過關注點分離解決了這些問題。\n\n**策略優勢:**\n\n- **減少檢測面**: KernelSU 本身不執行掛載,減少了檢測向量\n- **穩定性**: 核心守護程序保持穩定,而掛載實作可以不斷演進\n- **創新性**: 社群可以開發替代掛載策略,而無需分叉 KernelSU\n- **選擇性**: 使用者可以選擇最適合其需求的實作\n\n**掛載靈活性:**\n\n- **無掛載**: 對於僅使用無掛載模組的使用者,完全避免掛載開銷\n- **OverlayFS 掛載**: 傳統方法,支援讀寫層(透過 `meta-overlayfs`)\n- **魔術掛載**: Magisk 相容掛載,以獲得更好的應用程式相容性\n- **自訂實作**: 基於 FUSE 的覆蓋層、自訂 VFS 掛載或全新方法\n\n**超越掛載:**\n\n- **可擴充性**: 新增核心模組支援等功能,無需修改核心 KernelSU\n- **模組化**: 獨立於 KernelSU 版本更新實作\n- **客製化**: 為特定裝置或用例建立專門的解決方案\n\n::: warning 重要\n如果沒有安裝元模組,模組將**不會**被掛載。新安裝的 KernelSU 需要安裝元模組(如 `meta-overlayfs`)才能使模組正常運作。\n:::\n\n## 對於使用者\n\n### 安裝元模組\n\n像安裝常規模組一樣安裝元模組:\n\n1. 下載元模組 ZIP 檔案(例如 `meta-overlayfs.zip`)\n2. 開啟 KernelSU Manager 應用程式\n3. 點擊浮動操作按鈕(➕)\n4. 選擇元模組 ZIP 檔案\n5. 重新啟動裝置\n\n`meta-overlayfs` 元模組是官方參考實作,提供傳統的基於 overlayfs 的模組掛載,支援 ext4 映像。\n\n### 檢查活動的元模組\n\n您可以在 KernelSU Manager 應用程式的模組頁面中檢視目前活動的元模組。活動的元模組將顯示在模組清單中,並帶有特殊標識。\n\n### 解除安裝元模組\n\n::: danger 警告\n解除安裝元模組會影響**所有**模組。移除後,模組將不再被掛載,直到您安裝另一個元模組。\n:::\n\n解除安裝步驟:\n\n1. 開啟 KernelSU Manager\n2. 在模組清單中找到元模組\n3. 點擊解除安裝(您會看到特殊警告)\n4. 確認操作\n5. 重新啟動裝置\n\n解除安裝後,如果您希望模組繼續運作,應該安裝另一個元模組。\n\n### 單元模組約束\n\n一次只能安裝一個元模組。如果您嘗試安裝第二個元模組,KernelSU 將阻止安裝以避免衝突。\n\n切換元模組的步驟:\n\n1. 解除安裝所有常規模組\n2. 解除安裝目前元模組\n3. 重新啟動\n4. 安裝新元模組\n5. 重新安裝常規模組\n6. 再次重新啟動\n\n## 對於模組開發者\n\n如果您正在開發常規 KernelSU 模組,您不需要太擔心元模組。只要使用者安裝了相容的元模組(如 `meta-overlayfs`),您的模組就能正常運作。\n\n**您需要知道的:**\n\n- **掛載需要元模組**: 模組中的 `system` 目錄只有在使用者安裝了提供掛載功能的元模組時才會被掛載\n- **無需變更程式碼**: 現有模組無需修改即可繼續運作\n\n::: tip\n如果您熟悉 Magisk 模組開發,您的模組在安裝元模組後將在 KernelSU 中以相同方式運作,因為它提供了 Magisk 相容的掛載。\n:::\n\n## 對於元模組開發者\n\n建立元模組允許您自訂 KernelSU 處理模組安裝、掛載和解除安裝的方式。\n\n### 基本要求\n\n元模組透過 `module.prop` 中的特殊屬性來識別:\n\n```txt\nid=my_metamodule\nname=My Custom Metamodule\nversion=1.0\nversionCode=1\nauthor=Your Name\ndescription=Custom module mounting implementation\nmetamodule=1\n```\n\n`metamodule=1`(或 `metamodule=true`)屬性將此模組標記為元模組。沒有此屬性,模組將被視為常規模組。\n\n### 檔案結構\n\n元模組結構:\n\n```txt\nmy_metamodule/\n├── module.prop              (必須包含 metamodule=1)\n│\n│      *** 元模組特定鉤子 ***\n├── metamount.sh             (選用: 自訂掛載處理程式)\n├── metainstall.sh           (選用: 常規模組的安裝鉤子)\n├── metauninstall.sh         (選用: 常規模組的清理鉤子)\n│\n│      *** 標準模組檔案(全部選用) ***\n├── customize.sh             (安裝自訂)\n├── post-fs-data.sh          (post-fs-data 階段腳本)\n├── service.sh               (late_start service 腳本)\n├── boot-completed.sh        (啟動完成腳本)\n├── uninstall.sh             (元模組自己的解除安裝腳本)\n├── system/                  (無系統修改,如果需要)\n└── [任何其他檔案]\n```\n\n除了特殊的元模組鉤子外,元模組可以使用所有標準模組功能(生命週期腳本等)。\n\n### 鉤子腳本\n\n元模組可以提供最多三個特殊鉤子腳本:\n\n#### 1. metamount.sh - 掛載處理程式\n\n**目的**: 控制啟動期間模組的掛載方式。\n\n**執行時機**: 在 `post-fs-data` 階段,在任何模組腳本執行之前。\n\n**環境變數:**\n\n- `MODDIR`: 元模組的目錄路徑(例如 `/data/adb/modules/my_metamodule`)\n- 所有標準 KernelSU 環境變數\n\n**職責:**\n\n- 以無系統方式掛載所有已啟用的模組\n- 檢查 `skip_mount` 標誌\n- 處理特定模組的掛載要求\n\n::: danger 關鍵要求\n執行掛載操作時,**必須**將來源/裝置名稱設定為 `\"KSU\"`。這將掛載標識為屬於 KernelSU。\n\n**範例(正確):**\n\n```sh\nmount -t overlay -o lowerdir=/lower,upperdir=/upper,workdir=/work KSU /target\n```\n\n**對於現代掛載 API**,設定來源字串:\n\n```rust\nfsconfig_set_string(fs, \"source\", \"KSU\")?;\n```\n\n這對於 KernelSU 正確識別和管理其掛載至關重要。\n:::\n\n**範例腳本:**\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\n\n# 範例: 簡單的繫結掛載實作\nfor module in /data/adb/modules/*; do\n    if [ -f \"$module/disable\" ] || [ -f \"$module/skip_mount\" ]; then\n        continue\n    fi\n\n    if [ -d \"$module/system\" ]; then\n        # 使用 source=KSU 掛載(必需!)\n        mount -o bind,dev=KSU \"$module/system\" /system\n    fi\ndone\n```\n\n#### 2. metainstall.sh - 安裝鉤子\n\n**目的**: 自訂常規模組的安裝方式。\n\n**執行時機**: 在模組安裝期間,檔案提取後但安裝完成前。此腳本被內建安裝程式**引用**(而非執行),類似於 `customize.sh` 的運作方式。\n\n**環境變數和函式:**\n\n此腳本繼承內建 `install.sh` 的所有變數和函式:\n\n- **變數**: `MODPATH`、`TMPDIR`、`ZIPFILE`、`ARCH`、`API`、`IS64BIT`、`KSU`、`KSU_VER`、`KSU_VER_CODE`、`BOOTMODE` 等\n- **函式**:\n  - `ui_print <msg>` - 向主控台列印訊息\n  - `abort <msg>` - 列印錯誤並終止安裝\n  - `set_perm <target> <owner> <group> <permission> [context]` - 設定檔案權限\n  - `set_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]` - 遞迴設定權限\n  - `install_module` - 呼叫內建模組安裝程序\n\n**用例:**\n\n- 在內建安裝之前或之後處理模組檔案(準備好後呼叫 `install_module`)\n- 移動模組檔案\n- 驗證模組相容性\n- 設定特殊目錄結構\n- 初始化模組特定資源\n\n**注意**: 安裝元模組本身時**不會**呼叫此腳本。\n\n#### 3. metauninstall.sh - 清理鉤子\n\n**目的**: 解除安裝常規模組時清理資源。\n\n**執行時機**: 在模組解除安裝期間,在刪除模組目錄之前。\n\n**環境變數:**\n\n- `MODULE_ID`: 正在解除安裝的模組的 ID\n\n**用例:**\n\n- 處理檔案\n- 清理符號連結\n- 釋放配置的資源\n- 更新內部追蹤\n\n**範例腳本:**\n\n```sh\n#!/system/bin/sh\n# 解除安裝常規模組時呼叫\nMODULE_ID=\"$1\"\nIMG_MNT=\"/data/adb/metamodule/mnt\"\n\n# 從映像中刪除模組檔案\nif [ -d \"$IMG_MNT/$MODULE_ID\" ]; then\n    rm -rf \"$IMG_MNT/$MODULE_ID\"\nfi\n```\n\n### 執行順序\n\n了解啟動執行順序對於元模組開發至關重要:\n\n```txt\npost-fs-data 階段:\n  1. 執行通用 post-fs-data.d 腳本\n  2. 修剪模組,restorecon,載入 sepolicy.rule\n  3. 執行元模組的 post-fs-data.sh(如果存在)\n  4. 執行常規模組的 post-fs-data.sh\n  5. 載入 system.prop\n  6. 執行元模組的 metamount.sh\n     └─> 以無系統方式掛載所有模組\n  7. post-mount.d 階段執行\n     - 通用 post-mount.d 腳本\n     - 元模組的 post-mount.sh(如果存在)\n     - 常規模組的 post-mount.sh\n\nservice 階段:\n  1. 執行通用 service.d 腳本\n  2. 執行元模組的 service.sh(如果存在)\n  3. 執行常規模組的 service.sh\n\nboot-completed 階段:\n  1. 執行通用 boot-completed.d 腳本\n  2. 執行元模組的 boot-completed.sh(如果存在)\n  3. 執行常規模組的 boot-completed.sh\n```\n\n**要點:**\n\n- `metamount.sh` 在所有 post-fs-data 腳本(元模組和常規模組)**之後**執行\n- 元模組生命週期腳本(`post-fs-data.sh`、`service.sh`、`boot-completed.sh`)始終在常規模組腳本之前執行\n- `.d` 目錄中的通用腳本在元模組腳本之前執行\n- `post-mount` 階段在掛載完成後執行\n\n### 符號連結機制\n\n當安裝元模組時,KernelSU 會建立一個符號連結:\n\n```sh\n/data/adb/metamodule -> /data/adb/modules/<metamodule_id>\n```\n\n這為存取活動元模組提供了穩定的路徑,無論其 ID 如何。\n\n**好處:**\n\n- 一致的存取路徑\n- 輕鬆偵測活動元模組\n- 簡化設定\n\n### 真實範例: meta-overlayfs\n\n`meta-overlayfs` 元模組是官方參考實作。它展示了元模組開發的最佳實踐。\n\n#### 架構\n\n`meta-overlayfs` 使用**雙目錄架構**:\n\n1. **中繼資料目錄**: `/data/adb/modules/`\n   - 包含 `module.prop`、`disable`、`skip_mount` 標記\n   - 啟動期間快速掃描\n   - 儲存佔用小\n\n2. **內容目錄**: `/data/adb/metamodule/mnt/`\n   - 包含實際模組檔案(system、vendor、product 等)\n   - 儲存在 ext4 映像(`modules.img`)中\n   - 使用 ext4 功能最佳化空間\n\n#### metamount.sh 實作\n\n以下是 `meta-overlayfs` 如何實作掛載處理程式:\n\n```sh\n#!/system/bin/sh\nMODDIR=\"${0%/*}\"\nIMG_FILE=\"$MODDIR/modules.img\"\nMNT_DIR=\"$MODDIR/mnt\"\n\n# 如果尚未掛載,則掛載 ext4 映像\nif ! mountpoint -q \"$MNT_DIR\"; then\n    mkdir -p \"$MNT_DIR\"\n    mount -t ext4 -o loop,rw,noatime \"$IMG_FILE\" \"$MNT_DIR\"\nfi\n\n# 為雙目錄支援設定環境變數\nexport MODULE_METADATA_DIR=\"/data/adb/modules\"\nexport MODULE_CONTENT_DIR=\"$MNT_DIR\"\n\n# 執行掛載二進位檔案\n# (實際掛載邏輯在 Rust 二進位檔案中)\n\"$MODDIR/meta-overlayfs\"\n```\n\n#### 主要特性\n\n**Overlayfs 掛載:**\n\n- 使用核心 overlayfs 進行真正的無系統修改\n- 支援多個分割區(system、vendor、product、system_ext、odm、oem)\n- 透過 `/data/adb/modules/.rw/` 支援讀寫層\n\n**來源識別:**\n\n```rust\n// 來自 meta-overlayfs/src/mount.rs\nfsconfig_set_string(fs, \"source\", \"KSU\")?;  // 必需!\n```\n\n這為所有 overlay 掛載設定 `dev=KSU`,實現正確識別。\n\n### 最佳實踐\n\n開發元模組時:\n\n1. **始終將來源設定為\"KSU\"**以進行掛載操作 - 核心卸載和 zygisksu 卸載需要此設定才能正確卸載\n2. **優雅地處理錯誤** - 啟動程序對時間敏感\n3. **尊重標準標誌** - 支援 `skip_mount` 和 `disable`\n4. **記錄操作** - 使用 `echo` 或日誌記錄進行除錯\n5. **徹底測試** - 掛載錯誤可能導致啟動迴圈\n6. **記錄行為** - 清楚地解釋您的元模組做什麼\n7. **提供遷移路徑** - 協助使用者從其他解決方案切換\n\n### 測試您的元模組\n\n發布前:\n\n1. 在乾淨的 KernelSU 設定上**測試安裝**\n2. **驗證掛載**各種模組類型\n3. **檢查相容性**與常見模組\n4. **測試解除安裝**和清理\n5. **驗證啟動效能**(metamount.sh 是阻塞的!)\n6. **確保正確的錯誤處理**以避免啟動迴圈\n\n## 常見問題\n\n### 我需要元模組嗎?\n\n**對於使用者**: 僅當您想使用需要掛載的模組時。如果您只使用執行腳本而不修改系統檔案的模組,則不需要元模組。\n\n**對於模組開發者**: 不需要,您正常開發模組。僅當您的模組需要掛載時,使用者才需要元模組。\n\n**對於進階使用者**: 僅當您想自訂掛載行為或建立替代掛載實作時。\n\n### 我可以有多個元模組嗎?\n\n不可以。一次只能安裝一個元模組。這可以防止衝突並確保可預測的行為。\n\n### 如果我解除安裝了唯一的元模組會怎樣?\n\n模組將不再被掛載。您的裝置將正常啟動,但模組修改將不會套用,直到您安裝另一個元模組。\n\n### meta-overlayfs 是必需的嗎?\n\n不是。它提供與大多數模組相容的標準 overlayfs 掛載。如果您需要不同的行為,可以建立自己的元模組。\n\n## 另請參閱\n\n- [模組指南](module.md) - 通用模組開發\n- [與 Magisk 的差異](difference-with-magisk.md) - 比較 KernelSU 和 Magisk\n- [如何建置](how-to-build.md) - 從原始碼建置 KernelSU\n"
  },
  {
    "path": "website/docs/zh_TW/guide/module-config.md",
    "content": "# 模組配置\n\nKernelSU 提供了一個內建的配置系統,允許模組儲存持久化或暫時的鍵值設定。配置以二進位格式儲存在 `/data/adb/ksu/module_configs/<module_id>/`,具有以下特性:\n\n## 配置類型\n\n- **持久配置** (`persist.config`):重新開機後保留,直到明確刪除或解除安裝模組\n- **暫時配置** (`tmp.config`):在每次啟動時的 post-fs-data 階段自動清除\n\n讀取配置時,對於同一個鍵,暫時值優先於持久值。\n\n## 在模組腳本中使用配置\n\n所有模組腳本(`post-fs-data.sh`、`service.sh`、`boot-completed.sh` 等)執行時都會設定 `KSU_MODULE` 環境變數為模組 ID。您可以使用 `ksud module config` 命令來管理模組的配置:\n\n```bash\n# 獲取配置值\nvalue=$(ksud module config get my_setting)\n\n# 設定持久配置值\nksud module config set my_setting \"some value\"\n\n# 設定暫時配置值(重新開機後清除)\nksud module config set --temp runtime_state \"active\"\n\n# 列出所有配置項(合併持久和暫時配置)\nksud module config list\n\n# 刪除配置項\nksud module config delete my_setting\n\n# 刪除暫時配置項\nksud module config delete --temp runtime_state\n\n# 清除所有持久配置\nksud module config clear\n\n# 清除所有暫時配置\nksud module config clear --temp\n\n# 從 stdin 設定值(適用於多行或複雜資料)\nksud module config set my_key <<EOF\n多行\n文字值\nEOF\n\n# 或從命令管道輸入\necho \"value\" | ksud module config set my_key\n\n# 明確使用 stdin 標誌\ncat file.json | ksud module config set json_data --stdin\n```\n\n## 驗證限制\n\n配置系統強制執行以下限制:\n\n- **最大鍵長度**:256 位元組\n- **最大值長度**:1MB (1048576 位元組)\n- **最大配置項數**:每個模組 32 個\n- **鍵格式**:必須符合 `^[a-zA-Z][a-zA-Z0-9._-]+$`(與模組 ID 相同)\n  - 必須以字母(a-zA-Z)開頭\n  - 可包含字母、數字、點(`.`)、底線(`_`)或連字號(`-`)\n  - 最小長度:2 個字元\n- **值格式**:無限制 - 可包含任何 UTF-8 字元,包括換行符、控制字元等\n  - 以二進位格式儲存,帶長度前綴,確保安全處理所有資料\n\n## 生命週期\n\n- **啟動時**:所有暫時配置在 post-fs-data 階段清除\n- **模組解除安裝時**:所有配置(持久和暫時)自動刪除\n- 配置以二進位格式儲存,使用魔數 `0x4b53554d`(\"KSUM\")和版本驗證\n\n## 使用場景\n\n配置系統適用於:\n\n- **使用者偏好**:儲存使用者透過 WebUI 或 action 腳本配置的模組設定\n- **功能開關**:在不重新安裝的情況下啟用/停用模組功能\n- **執行時狀態**:追蹤應在重新開機時重置的暫時狀態(使用暫時配置)\n- **安裝設定**:記住模組安裝時做出的選擇\n- **複雜資料**:儲存 JSON、多行文字、Base64 編碼資料或任何結構化內容(最多 1MB)\n\n::: tip 最佳實踐\n- 對於應在重新開機後保留的使用者偏好,使用持久配置\n- 對於應在啟動時重置的執行時狀態或功能開關,使用暫時配置\n- 在腳本中使用配置值之前驗證它們\n- 使用 `ksud module config list` 命令偵錯配置問題\n:::\n\n## 進階功能\n\n模組配置系統提供了用於進階用例的特殊配置鍵:\n\n### 覆蓋模組描述 {#overriding-module-description}\n\n您可以透過設定 `override.description` 配置鍵來動態覆蓋 `module.prop` 中的 `description` 欄位:\n\n```bash\n# 覆蓋模組描述\nksud module config set override.description \"在管理器中顯示的自訂描述\"\n```\n\n當取得模組列表時,如果存在 `override.description` 配置,它將取代 `module.prop` 中的原始描述。這對於以下場景很有用:\n- 在模組描述中顯示動態狀態資訊\n- 向使用者顯示執行時配置詳情\n- 基於模組狀態更新描述而無需重新安裝\n\n### 宣告管理的功能\n\n模組可以使用 `manage.<feature>` 配置模式宣告它們管理的 KernelSU 功能。支援的功能對應於 KernelSU 內部的 `FeatureId` 列舉:\n\n**支援的功能:**\n- `su_compat` - SU 相容模式\n- `kernel_umount` - 核心自動卸載\n\n```bash\n# 宣告此模組管理 SU 相容性並將其啟用\nksud module config set manage.su_compat true\n\n# 宣告此模組管理核心卸載並將其停用\nksud module config set manage.kernel_umount false\n\n# 移除功能管理(模組不再控制此功能)\nksud module config delete manage.su_compat\n```\n\n**工作原理:**\n- `manage.<feature>` 鍵的存在表示模組正在管理該功能\n- 值表示期望的狀態:`true`/`1` 代表啟用,`false`/`0`(或任何其他值)代表停用\n- 要停止管理某個功能,請完全刪除該配置鍵\n\n管理的功能透過模組列表 API 以 `managedFeatures` 欄位(逗號分隔的字串)公開。這允許:\n- KernelSU 管理器偵測哪些模組管理哪些 KernelSU 功能\n- 防止多個模組嘗試管理同一功能時發生衝突\n- 更好地協調模組與核心 KernelSU 功能之間的關係\n\n::: warning 僅支援預定義功能\n僅使用上面列出的預定義功能名稱(`su_compat`、`kernel_umount`)。這些對應於實際的 KernelSU 內部功能。使用其他功能名稱不會導致錯誤,但沒有任何功能作用。\n:::\n"
  },
  {
    "path": "website/docs/zh_TW/guide/module-webui.md",
    "content": "# 模組 WebUI {#module-webui}\n\nKernelSU 的模組除了執行啟動腳本和修改系統檔案之外，還支援顯示 UI 介面和與使用者互動。\n\n該模組可以透過任何 Web 技術編寫 HTML + CSS + JavaScript 頁面。 KernelSU的管理器將透過 WebView 顯示這些頁面。它還提供了一些用於與系統互動的 JavaScript API，例如執行 shell 命令。\n\n## WebUI 根目錄 {#webroot-directory}\n\nWeb資源應放置在模組根目錄的 `webroot` 子目錄中，並且其中**必須**有一個名為 `index.html` 的文件，該檔案是模組頁面入口。\n\n包含Web介面的最簡單的模組結構如下：\n\n```txt\n❯ tree .\n.\n|-- module.prop\n`-- webroot\n    `-- index.html\n```\n\n:::warning 提醒\n安裝模組時，KernelSU 將自動設定`webroot`的權限和 SELinux context。如果您不知道自己在做什麼，請不要自行設定該目錄的權限！\n:::\n\n如果您的頁面包含 CSS 和 JavaScript，您也需要將其放入此目錄中。\n\n## JavaScript API\n\n如果只是一個顯示頁面，那和一般網頁沒有什麼不同。更重要的是，KernelSU 提供了一系列的系統 API，讓您可以實現模組獨特的功能。\n\nKernelSU [在 npm 上發布](https://www.npmjs.com/package/kernelsu)了一個 JavaScript 庫，您可以在網頁的 JavaScript 程式碼中使用它。\n\n例如，您可以執行 shell 命令來取得特定配置或修改屬性：\n\n```JavaScript\nimport { exec } from 'kernelsu';\n\nconst { errno, stdout } = exec(\"getprop ro.product.model\");\n```\n\n再例如，你可以讓網頁全螢幕顯示，或是顯示一個 Toast。\n\n[API 文檔](https://www.npmjs.com/package/kernelsu)\n\n如果您發現現有的 API 無法滿足您的需求或使用不方便，歡迎您在[這裡](https://github.com/tiann/KernelSU/issues)給我們建議！\n\n## 一些技巧 {#some-tips}\n\n1. 您可以正常使用 `localStorage` 來儲存一些數據，但解除安裝管理器後，這些數據將會遺失。如果需要持久保存，可以自行將資料寫入某個目錄。\n2. 對於簡單的頁面，我建議您使用 [parceljs](https://parceljs.org/) 進行打包。它無須設定，使用非常方便。不過，如果你是前端高手或有自己的喜好，那就選擇你喜歡的吧！\n"
  },
  {
    "path": "website/docs/zh_TW/guide/module.md",
    "content": "# 模組指南 {#module-guide}\n\nKernelSU 提供了一個模組機制，它可以在保持系統分割區完整性的同時達到修改系統分割區的效果；這種機制一般被稱為 systemless (無系統修改)。\n\nKernelSU 的模組運作機制與 Magisk 幾乎相同，如果您熟悉 Magisk 模組的開發，那麼開發 KernelSU 的模組大同小異，您可以跳過下列有關模組的介紹，只需要瞭解 [KernelSU 模組與 Magisk 模組的差異](difference-with-magisk.md)。\n\n::: warning METAMODULE 僅在修改系統檔案時需要\nKernelSU 使用 [metamodule](metamodule.md) 架構來掛載 `system` 目錄。**僅當您的模組需要修改 `/system` 檔案時**(透過 `system` 目錄)，您才需要安裝 metamodule(如 [meta-overlayfs](https://github.com/tiann/KernelSU/releases))。其他模組功能如腳本、sepolicy 規則和 system.prop 無需 metamodule 即可運作。\n:::\n\n## WebUI\n\nKernelSU 的模組支援顯示互動介面，請參閱 [WebUI 文檔](module-webui.md).\n\n## 模組配置\n\nKernelSU 提供了一個內建的配置系統，允許模組儲存持久化或暫時的鍵值設定。詳情請參閱[模組配置文檔](module-config.md)。\n\n## Busybox\n\nKernelSU 提供了一個完備的 BusyBox 二進位檔案 (包括完整的 SELinux 支援)。可執行檔位於 `/data/adb/ksu/bin/busybox`。\nKernelSU 的 BusyBox 支援執行時可切換的 \"ASH 獨立模式\"。\nASH 獨立模式在執行 BusyBox 時，每個命令都會直接使用 BusyBox 中內建的應用程式，而不論 `PATH` 的設定為何。\n例如，`ls`、`rm`、`chmod` 等命令將不會使用 `PATH` 中設定的命令 (在 Android 的狀況下，預設狀況下分別為 `/system/bin/ls`、`/system/bin/rm` 和 `/system/bin/chmod`)，而是直接呼叫 BusyBox 內建的應用程式。\n這確保了腳本始終在可預測的環境中執行，並始終具有完整的命令套件，不論它執行在哪個 Android 版本上。\n要強制下一個命令不使用 BusyBox，您必須使用完整路徑呼叫可執行檔。\n\n每個基於 KernelSU 上下文的腳本都將在 BusyBox 的獨立模式執行。對於第三方開發人員而言，這包括所有開機腳本和模組安裝腳本。\n\n對於想要在 KernelSU 之外使用這個「獨立模式」功能的使用者，有兩種啟用方法：\n\n1. 將環境變數 `ASH_STANDALONE` 設為 `1`。例如：`ASH_STANDALONE=1 /data/adb/ksu/bin/busybox sh <script>`\n2. 使用命令列選項切換：`/data/adb/ksu/bin/busybox sh -o standalone <script>`\n\n為了確保所有後續的 `sh` shell 都在獨立模式下執行，第一種是首選方法 (這也是 KernelSU 和 KernelSU 管理器內部使用的方法)，因為環境變數會被繼承到子執行緒中。\n\n::: tip 與 Magisk 的差異\nKernelSU 的 BusyBox 現在是直接使用 Magisk 專案編譯的二進位檔案，**感謝 Magisk！**\n因此，您完全不必擔心 BusyBox 腳本與在 Magisk 和 KernelSU 之間的相容性問題，因為它們完全相同！\n:::\n\n## KernelSU 模組 {#kernelsu-modules}\n\nKernelSU 模組是一個放置於 `/data/adb/modules` 且滿足下列結構的資料夾：\n\n```txt\n/data/adb/modules\n├── .\n├── .\n|\n├── $MODID                  <--- 模組的資料夾名稱與模組 ID 相同\n│   │\n│   │      *** 模組識別 ***\n│   │\n│   ├── module.prop         <--- 這個檔案儲存與模組相關的中繼資料，例如模組 ID、版本等\n│   │\n│   │      *** 主要內容 ***\n│   │\n│   ├── system              <--- 這個資料夾會在 skip_mount 不存在時被掛接至系統\n│   │   ├── ...\n│   │   ├── ...\n│   │   └── ...\n│   │\n│   │      *** 狀態旗標 ***\n│   │\n│   ├── skip_mount          <--- 如果這個檔案存在，那麼 KernelSU 將不會掛接您的系統資料夾\n│   ├── disable             <--- 如果這個檔案存在，那麼模組將會被停用\n│   ├── remove              <--- 如果這個檔案存在，那麼模組將會在下次重新開機時被移除\n│   │\n│   │      *** 選用檔案 ***\n│   │\n│   ├── post-fs-data.sh     <--- 這個腳本將會在 post-fs-data 中執行\n│   ├── service.sh          <--- 這個腳本將會在 late_start 服務中執行\n|   ├── uninstall.sh        <--- 這個腳本將會在 KernelSU 移除模組時執行\n│   ├── system.prop         <--- 這個檔案中指定的屬性將會在系統啟動時透過 resetprop 變更\n│   ├── sepolicy.rule       <--- 這個檔案中的 SELinux 原則將會在系統開機時載入\n│   │\n│   │      *** 自動產生的目錄，不要手動建立或修改！ ***\n│   │\n│   ├── vendor              <--- A symlink to $MODID/system/vendor\n│   ├── product             <--- A symlink to $MODID/system/product\n│   ├── system_ext          <--- A symlink to $MODID/system/system_ext\n│   │\n│   │      *** 允許的其他額外檔案/資料夾 ***\n│   │\n│   ├── ...\n│   └── ...\n|\n├── another_module\n│   ├── .\n│   └── .\n├── .\n├── .\n```\n\n::: tip 與 Magisk 的差異\nKernelSU 沒有內建的針對 Zygisk 的支援，因此模組中沒有與 Zygisk 相關的內容，但您可以透過安裝 [ZygiskNext](https://github.com/Dr-TSNG/ZygiskNext) 以支援 Zygisk 模組，此時 Zygisk 模組的內容與 Magisk 所支援的 Zygisk 完全相同。\n:::\n\n### module.prop\n\nmodule.prop 是一個模組的設定檔，在 KernelSU 中如果模組中不包含這個檔案，那麼它將不被認為是一個模組；這個檔案的格式如下：\n\n```txt\nid=<string>\nname=<string>\nversion=<string>\nversionCode=<int>\nauthor=<string>\ndescription=<string>\nupdateJson=<url> (optional)\nactionIcon=<path> (optional)\nwebuiIcon=<path> (optional)\n```\n\n- id 必須與這個正則表達式相符：`^[a-zA-Z][a-zA-Z0-9._-]+$` 例如：✓ `a_module`，✓ `a.module`，✓ `module-101`，✗ `a module`，✗ `1_module`，✗ `-a-module`。這是您的模組的唯一識別碼，發表後將無法變更。\n- versionCode 必須是一個整數，用於比較版本。\n- 其他未在上方提到的內容可以是任何單行字串。\n- 請確保使用 `UNIX (LF)` 分行符號類型，而非 `Windows (CR + LF)` 或 `Macintosh (CR)`。\n- actionIcon 與 webuiIcon 是可選的圖示路徑，會作為管理器中模組\n  Action 捷徑與 WebUI 捷徑的預設圖示。這些路徑必須是以模組根目錄為\n  基準的相對路徑。例如：`actionIcon=icon/icon.png`\n  會解析為 `<MODDIR>/icon/icon.png`。\n\n::: tip 動態描述\n`description` 欄位可以在執行階段使用模組配置系統動態覆蓋。詳情請參閱[覆蓋模組描述](module-config.md#overriding-module-description)。\n:::\n\n### Shell 腳本 {#shell-scripts}\n\n請閱讀 [開機腳本](#boot-scripts) 章節，以瞭解 `post-fs-data.sh` 和 `service.sh` 之間的差別。對於大多數模組開發人員來說，如果您只需要執行一個開機腳本，`service.sh` 應該已經足夠了。\n如果您需要在啟動完成後執行腳本，請使用 `boot-completed.sh`。\n如果你想在掛載 overlayfs 後做一些事情，請使用 `post-mount.sh`。\n\n在您的模組中的所有腳本中，請使用 `MODDIR=${0%/*}` 以取得您的模組基本目錄路徑；請不要在腳本中以硬式編碼的方式加入您的模組路徑。\n\n:::tip 與 Magisk 的差異\n您可以透過環境變數 `KSU` 來判斷腳本是執行在 KernelSU 還是 Magisk 中，如果執行在 KernelSU，這個值會被設為 `true`。\n:::\n\n### `system` 目錄 {#system-directories}\n\n這個目錄的內容會在系統啟動後，以 `overlayfs` 的方式覆疊在系統的 `/system` 分區之上，這表示：\n\n1. 系統中對應目錄的相同名稱的檔案會被此目錄中的檔案覆寫。\n2. 系統中對應目錄的相同名稱的檔案會與此目錄的檔案合併。\n\n如果您想要刪除系統先前的目錄中的某個檔案或資料夾，您需要在模組目錄中透過 `mknod filename c 0 0` 以建立一個 `filename` 的相同名稱的檔案；這樣 overlayfs 系統會自動「whiteout」等效刪除這個檔案 (`/system` 分割區並未被變更)。\n\n您也可以在 `customize.sh` 中宣告一個名為 `REMOVE` 並且包含一系列目錄的變數以執行移除作業，KernelSU 會自動為您在模組對應目錄執行 `mknod <TARGET> c 0 0`。例如：\n\n```sh\nREMOVE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n```\n\n上方的清單將會執行：`mknod $MODPATH/system/app/YouTuBe c 0 0` 和 `mknod $MODPATH/system/app/Bloatware c 0 0`；並且 `/system/app/YouTube` 和 `/system/app/Bloatware` 將會在模組生效前移除。\n\n如果您想要取代系統的某個目錄，您需要在模組目錄中建立一個相同路徑的目錄，然後為此目錄設定此屬性：`setfattr -n trusted.overlay.opaque -v y <TARGET>`；這樣 overlayfs 系統會自動將對應目錄取代 (`/system` 分割區並未被變更)。\n\n您可以在 `customize.sh` 中宣告一個名為 `REMOVE` 並且包含一系列目錄的變數以執行移除作業，KernelSU 會自動為您在模組對應目錄執行相關作業。例如：\n\n```sh\nREPLACE=\"\n/system/app/YouTube\n/system/app/Bloatware\n\"\n```\n\n上方的清單將會執行：自動建立目錄 `$MODPATH/system/app/YouTube` 和 `$MODPATH//system/app/Bloatware`，然後執行 `setfattr -n trusted.overlay.opaque -v y $$MODPATH/system/app/YouTube` 和 `setfattr -n trusted.overlay.opaque -v y $$MODPATH/system/app/Bloatware`；並且 `/system/app/YouTube` 和 `/system/app/Bloatware` 將會在模組生效後被取代為空白目錄。\n\n::: tip 與 Magisk 的差異\n\nKernelSU 的 systemless 機制透過核心的 overlayfs 實作，而 Magisk 目前則是透過 magic mount (bind mount)，兩者的實作方式有很大的差別，但最終的目標是一致的：不修改實際的 `/system` 分區但修改 `/system` 檔案。\n:::\n\n如果您對 overlayfs 感興趣，建議閱讀 Linux Kernel 關於 [overlayfs 的文檔](https://docs.kernel.org/filesystems/overlayfs.html)\n\n### system.prop\n\n這個檔案的格式與 `build.prop` 完全相同：每一行都是由 `[key]=[value]` 組成。\n\n### sepolicy.rule\n\n如果您的模組需要一些額外 sepolicy 修補，請將這些原則新增至這個檔案中。這個檔案的每一行都將被視為一個原則陳述。\n\n## 模組安裝程式 {#module-installer}\n\nKernelSU 的模組安裝程式就是一個可以透過 KernelSU 管理員應用程式刷新的 Zip 檔案，這個 Zip 檔案的格式如下：\n\n```txt\nmodule.zip\n│\n├── customize.sh                       <--- (Optional, more details later)\n│                                           This script will be sourced by update-binary\n├── ...\n├── ...  /* 其他模块文件 */\n│\n```\n\n:::warning 警告\nKernelSU 模組不支援在 Recovery 中安裝！！\n:::\n\n### 自訂安裝過程 {#customizing-installation}\n\n如果您想要控制模組的安裝過程，可以在模組的目錄下建立一個名為 `customize.sh` 的檔案，這個檔案將會在模組被解壓縮後經由 **source** 調用，如果您的模組需要依據裝置的 API 版本或裝置架構執行一些額外的作業，這個腳本將非常有用。\n\n如果您想完全控制腳本的安裝過程，您可以在 `customize.sh` 中宣告 `SKIPUNZIP=1` 以跳過所有的預設安裝步驟。此時，您需要自行處理所有的安裝過程(例如解壓縮模組、設定權限等)。\n\n`customize.sh` 腳本以「獨立模式」執行在 KernelSU 的 BusyBox `ash` shell 中。您可以使用下列變數和函式：\n\n#### 變數 {#variables}\n\n- `KSU` (bool): 標示此腳本執行於 KernelSU 環境中，此變數的值將永遠為 `true`，您可以透過它與 Magisk 進行區分。\n- `KSU_VER` (string): KernelSU 目前的版本名稱 (例如 `v0.4.0`)\n- `KSU_VER_CODE` (int): KernelSU 使用者空間目前的版本代碼 (例如 `10672`)\n- `KSU_KERNEL_VER_CODE` (int): KernelSU 核心空間目前的版本代碼 (例如 `10672`)\n- `BOOTMODE` (bool): 此變數在 KernelSU 中永遠為 `true`\n- `MODPATH` (path): 目前模組的安裝目錄\n- `TMPDIR` (path): 可以存放暫存檔的位置\n- `ZIPFILE` (path): 目前模組的安裝程式 Zip\n- `ARCH` (string): 裝置的 CPU 架構，有這幾種：`arm`, `arm64`, `x86`, or `x64`\n- `IS64BIT` (bool): 是否為 64 位元裝置\n- `API` (int): 目前裝置的 Android API 版本 (例如 Android 6.0 上為 `23`)\n\n::: warning 警告\n`MAGISK_VER_CODE` 在 KernelSU 永遠為 `25200`，`MAGISK_VER` 則為 `v25.2`，請不要透過這兩個變數來判斷是否為 KernelSU！\n:::\n\n#### 函式 {#functions}\n\n```txt\nui_print <msg>\n    print <msg> to console\n    Avoid using 'echo' as it will not display in custom recovery's console\n\nabort <msg>\n    print error message <msg> to console and terminate the installation\n    Avoid using 'exit' as it will skip the termination cleanup steps\n\nset_perm <target> <owner> <group> <permission> [context]\n    if [context] is not set, the default is \"u:object_r:system_file:s0\"\n    this function is a shorthand for the following commands:\n       chown owner.group target\n       chmod permission target\n       chcon context target\n\nset_perm_recursive <directory> <owner> <group> <dirpermission> <filepermission> [context]\n    if [context] is not set, the default is \"u:object_r:system_file:s0\"\n    for all files in <directory>, it will call:\n       set_perm file owner group filepermission context\n    for all directories in <directory> (including itself), it will call:\n       set_perm dir owner group dirpermission context\n```\n\n## 開機腳本 {#boot-scripts}\n\n在 KernelSU 中，依據腳本執行模式的不同分為兩種：post-fs-data 模式和 late_start 服務模式。\n\n- post-fs-data 模式\n  - 這個階段是 **阻塞** 的。在執行完成之前或 10 秒鐘之後，開機程序會被暫停。\n  - 腳本在任何模組被掛接之前執行。這使模組開發人員可以在模組被掛接之前動態調整他們的模組。\n  - 這個階段發生在 Zygote 啟動之前，這意味著 Android 中的一切。\n  - **警告**: 使用 `setprop` 會導致開機程序死鎖！請使用 `resetprop -n <prop_name> <prop_value>` 替代。\n  - **僅在必要時在此模式中執行腳本**。\n\n- late_start 服務模式\n  - 這個階段是 **非阻塞** 的。您的腳本會與其餘的啟動程序**平行**執行。\n  - **大多數腳本建議在這種模式下執行**。\n\n在 KernelSU 中，開機腳本依據存放位置的不同還分為兩種：一般腳本和模組腳本。\n\n- 一般腳本\n  - 放置於 `/data/adb/post-fs-data.d` 或 `/data/adb/service.d` 中。\n  - 僅有腳本被設為可執行 (`chmod +x script.sh`) 時才會被執行。\n  - 在 `post-fs-data.d` 中的腳本以 post-fs-data 模式執行，在 `service.d` 中的腳本以 late_start 服務模式執行。\n  - 模組**不應**在安裝程序中新增一般腳本。\n\n- 模組腳本\n  - 放置於模組自己的資料夾中。\n  - 僅有在模組啟用時才會執行。\n  - `post-fs-data.sh` 以 post-fs-data 模式運行，`post-mount.sh` 在 overlayfs 掛載後運行，而 `service.sh` 則以 late_start 服務模式運行，`boot-completed` 在 Android 系統啟動完畢後以服務模式運作。\n\n所有啟動腳本都將在 KernelSU 的 BusyBox ash shell 中執行，並啟用**獨立模式**。\n\n### 開機腳本解釋 {#boot-scripts-process-explanation}\n\n以下是Android的相關啟動流程（部分省略），其中包括KernelSU的運行（帶前導星號），可以幫助您更好地理解這些模組腳本的用途：\n```txt\n0. Bootloader (nothing on screen)\nload patched boot.img\nload kernel:\n    - GKI mode: GKI kernel with KernelSU integrated\n    - LKM mode: stock kernel\n...\n\n1. kernel exec init (oem logo on screen):\n    - GKI mode: stock init\n    - LKM mode: exec ksuinit, insmod kernelsu.ko, exec stock init\nmount /dev, /dev/pts, /proc, /sys, etc.\nproperty-init -> read default props\nread init.rc\n...\nearly-init -> init -> late_init\nearly-fs\n   start vold\nfs\n  mount /vendor, /system, /persist, etc.\npost-fs-data\n  *safe mode check\n  *execute general scripts in post-fs-data.d/\n  *load sepolicy.rule\n  *mount tmpfs\n  *execute module scripts post-fs-data.sh\n    **(Zygisk)./bin/zygisk-ptrace64 monitor\n  *(pre)load system.prop (same as resetprop -n)\n  *remount modules /system\n  *execute general scripts in post-mount.d/\n  *execute module scripts post-mount.sh\nzygote-start\nload_all_props_action\n  *execute resetprop (actual set props for resetprop with -n option)\n... -> boot\n  class_start core\n    start-service logd, console, vold, etc.\n  class_start main\n    start-service adb, netd (iptables), zygote, etc.\n\n2. kernel2user init (rom animation on screen, start by service bootanim)\n*execute general scripts in service.d/\n*execute module scripts service.sh\n*set props for resetprop without -p option\n  **(Zygisk) hook zygote (start zygiskd)\n  **(Zygisk) mount zygisksu/module.prop\nstart system apps (autostart)\n...\nboot complete (broadcast ACTION_BOOT_COMPLETED event)\n*execute general scripts in boot-completed.d/\n*execute module scripts boot-completed.sh\n\n3. User operable (lock screen)\ninput password to decrypt /data/data\n*actual set props for resetprop with -p option\nstart user apps (autostart)\n```\n\n如果您對 Android 的 Init 語言感興趣，建議閱讀它的 [文檔](https://android.googlesource.com/platform/system/core/+/master/init/README.md).\n\n## Late-load 模式 {#late-load-mode}\n\n除了上述標準啟動流程外，KernelSU 還支援 **late-load 模式**，用於 LKM（可載入核心模組）場景。在該模式下，KernelSU 核心模組在**系統完全啟動後**載入，而非在 init 過程中載入。\n\n### 什麼時候觸發 late-load？\n\n透過執行 `ksud late-load` 命令觸發。該命令會：\n\n1. 偵測當前 KMI 版本，從內嵌資源中載入對應的 `kernelsu.ko`。\n2. 執行模組初始化（SELinux 規則、白名單、feature 等），這些工作在標準啟動中發生在 boot 階段。\n\n由於系統已經完全運行，某些啟動時的機制不可用或不需要。\n\n### 與標準啟動的差異\n\n| 行為 | 標準啟動 | Late-load 模式 |\n|------|:---:|:---:|\n| 核心模組由 init (PID 1) 載入 | 是 | 否（啟動後載入） |\n| ksud 的 kprobe 鉤子 (execve/read/fstat/input) | 是 | 跳過 |\n| 安全模式偵測（音量鍵） | 是 | 始終停用 |\n| 啟動日誌擷取 (logcat/dmesg) | 是 | 跳過 |\n| Magisk 共存偵測 | 是 | 跳過 |\n| `post-fs-data` 事件通知核心 | 是 | 跳過 |\n| `boot-completed` 事件通知核心 | 是 | 初始化時直接設定 |\n| `post-fs-data.sh` / `post-fs-data.d/` 指令碼 | 是 | 由 `late-load` 階段替代 |\n| `system.prop` 載入 | 是 | 是 |\n| OverlayFS 掛載（metamodule） | 是 | 是 |\n| `post-mount.sh` / `post-mount.d/` 指令碼 | 是 | 是 |\n| `service.sh` / `service.d/` 指令碼 | 是 | 是 |\n| `boot-completed.sh` / `boot-completed.d/` 指令碼 | 是 | 是 |\n| `KSU_LATE_LOAD` 環境變數 | 未設定 | 設定為 `1` |\n| 核心 info 標誌位 `0x4` | 未設定 | 已設定 |\n\n### 指令碼執行順序\n\n在 late-load 模式下，指令碼執行順序如下：\n\n```txt\nksud late-load:\n  1. 載入 kernelsu.ko（如果尚未載入）\n  2. 釋放二進位檔案、處理模組更新、載入 SELinux 規則、初始化 feature\n  3. 執行 late-load.d/ 通用指令碼和模組的 late-load 指令碼（阻塞）\n  4. 載入 system.prop (resetprop -n)\n  5. 執行 metamodule mount 指令碼（OverlayFS 掛載）\n  6. 執行 post-mount.d/ 通用指令碼和模組的 post-mount.sh（阻塞）\n  7. 執行 service.d/ 通用指令碼和模組的 service.sh（非阻塞）\n  8. 執行 boot-completed.d/ 通用指令碼和模組的 boot-completed.sh（非阻塞）\n```\n\n### Late-load 專用指令碼\n\n模組可以提供 `late-load.sh` 指令碼，該指令碼**僅在 late-load 模式下執行**，作為 `post-fs-data.sh` 的替代。該指令碼在 OverlayFS 掛載之前執行，與標準流程中的 `post-fs-data.sh` 時機類似。\n\n此外，通用指令碼可以放置在 `/data/adb/late-load.d/` 目錄下，在該階段執行。\n\n### 在指令碼中偵測 late-load 模式\n\n模組可以透過 `KSU_LATE_LOAD` 環境變數偵測當前是否處於 late-load 模式：\n\n```sh\nif [ \"$KSU_LATE_LOAD\" = \"1\" ]; then\n    # 當前處於 late-load 模式\n    echo \"Late-load mode detected\"\nfi\n```\n\n這使得模組可以據此調整自身行為，例如跳過僅在早期啟動時才需要的操作。\n"
  },
  {
    "path": "website/docs/zh_TW/guide/rescue-from-bootloop.md",
    "content": "# 搶救開機迴圈 {#intruduction}\n\n在寫入裝置時，我們很可能會遇到裝置「變磚」的狀況，從理論上講，如果您只是使用 Fastboot 寫入 Boot 分區或者安裝不合適的模組導致裝置無法開機，那麼這都可以透過合適的作業還原您的手機；這個文件提供一些緊急方法可以讓您在「變磚」中還原。\n\n## 寫入 Boot 時變磚 {#brick-by-flashing-boot-partition}\n\n在 KernelSU 中，寫入 boot 時變磚有下列原因：\n\n1. 你寫入了錯誤格式的 Boot 映像。比如您的手機 Boot 格式為  `gz`，但您寫入 `lz4` 格式的映像，那麼此時手機將無法開機。\n2. 您的手機需要關閉 AVB 驗證才可正常開機 (這通常需要抹除手機上的所有資料)。\n3. 您的核心存在某些錯誤或您的核心並不適合這部手機寫入。\n\n無論哪種狀況，您都可以透過**寫入原廠 Boot** 還原；因此，在安裝教學最開始，我們已經強烈建議大家，在寫入之前備份自己的原廠 Boot！如果您沒有備份，那麼您可以透過其他與您相同裝置的使用者或官方韌體擷取 Boot。\n\n## 安裝模組變磚 {#brick-by-modules}\n\n安裝模組變磚可能是大家遇到的更常見的狀況，但是這裡要嚴正警示大家：**不要安裝未知來源的模組！！**。因為模組擁有 Root 權限，它能完全對您的裝置造成無法復原的損壞！\n\n### 一般模組 {#normal-modules}\n\n如果大家安裝了某些開放原始碼的或者被證明是安全的模組使手機無法開機，那麼這種狀況在 KernelSU 中非常容易還原，也無需擔心。KernelSU 內建了下列兩種機制以搶救您的裝置：\n\n1. AB 更新\n2. 透過按下「音量 -」搶救\n\n#### AB 更新 {#ab-update}\n\nKernelSU 的模組借鑒了 Android 系統 OTA 更新時的 AB 更新機制，如果您安裝了新模組或者對現存模組進行了更新作業，不會直接修改目前使用的模組檔案，而是會把所有模組建置為另外一個更新映像；系統重新啟動後，會使用這個更新映像嘗試重新啟動一次，如果 Android 系統成功開機，模組才會真正更新。\n\n因此，最簡單最常用的搶救方法就是：**強制重新開機一次**。如果您在刷新某個模組之後系統無法開機，您可以長按電源按鈕超過 10 秒，系統會自動重新開機；模組會回復為更新前的狀態，先前更新的模組也將會被自動停用。\n\n#### 透過按下「音量 -」搶救 {#rescue-by-pressing-volume-down}\n\n如果 AB 更新仍然無法解決，您可以嘗試使用**安全模式**。進入安全模式之後，所有的模組將會被停用。\n\n進入安全模式的方法有兩種：\n\n1. 某些系統內建的安全模式：有些系統是長按「音量 -」，有些系統 (例如 MIUI) 可以在 Recovery 中啟用安全模式。進入系統的安全模式後，KernelSU 也會進入安全模式，並自動停用模組。\n2. KernelSU 內建的安全模式：開機第一個畫面後，**連續按下「音量 -」按鈕超過三次**。注意是按下-抬起、按下-抬起、按下-抬起，並非一直按下。\n\n進入安全模式後，KernelSU 管理員的模組頁面的所有模組將會被停用，但您可以執行「解除安裝」作業，將可能存在問題的模組解除安裝。\n\n內建的安全模式在核心中實作，因此不會出現按鍵活動無法攔截的狀況。不過對於非 GKI 核心，可能需要手動整合程式碼，可以參閱官方文件指南。\n\n### 惡意模組 {#malicious-modules}\n\n如果以上方法無法搶救您的裝置，那麼很可能您安裝的模組存在惡意作業或透過其他方式損壞了您的裝置，在這種狀況下，只有兩個建議：\n\n1. 抹除資料並恢復為官方系統。\n2. 諮詢售後服務。\n"
  },
  {
    "path": "website/docs/zh_TW/guide/unofficially-support-devices.md",
    "content": "# 非官方支援裝置\n\n::: warning 警告\n該文件僅供存檔參考，不再維護更新。\n自 KernelSU v1.0 版本之後，我們放棄了對非 GKI 裝置的官方支援。\n:::\n\n::: warning 警告\n本文件列出由其他開發人員維護的支援 KernelSU 的非 GKI 裝置核心\n:::\n\n::: warning 警告\n本文件僅便於尋找裝置對應原始碼，這並非意味著這些原始碼被 KernelSU 開發人員**審查**，您應自行承擔風險。\n:::\n\n<script setup>\nimport data from '../../repos.json'\n</script>\n\n<table>\n   <thead>\n      <tr>\n         <th>維護者</th>\n         <th>存放庫</th>\n         <th>支援裝置</th>\n      </tr>\n   </thead>\n   <tbody>\n    <tr v-for=\"repo in data\" :key=\"repo.devices\">\n        <td><a :href=\"repo.maintainer_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.maintainer }}</a></td>\n        <td><a :href=\"repo.kernel_link\" target=\"_blank\" rel=\"noreferrer\">{{ repo.kernel_name }}</a></td>\n        <td>{{ repo.devices }}</td>\n    </tr>\n   </tbody>\n</table>"
  },
  {
    "path": "website/docs/zh_TW/guide/what-is-kernelsu.md",
    "content": "# 什麼是 KernelSU？ {#what-is-kernelsu}\n\nKernelSU 是 Android GKI 裝置的 Root 解決方案，它以核心模式運作，並直接在核心空間中為使用者空間應用程式授予 Root 權限。\n\n## 功能 {#features}\n\nKernelSU 的主要功能是它是**基於核心的**。 KernelSU 在核心空間中執行，所以它可以向我們提供從未有過的核心介面。例如，我們可以在核心模式中為任何處理程序新增硬體中斷點；我們可以在任何處理程序的實體記憶體中存取，而無人知曉；我們可以在核心空間攔截任何系統呼叫；等等。\n\n此外，KernelSU 提供了 [metamodule 系統](metamodule.md)，這是一個用於模組管理的可插拔架構。與傳統 root 解決方案將掛載邏輯內建於核心的做法不同，KernelSU 將此工作委託給 metamodule。這允許您安裝 metamodule(如 [meta-overlayfs](https://github.com/tiann/KernelSU/tree/main/userspace/meta-overlayfs))來提供對 `/system` 分區和其他分區的 systemless 修改。\n\n## 如何使用 {#how-to-use}\n\n請參閱：[安裝](installation)\n\n## 如何建置 {#how-to-build}\n\n请參閱：[如何建置](how-to-build)\n\n## 討論 {#discussion}\n\n- Telegram: [@KernelSU](https://t.me/KernelSU)\n"
  },
  {
    "path": "website/docs/zh_TW/index.md",
    "content": "---\nlayout: home\ntitle: 基於核心的 Android Root 解決方案\n\nhero:\n  name: KernelSU\n  text: 基於核心的 Android root 解決方案\n  tagline: \"\"\n  image:\n    src: /logo.png\n    alt: KernelSU\n  actions:\n    - theme: brand\n      text: 開始瞭解\n      link: /zh_TW/guide/what-is-kernelsu\n    - theme: alt\n      text: 在 GitHub 中檢視\n      link: https://github.com/tiann/KernelSU\n\nfeatures:\n  - title: 以核心為基礎\n    details: KernelSU 以 Linux 核心模式運作，對使用者空間有更強的掌控。\n  - title: 白名單存取控制\n    details: 僅有被授予 Root 權限的應用程式才可存取 su，而其他應用程式完全無法知悉。\n  - title: 可定制的 Root 權限\n    details: KernelSU 能夠對 su 的使用者ID（uid）、群組ID（gid）、群組、權限，以及 SELinux 規則進行客製化管理，以此加強 root 權限的安全性。\n  - title: Metamodule 系統\n    details: 可插拔的模組基礎架構,允許 systemless 方式修改 /system。安裝 metamodule(如 meta-overlayfs)以啟用模組掛載功能。\n\n"
  },
  {
    "path": "website/package.json",
    "content": "{\n  \"name\": \"KernelSU_website\",\n  \"version\": \"1.0.0\",\n  \"description\": \"KernelSU official website: https://kernelsu.org\",\n  \"main\": \"index.js\",\n  \"repository\": \"https://kernelsu.org\",\n  \"author\": \"weishu\",\n  \"license\": \"MIT\",\n  \"devDependencies\": {\n    \"vitepress\": \"^1.6.4\",\n    \"vue\": \"^3.5.22\"\n  },\n  \"scripts\": {\n    \"docs:dev\": \"vitepress dev docs\",\n    \"docs:build\": \"vitepress build docs\",\n    \"docs:preview\": \"vitepress preview docs\"\n  },\n  \"type\": \"module\"\n}"
  }
]